Projet

Général

Profil

0001-lingo-add-generic-cell-handling-all-invoice-categori.patch

Serghei Mihai, 04 avril 2016 17:23

Télécharger (16,3 ko)

Voir les différences:

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
combo/apps/lingo/migrations/0018_invoicescell.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import models, migrations
5
import ckeditor.fields
6
import combo.fields
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('auth', '0001_initial'),
13
        ('data', '0016_feedcell_limit'),
14
        ('lingo', '0017_auto_20160327_0831'),
15
    ]
16

  
17
    operations = [
18
        migrations.CreateModel(
19
            name='InvoicesCell',
20
            fields=[
21
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
22
                ('placeholder', models.CharField(max_length=20)),
23
                ('order', models.PositiveIntegerField()),
24
                ('slug', models.SlugField(verbose_name='Slug', blank=True)),
25
                ('public', models.BooleanField(default=True, verbose_name='Public')),
26
                ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
27
                ('regie', models.CharField(max_length=50, verbose_name='Regie', blank=True)),
28
                ('title', models.CharField(max_length=200, verbose_name='Title', blank=True)),
29
                ('text', ckeditor.fields.RichTextField(null=True, verbose_name='Text', blank=True)),
30
                ('categories', combo.fields.MultiSelectField(max_length=11, verbose_name='Categories', choices=[(b'active', 'Active'), (b'past', 'Past')])),
31
                ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
32
                ('page', models.ForeignKey(to='data.Page')),
33
            ],
34
            options={
35
                'verbose_name': 'Invoices',
36
            },
37
            bases=(models.Model,),
38
        ),
39
    ]
combo/apps/lingo/migrations/0019_manual_migrate_invoice_cells_20160404_1356.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import models, migrations
5

  
6
def migrate_activeitems_cells(apps, schema_editor):
7
    InvoicesCell = apps.get_model('lingo', 'InvoicesCell')
8
    Page = apps.get_model('data', 'Page')
9
    ActiveItems = apps.get_model('lingo', 'ActiveItems')
10
    for cell in ActiveItems.objects.all():
11
        page = Page.objects.get(pk=cell.page_id)
12
        InvoicesCell.objects.get_or_create(order=cell.order,
13
                        page=page, placeholder=cell.placeholder,
14
                        public=cell.public, regie=cell.regie,
15
                        restricted_to_unlogged=cell.restricted_to_unlogged,
16
                        slug=cell.slug, text=cell.text, title=cell.title,
17
                        categories='active'
18
        )
19
        cell.delete()
20

  
21
def migrate_itemshistory_cells(apps, schema_editor):
22
    InvoicesCell = apps.get_model('lingo', 'InvoicesCell')
23
    Page = apps.get_model('data', 'Page')
24
    ItemsHistory = apps.get_model('lingo', 'ItemsHistory')
25
    for cell in ItemsHistory.objects.all():
26
        page = Page.objects.get(pk=cell.page_id)
27
        InvoicesCell.objects.get_or_create(order=cell.order,
28
                        page=page, placeholder=cell.placeholder,
29
                        public=cell.public, regie=cell.regie,
30
                        restricted_to_unlogged=cell.restricted_to_unlogged,
31
                        slug=cell.slug, text=cell.text, title=cell.title,
32
                        categories='past'
33
        )
34
        cell.delete()
35

  
36
def restore_activeitems_cells(apps, schema_editor):
37
    InvoicesCell = apps.get_model('lingo', 'InvoicesCell')
38
    Page = apps.get_model('data', 'Page')
39
    ActiveItems = apps.get_model('lingo', 'ActiveItems')
40
    for cell in InvoicesCell.objects.filter(categories__contains='active'):
41
        page = Page.objects.get(pk=cell.page_id)
42
        ActiveItems.objects.get_or_create(order=cell.order,
43
                        page=page, placeholder=cell.placeholder,
44
                        public=cell.public, regie=cell.regie,
45
                        restricted_to_unlogged=cell.restricted_to_unlogged,
46
                        slug=cell.slug, text=cell.text, title=cell.title
47
        )
48
        cell.delete()
49

  
50
def restore_itemshistory_cells(apps, schema_editor):
51
    InvoicesCell = apps.get_model('lingo', 'InvoicesCell')
52
    Page = apps.get_model('data', 'Page')
53
    ItemsHistory = apps.get_model('lingo', 'ItemsHistory')
54
    for cell in InvoicesCell.objects.filter(categories__contains='past'):
55
        page = Page.objects.get(pk=cell.page_id)
56
        ItemsHistory.objects.get_or_create(order=cell.order,
57
                        page=page, placeholder=cell.placeholder,
58
                        public=cell.public, regie=cell.regie,
59
                        restricted_to_unlogged=cell.restricted_to_unlogged,
60
                        slug=cell.slug, text=cell.text, title=cell.title
61
        )
62
        cell.delete()
63

  
64

  
65
class Migration(migrations.Migration):
66

  
67
    dependencies = [
68
        ('lingo', '0018_invoicescell'),
69
    ]
