0001-lingo-add-generic-cell-handling-all-invoice-categori.patch
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 |
- |