From 85caa6b0581b76b8fb6aa92fd01aaffbec9e9bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 20:32:16 +0200 Subject: [PATCH 01/25] Use django-djconfig for kfet app. Old configuration(/settings), based on Settings model, system is deleted: SettingsForm, Settings. New system use `django-djconfig` module. - `kfet.config` module provides `kfet_config` to access configuration concerning kfet app. - Views, forms, models, etc now use this object to retrieve conf values. - Views no longer add config values to context, instead templates use `kfet_config` provided by a new context_processor. - Enhance list and update views of settings. - Fix: settings can directly be used without having to visit a specific page... Misc - Delete some py2/3 imports - Delete unused imports in kfet.models and kfet.views - Some PEP8 compliance --- cof/settings_dev.py | 4 + kfet/apps.py | 6 ++ kfet/config.py | 25 +++++ kfet/context_processors.py | 11 +- kfet/forms.py | 64 ++++++----- kfet/migrations/0051_delete_settings.py | 52 +++++++++ kfet/models.py | 124 +--------------------- kfet/templates/kfet/account_negative.html | 4 +- kfet/templates/kfet/account_read.html | 2 +- kfet/templates/kfet/history.html | 2 +- kfet/templates/kfet/settings.html | 52 ++++++--- kfet/templates/kfet/settings_update.html | 25 +++-- kfet/templatetags/kfet_tags.py | 5 +- kfet/urls.py | 2 +- kfet/views.py | 95 +++++++++-------- requirements.txt | 1 + 16 files changed, 247 insertions(+), 227 deletions(-) create mode 100644 kfet/config.py create mode 100644 kfet/migrations/0051_delete_settings.py diff --git a/cof/settings_dev.py b/cof/settings_dev.py index 18aadaad..b04165c8 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -47,6 +47,7 @@ INSTALLED_APPS = ( 'channels', 'widget_tweaks', 'custommail', + 'djconfig', ) MIDDLEWARE_CLASSES = ( @@ -60,6 +61,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', + 'djconfig.middleware.DjConfigMiddleware', ) ROOT_URLCONF = 'cof.urls' @@ -78,8 +80,10 @@ TEMPLATES = [ 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'django.core.context_processors.static', + 'djconfig.context_processors.config', 'gestioncof.shared.context_processor', 'kfet.context_processors.auth', + 'kfet.context_processors.config', ], }, }, diff --git a/kfet/apps.py b/kfet/apps.py index 29f9f98e..3dd2c0e8 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -12,3 +12,9 @@ class KFetConfig(AppConfig): def ready(self): import kfet.signals + self.register_config() + + def register_config(self): + import djconfig + from kfet.forms import KFetConfigForm + djconfig.register(KFetConfigForm) diff --git a/kfet/config.py b/kfet/config.py new file mode 100644 index 00000000..deb12504 --- /dev/null +++ b/kfet/config.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from djconfig import config + + +class KFetConfig(object): + """kfet app configuration. + + Enhance dependency with backend used to store config. + Usable after DjConfig middleware was called. + + """ + prefix = 'kfet_' + + def __getattr__(self, key): + dj_key = '{}{}'.format(self.prefix, key) + return getattr(config, dj_key) + + def list(self): + from kfet.forms import KFetConfigForm + return [(field.label, getattr(config, name), ) + for name, field in KFetConfigForm.base_fields.items()] + + +kfet_config = KFetConfig() diff --git a/kfet/context_processors.py b/kfet/context_processors.py index ef4f2e64..4c7b4fe4 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from builtins import * - from django.contrib.auth.context_processors import PermWrapper +from kfet.config import kfet_config + + def auth(request): if hasattr(request, 'real_user'): return { @@ -13,3 +12,7 @@ def auth(request): 'perms': PermWrapper(request.real_user), } return {} + + +def config(request): + return {'kfet_config': kfet_config} diff --git a/kfet/forms.py b/kfet/forms.py index 0fc02dd3..b6335fb8 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- +from datetime import timedelta from decimal import Decimal + from django import forms from django.core.exceptions import ValidationError from django.core.validators import MinLengthValidator @@ -8,12 +10,16 @@ from django.contrib.auth.models import User, Group, Permission from django.contrib.contenttypes.models import ContentType from django.forms import modelformset_factory from django.utils import timezone + +from djconfig.forms import ConfigForm + from kfet.models import ( Account, Checkout, Article, OperationGroup, Operation, - CheckoutStatement, ArticleCategory, Settings, AccountNegative, Transfer, + CheckoutStatement, ArticleCategory, AccountNegative, Transfer, TransferGroup, Supplier) from gestioncof.models import CofProfile + # ----- # Widgets # ----- @@ -379,40 +385,42 @@ class AddcostForm(forms.Form): self.cleaned_data['amount'] = 0 super(AddcostForm, self).clean() + # ----- # Settings forms # ----- -class SettingsForm(forms.ModelForm): - class Meta: - model = Settings - fields = ['value_decimal', 'value_account', 'value_duration'] - def clean(self): - name = self.instance.name - value_decimal = self.cleaned_data.get('value_decimal') - value_account = self.cleaned_data.get('value_account') - value_duration = self.cleaned_data.get('value_duration') +class KFetConfigForm(ConfigForm): - type_decimal = ['SUBVENTION_COF', 'ADDCOST_AMOUNT', 'OVERDRAFT_AMOUNT'] - type_account = ['ADDCOST_FOR'] - type_duration = ['OVERDRAFT_DURATION', 'CANCEL_DURATION'] + kfet_subvention_cof = forms.DecimalField( + label='Subvention COF', initial=Decimal('25'), + max_digits=6, decimal_places=2, + ) + kfet_addcost_amount = forms.DecimalField( + label='Montant de la majoration', initial=Decimal('0'), required=False, + max_digits=6, decimal_places=2, + ) + kfet_addcost_for = forms.ModelChoiceField( + label='Destinataire de la majoration', initial=None, required=False, + help_text='Laissez vide pour désactiver la majoration', + queryset=(Account.objects + .select_related('cofprofile', 'cofprofile__user') + .all()), + ) + kfet_overdraft_duration = forms.DurationField( + label='Durée du découvert autorisé par défaut', + initial=timedelta(days=1), + ) + kfet_overdraft_amount = forms.DecimalField( + label='Montant du découvert autorisé par défaut', initial=Decimal('20'), + max_digits=6, decimal_places=2, + ) + kfet_cancel_duration = forms.DurationField( + label='Durée pour annuler une commande sans mot de passe', + initial=timedelta(minutes=5), + ) - self.cleaned_data['name'] = name - if name in type_decimal: - if not value_decimal: - raise ValidationError('Renseignez une valeur décimale') - self.cleaned_data['value_account'] = None - self.cleaned_data['value_duration'] = None - elif name in type_account: - self.cleaned_data['value_decimal'] = None - self.cleaned_data['value_duration'] = None - elif name in type_duration: - if not value_duration: - raise ValidationError('Renseignez une durée') - self.cleaned_data['value_decimal'] = None - self.cleaned_data['value_account'] = None - super(SettingsForm, self).clean() class FilterHistoryForm(forms.Form): checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all()) diff --git a/kfet/migrations/0051_delete_settings.py b/kfet/migrations/0051_delete_settings.py new file mode 100644 index 00000000..5addad28 --- /dev/null +++ b/kfet/migrations/0051_delete_settings.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + +from kfet.forms import KFetConfigForm + + +def adapt_settings(apps, schema_editor): + Settings = apps.get_model('kfet', 'Settings') + db_alias = schema_editor.connection.alias + obj = Settings.objects.using(db_alias) + + cfg = {} + + def try_get(new, old, type_field): + try: + value = getattr(obj.get(name=old), type_field) + cfg[new] = value + except Settings.DoesNotExist: + pass + + try_get('kfet_subvention_cof', 'SUBVENTION_COF', 'value_decimal') + try_get('kfet_addcost_amount', 'ADDCOST_AMOUNT', 'value_decimal') + try_get('kfet_addcost_for', 'ADDCOST_FOR', 'value_account') + try_get('kfet_overdraft_duration', 'OVERDRAFT_DURATION', 'value_duration') + try_get('kfet_overdraft_amount', 'OVERDRAFT_AMOUNT', 'value_decimal') + try_get('kfet_cancel_duration', 'CANCEL_DURATION', 'value_duration') + + cfg_form = KFetConfigForm(initial=cfg) + if cfg_form.is_valid(): + cfg_form.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0050_remove_checkout'), + ('djconfig', '0001_initial'), + ] + + operations = [ + migrations.RunPython(adapt_settings), + migrations.RemoveField( + model_name='settings', + name='value_account', + ), + migrations.DeleteModel( + name='Settings', + ), + ] diff --git a/kfet/models.py b/kfet/models.py index c039ab06..de871f0c 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,12 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from builtins import * - from django.db import models from django.core.urlresolvers import reverse -from django.core.exceptions import PermissionDenied, ValidationError from django.core.validators import RegexValidator from django.contrib.auth.models import User from gestioncof.models import CofProfile @@ -15,11 +10,12 @@ from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible from django.db import transaction from django.db.models import F -from django.core.cache import cache -from datetime import date, timedelta +from datetime import date import re import hashlib +from kfet.config import kfet_config + def choices_length(choices): return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) @@ -113,8 +109,8 @@ class Account(models.Model): return data def perms_to_perform_operation(self, amount): - overdraft_duration_max = Settings.OVERDRAFT_DURATION() - overdraft_amount_max = Settings.OVERDRAFT_AMOUNT() + overdraft_duration_max = kfet_config.overdraft_duration + overdraft_amount_max = kfet_config.overdraft_amount perms = set() stop_ope = False # Checking is cash account @@ -620,116 +616,6 @@ class GlobalPermissions(models.Model): ('special_add_account', "Créer un compte avec une balance initiale") ) -class Settings(models.Model): - name = models.CharField( - max_length = 45, - unique = True, - db_index = True) - value_decimal = models.DecimalField( - max_digits = 6, decimal_places = 2, - blank = True, null = True, default = None) - value_account = models.ForeignKey( - Account, on_delete = models.PROTECT, - blank = True, null = True, default = None) - value_duration = models.DurationField( - blank = True, null = True, default = None) - - @staticmethod - def setting_inst(name): - return Settings.objects.get(name=name) - - @staticmethod - def SUBVENTION_COF(): - subvention_cof = cache.get('SUBVENTION_COF') - if subvention_cof: - return subvention_cof - try: - subvention_cof = Settings.setting_inst("SUBVENTION_COF").value_decimal - except Settings.DoesNotExist: - subvention_cof = 0 - cache.set('SUBVENTION_COF', subvention_cof) - return subvention_cof - - @staticmethod - def ADDCOST_AMOUNT(): - try: - return Settings.setting_inst("ADDCOST_AMOUNT").value_decimal - except Settings.DoesNotExist: - return 0 - - @staticmethod - def ADDCOST_FOR(): - try: - return Settings.setting_inst("ADDCOST_FOR").value_account - except Settings.DoesNotExist: - return None; - - @staticmethod - def OVERDRAFT_DURATION(): - overdraft_duration = cache.get('OVERDRAFT_DURATION') - if overdraft_duration: - return overdraft_duration - try: - overdraft_duration = Settings.setting_inst("OVERDRAFT_DURATION").value_duration - except Settings.DoesNotExist: - overdraft_duration = timedelta() - cache.set('OVERDRAFT_DURATION', overdraft_duration) - return overdraft_duration - - @staticmethod - def OVERDRAFT_AMOUNT(): - overdraft_amount = cache.get('OVERDRAFT_AMOUNT') - if overdraft_amount: - return overdraft_amount - try: - overdraft_amount = Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal - except Settings.DoesNotExist: - overdraft_amount = 0 - cache.set('OVERDRAFT_AMOUNT', overdraft_amount) - return overdraft_amount - - @staticmethod - def CANCEL_DURATION(): - cancel_duration = cache.get('CANCEL_DURATION') - if cancel_duration: - return cancel_duration - try: - cancel_duration = Settings.setting_inst("CANCEL_DURATION").value_duration - except Settings.DoesNotExist: - cancel_duration = timedelta() - cache.set('CANCEL_DURATION', cancel_duration) - return cancel_duration - - @staticmethod - def create_missing(): - s, created = Settings.objects.get_or_create(name='SUBVENTION_COF') - if created: - s.value_decimal = 25 - s.save() - s, created = Settings.objects.get_or_create(name='ADDCOST_AMOUNT') - if created: - s.value_decimal = 0.5 - s.save() - s, created = Settings.objects.get_or_create(name='ADDCOST_FOR') - s, created = Settings.objects.get_or_create(name='OVERDRAFT_DURATION') - if created: - s.value_duration = timedelta(days=1) # 24h - s.save() - s, created = Settings.objects.get_or_create(name='OVERDRAFT_AMOUNT') - if created: - s.value_decimal = 20 - s.save() - s, created = Settings.objects.get_or_create(name='CANCEL_DURATION') - if created: - s.value_duration = timedelta(minutes=5) # 5min - s.save() - - @staticmethod - def empty_cache(): - cache.delete_many([ - 'SUBVENTION_COF', 'OVERDRAFT_DURATION', 'OVERDRAFT_AMOUNT', - 'CANCEL_DURATION', 'ADDCOST_AMOUNT', 'ADDCOST_FOR', - ]) class GenericTeamToken(models.Model): token = models.CharField(max_length = 50, unique = True) diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index 5f77b8f0..af4366ed 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -16,8 +16,8 @@
Découvert autorisé par défaut
-
Montant: {{ settings.overdraft_amount }}€
-
Pendant: {{ settings.overdraft_duration }}
+
Montant: {{ kfet_config.overdraft_amount }}€
+
Pendant: {{ kfet_config.overdraft_duration }}
{% if perms.kfet.change_settings %} diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 50ab7f20..5c627aad 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -95,7 +95,7 @@ - {% if account.user == request.user %} @@ -18,11 +17,11 @@ $(document).ready(function() { var stat_last = new StatsGroup( "{% url 'kfet.account.stat.operation.list' trigramme=account.trigramme %}", - $("#stat_last"), + $("#stat_last") ); var stat_balance = new StatsGroup( "{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}", - $("#stat_balance"), + $("#stat_balance") ); }); From 1e18c4043e4593849ebd982340ea26f04f02a363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 9 Apr 2017 15:47:16 +0200 Subject: [PATCH 20/25] Use last channels & co versions - Use last official releases of channels, asgiref, daphne and asgi-redis packages. - Customization of JsonWebsocketConsumer is now in kfet app through a custom class (and so, doesn't require anymore a forked version of channels). - Clean kfet consumers code. --- kfet/consumers.py | 27 +++++++++++---------------- requirements.txt | 8 ++++---- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/kfet/consumers.py b/kfet/consumers.py index dcd69bdf..6e9dc6ca 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -1,26 +1,21 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from builtins import * +from django.core.serializers.json import json, DjangoJSONEncoder -from channels import Group from channels.generic.websockets import JsonWebsocketConsumer -class KPsul(JsonWebsocketConsumer): - # Set to True if you want them, else leave out - strict_ordering = False - slight_ordering = False +class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): + """Custom Json Websocket Consumer. - def connection_groups(self, **kwargs): - return ['kfet.kpsul'] + Encode to JSON with DjangoJSONEncoder. - def connect(self, message, **kwargs): - pass + """ - def receive(self, content, **kwargs): - pass + @classmethod + def encode_json(cls, content): + return json.dumps(content, cls=DjangoJSONEncoder) - def disconnect(self, message, **kwargs): - pass + +class KPsul(DjangoJsonWebsocketConsumer): + groups = ['kfet.kpsul'] diff --git a/requirements.txt b/requirements.txt index ce081588..990fba3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,13 +11,13 @@ six==1.10.0 unicodecsv==0.14.1 icalendar==3.10 django-bootstrap-form==3.2.1 -asgiref==0.14.0 -daphne==0.14.3 -asgi-redis==0.14.0 +asgiref==1.1.1 +daphne==1.2.0 +asgi-redis==1.3.0 statistics==1.0.3.5 future==0.15.2 django-widget-tweaks==1.4.1 git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail ldap3 -git+https://github.com/Aureplop/channels.git#egg=channels +channels==1.1.3 python-dateutil From 029d59e615adba19c61f2cd2be72a6f2d71cc23a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 9 Apr 2017 16:10:27 +0200 Subject: [PATCH 21/25] Enable authentication on KPsul websocket. - PermConsumerMixin allows checking permissions on connection to a consumer. - KPsul consumer uses this mixin to check if connecting user has the permission `kfet.is_team`. Fixes #67. --- kfet/consumers.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/kfet/consumers.py b/kfet/consumers.py index 6e9dc6ca..ee096368 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -17,5 +17,25 @@ class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): return json.dumps(content, cls=DjangoJSONEncoder) -class KPsul(DjangoJsonWebsocketConsumer): +class PermConsumerMixin(object): + """Add support to check permissions on Consumers. + + Attributes: + perms_connect (list): Required permissions to connect to this + consumer. + + """ + http_user = True # Enable message.user + perms_connect = [] + + def connect(self, message, **kwargs): + """Check permissions on connection.""" + if message.user.has_perms(self.perms_connect): + super().connect(message, **kwargs) + else: + self.close() + + +class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer): groups = ['kfet.kpsul'] + perms_connect = ['kfet.is_team'] From c228416809149c7ed39fd42f9d3309b66a127167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 10 Apr 2017 11:36:06 +0200 Subject: [PATCH 22/25] =?UTF-8?q?Subvention=20->=20R=C3=A9duction=20+=20un?= =?UTF-8?q?its=20for=20kfet=5Fconfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kfet_config gives "reduction_cof" as editable instead of "subvention_cof" - this last one can still be accessed via kfet_config (computed from new "reduction_cof" - add units to numeric values of kfet_config form --- kfet/config.py | 5 +++++ kfet/forms.py | 14 +++++++++----- kfet/migrations/0054_delete_settings.py | 8 +++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/kfet/config.py b/kfet/config.py index 5023e8b0..76da5a79 100644 --- a/kfet/config.py +++ b/kfet/config.py @@ -16,6 +16,11 @@ class KFetConfig(object): prefix = 'kfet_' def __getattr__(self, key): + if key == 'subvention_cof': + # Allows accessing to the reduction as a subvention + # Other reason: backward compatibility + reduction_mult = 1 - self.reduction_cof/100 + return (1/reduction_mult - 1) * 100 return getattr(config, self._get_dj_key(key)) def list(self): diff --git a/kfet/forms.py b/kfet/forms.py index 9b098b75..f89b8f08 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -403,17 +403,20 @@ class AddcostForm(forms.Form): class KFetConfigForm(ConfigForm): - kfet_subvention_cof = forms.DecimalField( - label='Subvention COF', initial=Decimal('25'), + kfet_reduction_cof = forms.DecimalField( + label='Réduction COF', initial=Decimal('20'), max_digits=6, decimal_places=2, + help_text="Réduction, à donner en pourcentage, appliquée lors d'un " + "achat par un-e membre du COF sur le montant en euros.", ) kfet_addcost_amount = forms.DecimalField( - label='Montant de la majoration', initial=Decimal('0'), required=False, + label='Montant de la majoration (en €)', initial=Decimal('0'), + required=False, max_digits=6, decimal_places=2, ) kfet_addcost_for = forms.ModelChoiceField( label='Destinataire de la majoration', initial=None, required=False, - help_text='Laissez vide pour désactiver la majoration', + help_text='Laissez vide pour désactiver la majoration.', queryset=(Account.objects .select_related('cofprofile', 'cofprofile__user') .all()), @@ -423,7 +426,8 @@ class KFetConfigForm(ConfigForm): initial=timedelta(days=1), ) kfet_overdraft_amount = forms.DecimalField( - label='Montant du découvert autorisé par défaut', initial=Decimal('20'), + label='Montant du découvert autorisé par défaut (en €)', + initial=Decimal('20'), max_digits=6, decimal_places=2, ) kfet_cancel_duration = forms.DurationField( diff --git a/kfet/migrations/0054_delete_settings.py b/kfet/migrations/0054_delete_settings.py index 7a0b1ab8..80ee1d24 100644 --- a/kfet/migrations/0054_delete_settings.py +++ b/kfet/migrations/0054_delete_settings.py @@ -21,7 +21,13 @@ def adapt_settings(apps, schema_editor): except Settings.DoesNotExist: pass - try_get('kfet_subvention_cof', 'SUBVENTION_COF', 'value_decimal') + try: + subvention = obj.get(name='SUBVENTION_COF').value_decimal + subvention_mult = 1 + subvention/100 + reduction = (1 - 1/subvention_mult) * 100 + cfg['kfet_reduction_cof'] = reduction + except Settings.DoesNotExist: + pass try_get('kfet_addcost_amount', 'ADDCOST_AMOUNT', 'value_decimal') try_get('kfet_addcost_for', 'ADDCOST_FOR', 'value_account') try_get('kfet_overdraft_duration', 'OVERDRAFT_DURATION', 'value_duration') From 5d6012b6bddf44189a1709c85120698117146f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 10 Apr 2017 11:52:57 +0200 Subject: [PATCH 23/25] Fix kfet tests - and add test for `kfet_config.subvention_cof` --- kfet/tests/test_config.py | 13 ++++++++++--- kfet/{tests.py => tests/test_statistic.py} | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) rename kfet/{tests.py => tests/test_statistic.py} (97%) diff --git a/kfet/tests/test_config.py b/kfet/tests/test_config.py index 79781c0d..03c9cf3c 100644 --- a/kfet/tests/test_config.py +++ b/kfet/tests/test_config.py @@ -22,13 +22,20 @@ class ConfigTest(TestCase): def test_get(self): self.assertTrue(hasattr(kfet_config, 'subvention_cof')) + def test_subvention_cof(self): + reduction_cof = Decimal('20') + subvention_cof = Decimal('25') + kfet_config.set(reduction_cof=reduction_cof) + + self.assertEqual(kfet_config.subvention_cof, subvention_cof) + def test_set_decimal(self): """Test field of decimal type.""" - subvention_cof = Decimal('10') + reduction_cof = Decimal('10') # IUT - kfet_config.set(subvention_cof=subvention_cof) + kfet_config.set(reduction_cof=reduction_cof) # check - self.assertEqual(kfet_config.subvention_cof, subvention_cof) + self.assertEqual(kfet_config.reduction_cof, reduction_cof) def test_set_modelinstance(self): """Test field of model instance type.""" diff --git a/kfet/tests.py b/kfet/tests/test_statistic.py similarity index 97% rename from kfet/tests.py rename to kfet/tests/test_statistic.py index 991b2545..4fb0785d 100644 --- a/kfet/tests.py +++ b/kfet/tests/test_statistic.py @@ -5,7 +5,7 @@ from unittest.mock import patch from django.test import TestCase, Client from django.contrib.auth.models import User, Permission -from .models import Account, Article, ArticleCategory +from kfet.models import Account, Article, ArticleCategory class TestStats(TestCase): From 18425b82c21c8c5d1fd0be0e2e7d776e5dd86b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 13 Apr 2017 15:15:59 +0200 Subject: [PATCH 24/25] Check negative on cancellation. - Like perform operations, cancel_operations can add/remove an account from negative accounts system. - Balances checks are now performed against real_balance instead of balance. So if someone with a balance_offset go, for real, to positive land (ie even without taking into account the balance offset), its account is removed from the negative system. - Fix bug on real_balance when negative exists but balance_offset is not set. Fixes #156. --- kfet/models.py | 25 ++++++++++++++++++++++++- kfet/views.py | 36 ++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index 7c03191a..af24db49 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -81,7 +81,7 @@ class Account(models.Model): # Propriétés supplémentaires @property def real_balance(self): - if (hasattr(self, 'negative')): + if hasattr(self, 'negative') and self.negative.balance_offset: return self.balance - self.negative.balance_offset return self.balance @@ -210,6 +210,29 @@ class Account(models.Model): def delete(self, *args, **kwargs): pass + def check_negative(self): + if self.real_balance < 0: + if hasattr(self, 'negative') and not self.negative.start: + self.negative.start = timezone.now() + self.negative.save() + elif not hasattr(self, 'negative'): + self.negative = ( + AccountNegative.objects.create( + account=self, start=timezone.now(), + ) + ) + elif hasattr(self, 'negative'): + # self.real_balance >= 0 + balance_offset = self.negative.balance_offset + if balance_offset: + ( + Account.objects + .filter(pk=self.pk) + .update(balance=F('balance')-balance_offset) + ) + self.refresh_from_db() + self.negative.delete() + class UserHasAccount(Exception): def __init__(self, trigramme): self.trigramme = trigramme diff --git a/kfet/views.py b/kfet/views.py index 330b195a..82ef8433 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1105,22 +1105,15 @@ def kpsul_perform_operations(request): with transaction.atomic(): # If not cash account, # saving account's balance and adding to Negative if not in - if not operationgroup.on_acc.is_cash: - Account.objects.filter(pk=operationgroup.on_acc.pk).update( - balance=F('balance') + operationgroup.amount) - operationgroup.on_acc.refresh_from_db() - if operationgroup.on_acc.balance < 0: - if hasattr(operationgroup.on_acc, 'negative'): - if not operationgroup.on_acc.negative.start: - operationgroup.on_acc.negative.start = timezone.now() - operationgroup.on_acc.negative.save() - else: - negative = AccountNegative( - account=operationgroup.on_acc, start=timezone.now()) - negative.save() - elif (hasattr(operationgroup.on_acc, 'negative') and - not operationgroup.on_acc.negative.balance_offset): - operationgroup.on_acc.negative.delete() + on_acc = operationgroup.on_acc + if not on_acc.is_cash: + ( + Account.objects + .filter(pk=on_acc.pk) + .update(balance=F('balance') + operationgroup.amount) + ) + on_acc.refresh_from_db() + on_acc.check_negative() # Updating checkout's balance if to_checkout_balance: @@ -1311,8 +1304,15 @@ def kpsul_cancel_operations(request): (Operation.objects.filter(pk__in=opes) .update(canceled_by=canceled_by, canceled_at=canceled_at)) for account in to_accounts_balances: - Account.objects.filter(pk=account.pk).update( - balance = F('balance') + to_accounts_balances[account]) + ( + Account.objects + .filter(pk=account.pk) + .update(balance=F('balance') + to_accounts_balances[account]) + ) + if not account.is_cash: + # Should always be true, but we want to be sure + account.refresh_from_db() + account.check_negative() for checkout in to_checkouts_balances: Checkout.objects.filter(pk=checkout.pk).update( balance = F('balance') + to_checkouts_balances[checkout]) From 9668f1d1ec94dc92d6befa6b61bcffc540eb1c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 13 Apr 2017 15:48:13 +0200 Subject: [PATCH 25/25] Account: check_negative() -> update_negative() --- kfet/models.py | 2 +- kfet/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index af24db49..6c1f1240 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -210,7 +210,7 @@ class Account(models.Model): def delete(self, *args, **kwargs): pass - def check_negative(self): + def update_negative(self): if self.real_balance < 0: if hasattr(self, 'negative') and not self.negative.start: self.negative.start = timezone.now() diff --git a/kfet/views.py b/kfet/views.py index 82ef8433..60dbb44b 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1113,7 +1113,7 @@ def kpsul_perform_operations(request): .update(balance=F('balance') + operationgroup.amount) ) on_acc.refresh_from_db() - on_acc.check_negative() + on_acc.update_negative() # Updating checkout's balance if to_checkout_balance: @@ -1312,7 +1312,7 @@ def kpsul_cancel_operations(request): if not account.is_cash: # Should always be true, but we want to be sure account.refresh_from_db() - account.check_negative() + account.update_negative() for checkout in to_checkouts_balances: Checkout.objects.filter(pk=checkout.pk).update( balance = F('balance') + to_checkouts_balances[checkout])