70

  
71
    operations = [
72
        migrations.RunPython(migrate_activeitems_cells,
73
                             restore_activeitems_cells),
74
        migrations.RunPython(migrate_itemshistory_cells,
75
                             restore_itemshistory_cells)
76
    ]
combo/apps/lingo/models.py
40 40
from combo.data.models import CellBase
41 41
from combo.data.library import register_cell_class
42 42
from combo.utils import NothingInCacheException, sign_url
43
from combo.fields import MultiSelectField
43 44

  
44 45
EXPIRED = 9999
46
ACTIVE_ITEMS = 'active'
47
PAST_ITEMS = 'past'
45 48

  
46 49

  
47 50
SERVICES = [
......
55 58
    (eopayment.PAYZEN, _('PayZen')),
56 59
]
57 60

  
61
INVOICE_CATEGORIES = (
62
    (ACTIVE_ITEMS, _('Active')),
63
    (PAST_ITEMS, _('Past'))
64
)
65

  
58 66
def build_remote_item(data, regie):
59 67
    return RemoteItem(id=data.get('id'), regie=regie,
60 68
                      creation_date=data['created'],
......
358 366
        return basket_template.render(context)
359 367

  
360 368

  
369
@register_cell_class
370
class InvoicesCell(CellBase):
371
    regie = models.CharField(_('Regie'), max_length=50, blank=True)
372
    title = models.CharField(_('Title'), max_length=200, blank=True)
373
    text = RichTextField(_('Text'), blank=True, null=True)
374
    categories = MultiSelectField(_('Categories'), choices=INVOICE_CATEGORIES)
375

  
376
    user_dependant = True
377
    template_name = 'lingo/combo/items.html'
378

  
379
    class Meta:
380
        verbose_name = _('Invoices')
381

  
382
    class Media:
383
        js = ('xstatic/jquery-ui.min.js', 'js/gadjo.js',)
384
        css = {'all': ('xstatic/themes/smoothness/jquery-ui.min.css', )}
385

  
386
    @classmethod
387
    def is_enabled(cls):
388
        return Regie.objects.count() > 0
389

  
390
    def get_default_form_class(self):
391
        fields = ['title', 'categories', 'text']
392
        widgets = {}
393
        if Regie.objects.count() > 1:
394
            regies = [('', _('All'))]
395
            regies.extend([(r.slug, r.label) for r in Regie.objects.all()])
396
            widgets['regie'] = Select(choices=regies)
397
            fields.insert(0, 'regie')
398
        return model_forms.modelform_factory(self.__class__, fields=fields, widgets=widgets)
399

  
400
    def get_regies(self):
401
        if self.regie:
402
            return [Regie.objects.get(slug=self.regie)]
403
        return Regie.objects.all()
404

  
405
    def get_cell_extra_context(self, context):
406
        ctx = {'title': self.title, 'text': self.text}
407
        invoices = []
408
        for r in self.get_regies():
409
            if ACTIVE_ITEMS in self.categories:
410
                invoices.extend(r.get_items(self.context))
411
            if PAST_ITEMS in self.categories:
412
                invoices.extend(r.get_past_items(self.context))
413
        # sort items by creation date
414
        invoices.sort(key=lambda i: i.creation_date, reverse=True)
415
        ctx.update({'items': invoices})
416
        return ctx
417

  
418
    def render(self, context):
419
        self.context = context
420
        if not context.get('synchronous'):
421
            raise NothingInCacheException()
422
        return super(InvoicesCell, self).render(context)
423

  
424

  
361 425
class Items(CellBase):
362 426
    regie = models.CharField(_('Regie'), max_length=50, blank=True)
363 427
    title = models.CharField(_('Title'), max_length=200, blank=True)
combo/fields.py
1
# -*- coding: utf-8 -*-
2
#
3
# This is a modified copy of https://github.com/goinnn/django-multiselectfield
4
#
5
# Copyright (c) 2012 by Pablo Martín <goinnn@gmail.com>
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Lesser General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
# GNU Lesser General Public License for more details.
16
#
17
# You should have received a copy of the GNU Lesser General Public License
18
# along with this programe.  If not, see <http://www.gnu.org/licenses/>.
19

  
20
import sys
21

  
22
import django
23

  
24
from django.db import models
25
from django.core import validators
26
from django import forms
27
from django.utils.text import capfirst
28
from django.core import exceptions
29

  
30
if sys.version_info[0] == 2:
31
    string_type = unicode
32
else:
33
    string_type = str
34

  
35
# Code from six egg https://bitbucket.org/gutworth/six/src/a3641cb211cc360848f1e2dd92e9ae6cd1de55dd/six.py?at=default
36

  
37
def get_max_length(choices, max_length, default=200):
38
    if max_length is None:
39
        if choices:
40
            return len(','.join([string_type(key) for key, label in choices]))
41
        else:
42
            return default
43
    return max_length
44

  
45

  
46
class MaxValueMultiFieldValidator(validators.MaxLengthValidator):
47
    clean = lambda self, x: len(','.join(x))
48
    code = 'max_multifield_value'
49

  
50

  
51
class MultiSelectFormField(forms.MultipleChoiceField):
52
    widget = forms.CheckboxSelectMultiple
53

  
54
    def __init__(self, *args, **kwargs):
55
        self.max_choices = kwargs.pop('max_choices', None)
56
        self.max_length = kwargs.pop('max_length', None)
57
        super(MultiSelectFormField, self).__init__(*args, **kwargs)
58
        self.max_length = get_max_length(self.choices, self.max_length)
59
        self.validators.append(MaxValueMultiFieldValidator(self.max_length))
60

  
61

  
62
class MultiSelectField(models.CharField):
63
    """ Choice values can not contain commas. """
64

  
65
    __metaclass__ = models.SubfieldBase
66

  
67
    def __init__(self, *args, **kwargs):
68
        self.max_choices = kwargs.pop('max_choices', None)
69
        super(MultiSelectField, self).__init__(*args, **kwargs)
70
        self.max_length = get_max_length(self.choices, self.max_length)
71
        self.validators[0] = MaxValueMultiFieldValidator(self.max_length)
72

  
73
    @property
74
    def flatchoices(self):
75
        return None
76

  
77
    def get_choices_default(self):
78
        return self.get_choices(include_blank=False)
79

  
80
    def get_choices_selected(self, arr_choices):
81
        choices_selected = []
82
        for choice_selected in arr_choices:
83
            choices_selected.append(string_type(choice_selected[0]))
84
        return choices_selected
85

  
86
    def value_to_string(self, obj):
87
        value = self._get_val_from_obj(obj)
88
        return self.get_prep_value(value)
89

  
90
    def validate(self, value, model_instance):
91
        arr_choices = self.get_choices_selected(self.get_choices_default())
92
        for opt_select in value:
93
            if (opt_select not in arr_choices):
94
                raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value)
