diff --git a/cof/settings_dev.py b/cof/settings_dev.py index 750a53ee..9e10d733 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -48,6 +48,7 @@ INSTALLED_APPS = ( 'widget_tweaks', 'django_js_reverse', 'custommail', + 'djconfig', ) MIDDLEWARE_CLASSES = ( @@ -61,6 +62,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', + 'djconfig.middleware.DjConfigMiddleware', ) ROOT_URLCONF = 'cof.urls' @@ -79,8 +81,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..76da5a79 --- /dev/null +++ b/kfet/config.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +from django.core.exceptions import ValidationError +from django.db import models + +from djconfig import config + + +class KFetConfig(object): + """kfet app configuration. + + Enhance isolation with backend used to store config. + Usable after DjConfig middleware was called. + + """ + 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): + """Get list of kfet app configuration. + + Returns: + (key, value) for each configuration entry as list. + + """ + # prevent circular imports + from kfet.forms import KFetConfigForm + return [(field.label, getattr(config, name), ) + for name, field in KFetConfigForm.base_fields.items()] + + def _get_dj_key(self, key): + return '{}{}'.format(self.prefix, key) + + def set(self, **kwargs): + """Update configuration value(s). + + Args: + **kwargs: Keyword arguments. Keys must be in kfet config. + Config entries are updated to given values. + + """ + # prevent circular imports + from kfet.forms import KFetConfigForm + + # get old config + new_cfg = KFetConfigForm().initial + # update to new config + for key, value in kwargs.items(): + dj_key = self._get_dj_key(key) + if isinstance(value, models.Model): + new_cfg[dj_key] = value.pk + else: + new_cfg[dj_key] = value + # save new config + cfg_form = KFetConfigForm(new_cfg) + if cfg_form.is_valid(): + cfg_form.save() + else: + raise ValidationError( + 'Invalid values in kfet_config.set: %(fields)s', + params={'fields': list(cfg_form.errors)}) + + +kfet_config = KFetConfig() diff --git a/kfet/consumers.py b/kfet/consumers.py index dcd69bdf..ee096368 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -1,26 +1,41 @@ # -*- 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. + + """ + + @classmethod + def encode_json(cls, content): + return json.dumps(content, cls=DjangoJSONEncoder) + + +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): - pass + """Check permissions on connection.""" + if message.user.has_perms(self.perms_connect): + super().connect(message, **kwargs) + else: + self.close() - def receive(self, content, **kwargs): - pass - def disconnect(self, message, **kwargs): - pass +class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer): + groups = ['kfet.kpsul'] + perms_connect = ['kfet.is_team'] 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 7acd0880..f89b8f08 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 # ----- @@ -389,40 +395,46 @@ 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_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 (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.', + 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 (en €)', + 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/0054_delete_settings.py b/kfet/migrations/0054_delete_settings.py new file mode 100644 index 00000000..80ee1d24 --- /dev/null +++ b/kfet/migrations/0054_delete_settings.py @@ -0,0 +1,58 @@ +# -*- 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: + 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') + 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', '0053_created_at'), + ('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 2ad1c091..6c1f1240 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) @@ -85,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 @@ -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 @@ -214,6 +210,29 @@ class Account(models.Model): def delete(self, *args, **kwargs): pass + def update_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 @@ -632,116 +651,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 1af7f277..b55f2b99 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -8,7 +8,6 @@ - {% 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") ); }); @@ -95,7 +94,7 @@ $(document).ready(function() {