From 4850bccff38a923f1098d300c4fe9587ba1c128d Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Wed, 30 Mar 2016 14:29:17 +0200 Subject: [PATCH 1/4] lingo: add generic cell handling all invoice categories (#10483) Migrate data from existing items cell to the new one --- combo/apps/lingo/migrations/0018_invoicescell.py | 39 ++++++ ...9_manual_migrate_invoice_cells_20160404_1356.py | 76 +++++++++++ combo/apps/lingo/models.py | 64 +++++++++ combo/fields.py | 143 +++++++++++++++++++++ tests/test_lingo_cells.py | 15 +++ 5 files changed, 337 insertions(+) create mode 100644 combo/apps/lingo/migrations/0018_invoicescell.py create mode 100644 combo/apps/lingo/migrations/0019_manual_migrate_invoice_cells_20160404_1356.py create mode 100644 combo/fields.py diff --git a/combo/apps/lingo/migrations/0018_invoicescell.py b/combo/apps/lingo/migrations/0018_invoicescell.py new file mode 100644 index 0000000..a61bfa8 --- /dev/null +++ b/combo/apps/lingo/migrations/0018_invoicescell.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import ckeditor.fields +import combo.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0001_initial'), + ('data', '0016_feedcell_limit'), + ('lingo', '0017_auto_20160327_0831'), + ] + + operations = [ + migrations.CreateModel( + name='InvoicesCell', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('placeholder', models.CharField(max_length=20)), + ('order', models.PositiveIntegerField()), + ('slug', models.SlugField(verbose_name='Slug', blank=True)), + ('public', models.BooleanField(default=True, verbose_name='Public')), + ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), + ('regie', models.CharField(max_length=50, verbose_name='Regie', blank=True)), + ('title', models.CharField(max_length=200, verbose_name='Title', blank=True)), + ('text', ckeditor.fields.RichTextField(null=True, verbose_name='Text', blank=True)), + ('categories', combo.fields.MultiSelectField(max_length=11, verbose_name='Categories', choices=[(b'active', 'Active'), (b'past', 'Past')])), + ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), + ('page', models.ForeignKey(to='data.Page')), + ], + options={ + 'verbose_name': 'Invoices', + }, + bases=(models.Model,), + ), + ] diff --git a/combo/apps/lingo/migrations/0019_manual_migrate_invoice_cells_20160404_1356.py b/combo/apps/lingo/migrations/0019_manual_migrate_invoice_cells_20160404_1356.py new file mode 100644 index 0000000..266f8e4 --- /dev/null +++ b/combo/apps/lingo/migrations/0019_manual_migrate_invoice_cells_20160404_1356.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def migrate_activeitems_cells(apps, schema_editor): + InvoicesCell = apps.get_model('lingo', 'InvoicesCell') + Page = apps.get_model('data', 'Page') + ActiveItems = apps.get_model('lingo', 'ActiveItems') + for cell in ActiveItems.objects.all(): + page = Page.objects.get(pk=cell.page_id) + InvoicesCell.objects.get_or_create(order=cell.order, + page=page, placeholder=cell.placeholder, + public=cell.public, regie=cell.regie, + restricted_to_unlogged=cell.restricted_to_unlogged, + slug=cell.slug, text=cell.text, title=cell.title, + categories='active' + ) + cell.delete() + +def migrate_itemshistory_cells(apps, schema_editor): + InvoicesCell = apps.get_model('lingo', 'InvoicesCell') + Page = apps.get_model('data', 'Page') + ItemsHistory = apps.get_model('lingo', 'ItemsHistory') + for cell in ItemsHistory.objects.all(): + page = Page.objects.get(pk=cell.page_id) + InvoicesCell.objects.get_or_create(order=cell.order, + page=page, placeholder=cell.placeholder, + public=cell.public, regie=cell.regie, + restricted_to_unlogged=cell.restricted_to_unlogged, + slug=cell.slug, text=cell.text, title=cell.title, + categories='past' + ) + cell.delete() + +def restore_activeitems_cells(apps, schema_editor): + InvoicesCell = apps.get_model('lingo', 'InvoicesCell') + Page = apps.get_model('data', 'Page') + ActiveItems = apps.get_model('lingo', 'ActiveItems') + for cell in InvoicesCell.objects.filter(categories__contains='active'): + page = Page.objects.get(pk=cell.page_id) + ActiveItems.objects.get_or_create(order=cell.order, + page=page, placeholder=cell.placeholder, + public=cell.public, regie=cell.regie, + restricted_to_unlogged=cell.restricted_to_unlogged, + slug=cell.slug, text=cell.text, title=cell.title + ) + cell.delete() + +def restore_itemshistory_cells(apps, schema_editor): + InvoicesCell = apps.get_model('lingo', 'InvoicesCell') + Page = apps.get_model('data', 'Page') + ItemsHistory = apps.get_model('lingo', 'ItemsHistory') + for cell in InvoicesCell.objects.filter(categories__contains='past'): + page = Page.objects.get(pk=cell.page_id) + ItemsHistory.objects.get_or_create(order=cell.order, + page=page, placeholder=cell.placeholder, + public=cell.public, regie=cell.regie, + restricted_to_unlogged=cell.restricted_to_unlogged, + slug=cell.slug, text=cell.text, title=cell.title + ) + cell.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('lingo', '0018_invoicescell'), + ] + + operations = [ + migrations.RunPython(migrate_activeitems_cells, + restore_activeitems_cells), + migrations.RunPython(migrate_itemshistory_cells, + restore_itemshistory_cells) + ] diff --git a/combo/apps/lingo/models.py b/combo/apps/lingo/models.py index 771ad13..553ff7e 100644 --- a/combo/apps/lingo/models.py +++ b/combo/apps/lingo/models.py @@ -40,8 +40,11 @@ from ckeditor.fields import RichTextField from combo.data.models import CellBase from combo.data.library import register_cell_class from combo.utils import NothingInCacheException, sign_url +from combo.fields import MultiSelectField EXPIRED = 9999 +ACTIVE_ITEMS = 'active' +PAST_ITEMS = 'past' SERVICES = [ @@ -55,6 +58,11 @@ SERVICES = [ (eopayment.PAYZEN, _('PayZen')), ] +INVOICE_CATEGORIES = ( + (ACTIVE_ITEMS, _('Active')), + (PAST_ITEMS, _('Past')) +) + def build_remote_item(data, regie): return RemoteItem(id=data.get('id'), regie=regie, creation_date=data['created'], @@ -358,6 +366,62 @@ class LingoBasketLinkCell(CellBase): return basket_template.render(context) +@register_cell_class +class InvoicesCell(CellBase): + regie = models.CharField(_('Regie'), max_length=50, blank=True) + title = models.CharField(_('Title'), max_length=200, blank=True) + text = RichTextField(_('Text'), blank=True, null=True) + categories = MultiSelectField(_('Categories'), choices=INVOICE_CATEGORIES) + + user_dependant = True + template_name = 'lingo/combo/items.html' + + class Meta: + verbose_name = _('Invoices') + + class Media: + js = ('xstatic/jquery-ui.min.js', 'js/gadjo.js',) + css = {'all': ('xstatic/themes/smoothness/jquery-ui.min.css', )} + + @classmethod + def is_enabled(cls): + return Regie.objects.count() > 0 + + def get_default_form_class(self): + fields = ['title', 'categories', 'text'] + widgets = {} + if Regie.objects.count() > 1: + regies = [('', _('All'))] + regies.extend([(r.slug, r.label) for r in Regie.objects.all()]) + widgets['regie'] = Select(choices=regies) + fields.insert(0, 'regie') + return model_forms.modelform_factory(self.__class__, fields=fields, widgets=widgets) + + def get_regies(self): + if self.regie: + return [Regie.objects.get(slug=self.regie)] + return Regie.objects.all() + + def get_cell_extra_context(self, context): + ctx = {'title': self.title, 'text': self.text} + invoices = [] + for r in self.get_regies(): + if ACTIVE_ITEMS in self.categories: + invoices.extend(r.get_items(self.context)) + if PAST_ITEMS in self.categories: + invoices.extend(r.get_past_items(self.context)) + # sort items by creation date + invoices.sort(key=lambda i: i.creation_date, reverse=True) + ctx.update({'items': invoices}) + return ctx + + def render(self, context): + self.context = context + if not context.get('synchronous'): + raise NothingInCacheException() + return super(InvoicesCell, self).render(context) + + class Items(CellBase): regie = models.CharField(_('Regie'), max_length=50, blank=True) title = models.CharField(_('Title'), max_length=200, blank=True) diff --git a/combo/fields.py b/combo/fields.py new file mode 100644 index 0000000..e0d3fa3 --- /dev/null +++ b/combo/fields.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# This is a modified copy of https://github.com/goinnn/django-multiselectfield +# +# Copyright (c) 2012 by Pablo Martín +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this programe. If not, see . + +import sys + +import django + +from django.db import models +from django.core import validators +from django import forms +from django.utils.text import capfirst +from django.core import exceptions + +if sys.version_info[0] == 2: + string_type = unicode +else: + string_type = str + +# Code from six egg https://bitbucket.org/gutworth/six/src/a3641cb211cc360848f1e2dd92e9ae6cd1de55dd/six.py?at=default + +def get_max_length(choices, max_length, default=200): + if max_length is None: + if choices: + return len(','.join([string_type(key) for key, label in choices])) + else: + return default + return max_length + + +class MaxValueMultiFieldValidator(validators.MaxLengthValidator): + clean = lambda self, x: len(','.join(x)) + code = 'max_multifield_value' + + +class MultiSelectFormField(forms.MultipleChoiceField): + widget = forms.CheckboxSelectMultiple + + def __init__(self, *args, **kwargs): + self.max_choices = kwargs.pop('max_choices', None) + self.max_length = kwargs.pop('max_length', None) + super(MultiSelectFormField, self).__init__(*args, **kwargs) + self.max_length = get_max_length(self.choices, self.max_length) + self.validators.append(MaxValueMultiFieldValidator(self.max_length)) + + +class MultiSelectField(models.CharField): + """ Choice values can not contain commas. """ + + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + self.max_choices = kwargs.pop('max_choices', None) + super(MultiSelectField, self).__init__(*args, **kwargs) + self.max_length = get_max_length(self.choices, self.max_length) + self.validators[0] = MaxValueMultiFieldValidator(self.max_length) + + @property + def flatchoices(self): + return None + + def get_choices_default(self): + return self.get_choices(include_blank=False) + + def get_choices_selected(self, arr_choices): + choices_selected = [] + for choice_selected in arr_choices: + choices_selected.append(string_type(choice_selected[0])) + return choices_selected + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + return self.get_prep_value(value) + + def validate(self, value, model_instance): + arr_choices = self.get_choices_selected(self.get_choices_default()) + for opt_select in value: + if (opt_select not in arr_choices): + raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value) + + def get_default(self): + default = super(MultiSelectField, self).get_default() + if isinstance(default, int): + default = string_type(default) + return default + + def formfield(self, **kwargs): + defaults = {'required': not self.blank, + 'label': capfirst(self.verbose_name), + 'help_text': self.help_text, + 'choices': self.choices, + 'max_length': self.max_length, + 'max_choices': self.max_choices} + if self.has_default(): + defaults['initial'] = self.get_default() + defaults.update(kwargs) + return MultiSelectFormField(**defaults) + + def get_prep_value(self, value): + return '' if value is None else ",".join(value) + + def to_python(self, value): + if value: + return value if isinstance(value, list) else value.split(',') + + def contribute_to_class(self, cls, name): + super(MultiSelectField, self).contribute_to_class(cls, name) + if self.choices: + def get_list(obj): + fieldname = name + choicedict = dict(self.choices) + display = [] + if getattr(obj, fieldname): + for value in getattr(obj, fieldname): + item_display = choicedict.get(value, None) + if item_display is None: + try: + item_display = choicedict.get(int(value), value) + except (ValueError, TypeError): + item_display = value + display.append(string_type(item_display)) + return display + + def get_display(obj): + return ", ".join(get_list(obj)) + + setattr(cls, 'get_%s_list' % self.name, get_list) + setattr(cls, 'get_%s_display' % self.name, get_display) diff --git a/tests/test_lingo_cells.py b/tests/test_lingo_cells.py index 8c9d83d..0848df4 100644 --- a/tests/test_lingo_cells.py +++ b/tests/test_lingo_cells.py @@ -6,7 +6,9 @@ from django.template import Context from django.utils import timezone from combo.data.models import Page +from combo.utils import NothingInCacheException from combo.apps.lingo.models import Regie, BasketItem, Transaction +from combo.apps.lingo.models import ACTIVE_ITEMS, InvoicesCell from combo.apps.lingo.models import LingoBasketCell, LingoRecentTransactionsCell pytestmark = pytest.mark.django_db @@ -68,3 +70,16 @@ def test_basket_cell(regie, user): content = cell.render(context) assert '12345' in content + +def test_invoices_cell(regie, user): + page = Page(title='invoices', slug='test_invoices_cell', template_name='standard') + page.save() + cell = InvoicesCell(page=page, placeholder='content', order=0, + regie=regie.slug, title='Active Invoices', categories=ACTIVE_ITEMS) + context = Context({'request': RequestFactory().get('/')}) + context['request'].user = user + assert cell.is_relevant(context) is True + with pytest.raises(NothingInCacheException): + cell.render(context) + context['synchronous'] = True + assert 'Active Invoices' in cell.render(context) -- 2.8.0.rc3