95

  
96
    def get_default(self):
97
        default = super(MultiSelectField, self).get_default()
98
        if isinstance(default, int):
99
            default = string_type(default)
100
        return default
101

  
102
    def formfield(self, **kwargs):
103
        defaults = {'required': not self.blank,
104
                    'label': capfirst(self.verbose_name),
105
                    'help_text': self.help_text,
106
                    'choices': self.choices,
107
                    'max_length': self.max_length,
108
                    'max_choices': self.max_choices}
109
        if self.has_default():
110
            defaults['initial'] = self.get_default()
111
        defaults.update(kwargs)
112
        return MultiSelectFormField(**defaults)
113

  
114
    def get_prep_value(self, value):
115
        return '' if value is None else ",".join(value)
116

  
117
    def to_python(self, value):
118
        if value:
119
            return value if isinstance(value, list) else value.split(',')
120

  
121
    def contribute_to_class(self, cls, name):
122
        super(MultiSelectField, self).contribute_to_class(cls, name)
123
        if self.choices:
124
            def get_list(obj):
125
                fieldname = name
126
                choicedict = dict(self.choices)
127
                display = []
128
                if getattr(obj, fieldname):
129
                    for value in getattr(obj, fieldname):
130
                        item_display = choicedict.get(value, None)
131
                        if item_display is None:
132
                            try:
133
                                item_display = choicedict.get(int(value), value)
134
                            except (ValueError, TypeError):
135
                                item_display = value
136
                        display.append(string_type(item_display))
137
                return display
138

  
139
            def get_display(obj):
140
                return ", ".join(get_list(obj))
141

  
142
            setattr(cls, 'get_%s_list' % self.name, get_list)
143
            setattr(cls, 'get_%s_display' % self.name, get_display)
tests/test_lingo_cells.py
6 6
from django.utils import timezone
7 7

  
8 8
from combo.data.models import Page
9
from combo.utils import NothingInCacheException
9 10
from combo.apps.lingo.models import Regie, BasketItem, Transaction
11
from combo.apps.lingo.models import ACTIVE_ITEMS, InvoicesCell
10 12
from combo.apps.lingo.models import LingoBasketCell, LingoRecentTransactionsCell
11 13

  
12 14
pytestmark = pytest.mark.django_db
......
68 70

  
69 71
    content = cell.render(context)
70 72
    assert '12345' in content
73

  
74
def test_invoices_cell(regie, user):
75
    page = Page(title='invoices', slug='test_invoices_cell', template_name='standard')
76
    page.save()
77
    cell = InvoicesCell(page=page, placeholder='content', order=0,
78
                        regie=regie.slug, title='Active Invoices', categories=ACTIVE_ITEMS)
79
    context = Context({'request': RequestFactory().get('/')})
80
    context['request'].user = user
81
    assert cell.is_relevant(context) is True
82
    with pytest.raises(NothingInCacheException):
83
        cell.render(context)
84
    context['synchronous'] = True
85
    assert 'Active Invoices' in cell.render(context)
71
-