From ee6de3562708a359767b9ecd01717f2c675f6084 Mon Sep 17 00:00:00 2001 From: Qwann Date: Fri, 10 Mar 2017 18:28:48 +0100 Subject: [PATCH 01/60] category addcost added --- .../0048_articlecategory_has_addcost.py | 19 +++++++++++++++++++ kfet/models.py | 5 ++++- kfet/views.py | 4 ++-- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 kfet/migrations/0048_articlecategory_has_addcost.py diff --git a/kfet/migrations/0048_articlecategory_has_addcost.py b/kfet/migrations/0048_articlecategory_has_addcost.py new file mode 100644 index 00000000..e79ad7e6 --- /dev/null +++ b/kfet/migrations/0048_articlecategory_has_addcost.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0047_auto_20170104_1528'), + ] + + operations = [ + migrations.AddField( + model_name='articlecategory', + name='has_addcost', + field=models.BooleanField(default=True), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index c6853577..129ea7f3 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -327,13 +327,16 @@ class CheckoutStatement(models.Model): balance=F('balance') - last_statement.balance_new + self.balance_new) super(CheckoutStatement, self).save(*args, **kwargs) + @python_2_unicode_compatible class ArticleCategory(models.Model): - name = models.CharField(max_length = 45) + name = models.CharField(max_length=45) + has_addcost = models.BooleanField(default=True) def __str__(self): return self.name + @python_2_unicode_compatible class Article(models.Model): name = models.CharField(max_length = 45) diff --git a/kfet/views.py b/kfet/views.py index 19574f11..2164de47 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -990,7 +990,7 @@ def kpsul_perform_operations(request): for operation in operations: if operation.type == Operation.PURCHASE: operation.amount = - operation.article.price * operation.article_nb - if is_addcost: + if is_addcost & operation.article.category.has_addcost: operation.addcost_for = addcost_for operation.addcost_amount = addcost_amount * operation.article_nb operation.amount -= operation.addcost_amount @@ -1001,7 +1001,7 @@ def kpsul_perform_operations(request): else: operation.is_checkout = False if operationgroup.on_acc.is_cof: - if is_addcost: + if is_addcost & operation.article.category.has_addcost: operation.addcost_amount = operation.addcost_amount / cof_grant_divisor operation.amount = operation.amount / cof_grant_divisor to_articles_stocks[operation.article] -= operation.article_nb From fcc2ab8810adb8a5a937ca21948b65c54c2660ec Mon Sep 17 00:00:00 2001 From: Qwann Date: Fri, 17 Mar 2017 19:17:36 +0100 Subject: [PATCH 02/60] frontend working --- kfet/templates/kfet/kpsul.html | 9 ++++++--- kfet/views.py | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 264422f0..7b907f80 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -646,7 +646,7 @@ $(document).ready(function() { }); $after.after(article_html); // Pour l'autocomplétion - articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock']]); + articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock'],article['category__has_addcost']]); } function getArticles() { @@ -830,8 +830,11 @@ $(document).ready(function() { while (i Date: Fri, 17 Mar 2017 19:23:44 +0100 Subject: [PATCH 03/60] migration renamed --- .../{0048_article_hidden.py => 0049_article_hidden.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename kfet/migrations/{0048_article_hidden.py => 0049_article_hidden.py} (88%) diff --git a/kfet/migrations/0048_article_hidden.py b/kfet/migrations/0049_article_hidden.py similarity index 88% rename from kfet/migrations/0048_article_hidden.py rename to kfet/migrations/0049_article_hidden.py index 63869f77..13c25508 100644 --- a/kfet/migrations/0048_article_hidden.py +++ b/kfet/migrations/0049_article_hidden.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('kfet', '0047_auto_20170104_1528'), + ('kfet', '0048_articlecategory_has_addcost'), ] operations = [ From de724a2c0d85ff4eaebbb850eaffe22b292d721c Mon Sep 17 00:00:00 2001 From: Qwann Date: Fri, 17 Mar 2017 19:53:23 +0100 Subject: [PATCH 04/60] PEP8 for perform_operation --- kfet/views.py | 93 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index b0415c30..7a2700d1 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -921,13 +921,14 @@ def kpsul_update_addcost(request): addcost_form = AddcostForm(request.POST) if not addcost_form.is_valid(): - data = { 'errors': { 'addcost': list(addcost_form.errors) } } + data = {'errors': {'addcost': list(addcost_form.errors)}} return JsonResponse(data, status=400) required_perms = ['kfet.manage_addcosts'] if not request.user.has_perms(required_perms): data = { 'errors': { - 'missing_perms': get_missing_perms(required_perms, request.user) + 'missing_perms': get_missing_perms(required_perms, + request.user) } } return JsonResponse(data, status=403) @@ -935,7 +936,8 @@ def kpsul_update_addcost(request): trigramme = addcost_form.cleaned_data['trigramme'] account = trigramme and Account.objects.get(trigramme=trigramme) or None Settings.objects.filter(name='ADDCOST_FOR').update(value_account=account) - Settings.objects.filter(name='ADDCOST_AMOUNT').update(value_decimal=addcost_form.cleaned_data['amount']) + (Settings.objects.filter(name='ADDCOST_AMOUNT') + .update(value_decimal=addcost_form.cleaned_data['amount'])) cache.delete('ADDCOST_FOR') cache.delete('ADDCOST_AMOUNT') data = { @@ -947,20 +949,23 @@ def kpsul_update_addcost(request): consumers.KPsul.group_send('kfet.kpsul', data) return JsonResponse(data) + def get_missing_perms(required_perms, user): - missing_perms_codenames = [ (perm.split('.'))[1] - for perm in required_perms if not user.has_perm(perm)] + missing_perms_codenames = [(perm.split('.'))[1] + for perm in required_perms + if not user.has_perm(perm)] missing_perms = list( - Permission.objects - .filter(codename__in=missing_perms_codenames) - .values_list('name', flat=True)) + Permission.objects + .filter(codename__in=missing_perms_codenames) + .values_list('name', flat=True)) return missing_perms + @teamkfet_required def kpsul_perform_operations(request): # Initializing response data - data = { 'operationgroup': 0, 'operations': [], - 'warnings': {}, 'errors': {} } + data = {'operationgroup': 0, 'operations': [], + 'warnings': {}, 'errors': {}} # Checking operationgroup operationgroup_form = KPsulOperationGroupForm(request.POST) @@ -968,7 +973,7 @@ def kpsul_perform_operations(request): data['errors']['operation_group'] = list(operationgroup_form.errors) # Checking operation_formset - operation_formset = KPsulOperationFormSet(request.POST) + operation_formset = KPsulOperationFormSet(request.POST) if not operation_formset.is_valid(): data['errors']['operations'] = list(operation_formset.errors) @@ -977,34 +982,36 @@ def kpsul_perform_operations(request): return JsonResponse(data, status=400) # Pre-saving (no commit) - operationgroup = operationgroup_form.save(commit = False) - operations = operation_formset.save(commit = False) + operationgroup = operationgroup_form.save(commit=False) + operations = operation_formset.save(commit=False) # Retrieving COF grant cof_grant = Settings.SUBVENTION_COF() # Retrieving addcosts data addcost_amount = Settings.ADDCOST_AMOUNT() - addcost_for = Settings.ADDCOST_FOR() + addcost_for = Settings.ADDCOST_FOR() # Initializing vars - required_perms = set() # Required perms to perform all operations + required_perms = set() # Required perms to perform all operations cof_grant_divisor = 1 + cof_grant / 100 - to_addcost_for_balance = 0 # For balance of addcost_for - to_checkout_balance = 0 # For balance of selected checkout - to_articles_stocks = defaultdict(lambda:0) # For stocks articles + to_addcost_for_balance = 0 # For balance of addcost_for + to_checkout_balance = 0 # For balance of selected checkout + to_articles_stocks = defaultdict(lambda: 0) # For stocks articles is_addcost = (addcost_for and addcost_amount - and addcost_for != operationgroup.on_acc) + and addcost_for != operationgroup.on_acc) need_comment = operationgroup.on_acc.need_comment - # Filling data of each operations + operationgroup + calculating other stuffs + # Filling data of each operations + # + operationgroup + calculating other stuffs for operation in operations: if operation.type == Operation.PURCHASE: operation.amount = - operation.article.price * operation.article_nb if is_addcost & operation.article.category.has_addcost: - operation.addcost_for = addcost_for - operation.addcost_amount = addcost_amount * operation.article_nb - operation.amount -= operation.addcost_amount - to_addcost_for_balance += operation.addcost_amount + operation.addcost_for = addcost_for + operation.addcost_amount = addcost_amount \ + * operation.article_nb + operation.amount -= operation.addcost_amount + to_addcost_for_balance += operation.addcost_amount if operationgroup.on_acc.is_cash: operation.is_checkout = True to_checkout_balance += -operation.amount @@ -1012,12 +1019,14 @@ def kpsul_perform_operations(request): operation.is_checkout = False if operationgroup.on_acc.is_cof: if is_addcost & operation.article.category.has_addcost: - operation.addcost_amount = operation.addcost_amount / cof_grant_divisor + operation.addcost_amount = operation.addcost_amount \ + / cof_grant_divisor operation.amount = operation.amount / cof_grant_divisor to_articles_stocks[operation.article] -= operation.article_nb else: if operationgroup.on_acc.is_cash: - data['errors']['account'] = 'Charge et retrait impossible sur LIQ' + data['errors']['account'] = ("Charge et retrait" + " impossible sur LIQ") to_checkout_balance += operation.amount operationgroup.amount += operation.amount if operation.type == Operation.DEPOSIT: @@ -1029,8 +1038,10 @@ def kpsul_perform_operations(request): if operationgroup.on_acc.is_cof: to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor - (perms, stop) = operationgroup.on_acc.perms_to_perform_operation( - amount = operationgroup.amount) + (perms, stop) = (operationgroup.on_acc + .perms_to_perform_operation( + amount=operationgroup.amount) + ) required_perms |= perms if need_comment: @@ -1059,7 +1070,7 @@ def kpsul_perform_operations(request): # 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) + balance=F('balance') + operationgroup.amount) operationgroup.on_acc.refresh_from_db() if operationgroup.on_acc.balance < 0: if hasattr(operationgroup.on_acc, 'negative'): @@ -1068,7 +1079,7 @@ def kpsul_perform_operations(request): operationgroup.on_acc.negative.save() else: negative = AccountNegative( - account = operationgroup.on_acc, start = timezone.now()) + account=operationgroup.on_acc, start = timezone.now()) negative.save() elif (hasattr(operationgroup.on_acc, 'negative') and not operationgroup.on_acc.negative.balance_offset): @@ -1077,12 +1088,12 @@ def kpsul_perform_operations(request): # Updating checkout's balance if to_checkout_balance: Checkout.objects.filter(pk=operationgroup.checkout.pk).update( - balance = F('balance') + to_checkout_balance) + balance=F('balance') + to_checkout_balance) # Saving addcost_for with new balance if there is one if is_addcost and to_addcost_for_balance: Account.objects.filter(pk=addcost_for.pk).update( - balance = F('balance') + to_addcost_for_balance) + balance=F('balance') + to_addcost_for_balance) # Saving operation group operationgroup.save() @@ -1097,7 +1108,7 @@ def kpsul_perform_operations(request): # Updating articles stock for article in to_articles_stocks: Article.objects.filter(pk=article.pk).update( - stock = F('stock') + to_articles_stocks[article]) + stock=F('stock') + to_articles_stocks[article]) # Websocket data websocket_data = {} @@ -1109,18 +1120,21 @@ def kpsul_perform_operations(request): 'at': operationgroup.at, 'is_cof': operationgroup.is_cof, 'comment': operationgroup.comment, - 'valid_by__trigramme': ( operationgroup.valid_by and - operationgroup.valid_by.trigramme or None), + 'valid_by__trigramme': (operationgroup.valid_by and + operationgroup.valid_by.trigramme or None), 'on_acc__trigramme': operationgroup.on_acc.trigramme, 'opes': [], }] for operation in operations: ope_data = { - 'id': operation.pk, 'type': operation.type, 'amount': operation.amount, + 'id': operation.pk, 'type': operation.type, + 'amount': operation.amount, 'addcost_amount': operation.addcost_amount, - 'addcost_for__trigramme': is_addcost and addcost_for.trigramme or None, + 'addcost_for__trigramme': is_addcost + and addcost_for.trigramme or None, 'is_checkout': operation.is_checkout, - 'article__name': operation.article and operation.article.name or None, + 'article__name': operation.article + and operation.article.name or None, 'article_nb': operation.article_nb, 'group_id': operationgroup.pk, 'canceled_by__trigramme': None, 'canceled_at': None, @@ -1134,7 +1148,7 @@ def kpsul_perform_operations(request): }] websocket_data['articles'] = [] # Need refresh from db cause we used update on querysets - articles_pk = [ article.pk for article in to_articles_stocks] + articles_pk = [article.pk for article in to_articles_stocks] articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk) for article in articles: websocket_data['articles'].append({ @@ -1144,6 +1158,7 @@ def kpsul_perform_operations(request): consumers.KPsul.group_send('kfet.kpsul', websocket_data) return JsonResponse(data) + @teamkfet_required def kpsul_cancel_operations(request): # Pour la réponse From 3b793dc7268b2e104e60bece36735acd65fc1776 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 28 Mar 2017 23:47:41 -0300 Subject: [PATCH 05/60] Add first improvements for inventory --- kfet/forms.py | 9 +- kfet/templates/kfet/inventory_create.html | 107 +++++++++++++++------- kfet/views.py | 3 +- 3 files changed, 86 insertions(+), 33 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 2b59e1b3..72a18ab6 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -457,7 +457,13 @@ class InventoryArticleForm(forms.Form): queryset = Article.objects.all(), widget = forms.HiddenInput(), ) - stock_new = forms.IntegerField(required = False) + stock_new = forms.IntegerField( + required=False, + widget=forms.NumberInput( + attrs={'class': 'form-control', + 'readonly': '',} + ) + ) def __init__(self, *args, **kwargs): super(InventoryArticleForm, self).__init__(*args, **kwargs) @@ -466,6 +472,7 @@ class InventoryArticleForm(forms.Form): self.stock_old = kwargs['initial']['stock_old'] self.category = kwargs['initial']['category'] self.category_name = kwargs['initial']['category__name'] + self.box_capacity = kwargs['initial']['box_capacity'] # ----- # Order forms diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index d4f53c3c..7c970c12 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -6,38 +6,83 @@ {% block content %} {% include 'kfet/base_messages.html' %} - -
- - - - - - - - - - {% for form in formset %} - {% ifchanged form.category %} +
+
+ +
ArticleThéo.Réel
+ - + + + + + + + - {% endifchanged %} - - {{ form.article }} - - - - - {% endfor %} - -
{{ form.category_name }}ArticleQuantité par caisseStock ThéoriqueCaisses en réserveCaisses en arrièreVracStock total
{{ form.name }}{{ form.stock_old }}{{ form.stock_new }}
- {% if not perms.kfet.add_inventory %} - - {% endif %} - {% csrf_token %} - {{ formset.management_form }} - -
+ + + {% for form in formset %} + {% ifchanged form.category %} + + {{ form.category_name }} + + + {% endifchanged %} + + {{ form.article }} + {{ form.name }} + {{ form.box_capacity }} + {{ form.stock_old }} + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
{{ form.stock_new }}
+ + + {% endfor %} + {{ formset.management_form }} + {% if not perms.kfet.add_inventory %} + + + {% else %} + + {% endif %} + {% csrf_token %} + + + + + + + + {% endblock %} diff --git a/kfet/views.py b/kfet/views.py index b6a3338a..69df6c3f 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1658,7 +1658,8 @@ def inventory_create(request): 'stock_old': article.stock, 'name' : article.name, 'category' : article.category_id, - 'category__name': article.category.name + 'category__name': article.category.name, + 'box_capacity': article.box_capacity or 0, }) cls_formset = formset_factory( From e6a1d16860a808f778f98d7f74d11a122886d695 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 29 Mar 2017 00:58:47 -0300 Subject: [PATCH 06/60] Websocket to manage possible kpsul conflicts --- kfet/static/kfet/css/index.css | 6 ++ kfet/templates/kfet/inventory_create.html | 74 ++++++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 563d3839..fa404b5f 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -549,3 +549,9 @@ thead .tooltip { .help-block { padding-top: 15px; } + +/* Inventaires */ + +.inventory_modified { + background:rgba(236,100,0,0.15); +} diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 7c970c12..2882d5ad 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -1,4 +1,10 @@ {% extends 'kfet/base.html' %} +{% load staticfiles %} + +{% block extra_head %} + + +{% endblock %} {% block title %}Nouvel inventaire{% endblock %} {% block content-header-title %}Nouvel inventaire{% endblock %} @@ -8,7 +14,7 @@ {% include 'kfet/base_messages.html' %}
-
+ @@ -71,6 +77,11 @@ From eb7d436b90f85e97079ef9203799a2c591bce8db Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 29 Mar 2017 20:43:48 -0300 Subject: [PATCH 07/60] Add "count finished" checkbox --- kfet/static/kfet/css/jconfirm-kfet.css | 1 + kfet/templates/kfet/inventory_create.html | 111 +++++++++++++--------- 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/kfet/static/kfet/css/jconfirm-kfet.css b/kfet/static/kfet/css/jconfirm-kfet.css index 0bd53ab7..1aee70f1 100644 --- a/kfet/static/kfet/css/jconfirm-kfet.css +++ b/kfet/static/kfet/css/jconfirm-kfet.css @@ -28,6 +28,7 @@ .jconfirm .jconfirm-box .content { border-bottom:1px solid #ddd; + padding:5px 10px; } .jconfirm .jconfirm-box input { diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 2882d5ad..7c611eb6 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -25,6 +25,7 @@ + @@ -32,40 +33,41 @@ {% ifchanged form.category %} - + {% endifchanged %} {{ form.article }} - - - - + + + - - - + {% endfor %} {{ formset.management_form }} {% if not perms.kfet.add_inventory %} - + {% else %} - + {% endif %} {% csrf_token %} @@ -84,20 +86,32 @@ $(document).ready(function() { */ $('input[type="number"]').on('input', function() { - var prefix = $(this).attr('prefix'); var $line = $(this).closest('tr'); - var box_capacity = +$line.find('#id_'+prefix+'-box_capacity').text(); - var box_cellar = +$line.find('#id_'+prefix+'-box_cellar').val(); - var box_bar = +$line.find('#id_'+prefix+'-box_bar').val(); - var misc = +$line.find('#id_'+prefix+'-misc').val(); - $line.find('#id_'+prefix+'-stock_new').val(box_capacity*(box_cellar +box_bar)+misc); + var box_capacity = +$line.find('.box_capacity').text(); + var box_cellar = +$line.find('.box_cellar input').val(); + var box_bar = +$line.find('.box_bar input').val(); + var misc = +$line.find('.misc input').val(); + $line.find('.stock_new input').val(box_capacity*(box_cellar +box_bar)+misc); + }); + + /* + * Remove warning if .finished is unchecked + */ + + $('.finished input').change(function() { + if (!$(this).is(":checked")) { + var $line = $(this).closest('tr'); + var id = $line.find('input[type="hidden"]').val(); + $(this).closest('tr').removeClass('inventory_modified'); + conflicts = conflicts.filter(item => item != id); + } }); /** * Websocket */ - var modified = []; + var conflicts = []; var websocket_msg_default = {'articles':[]} var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws'; @@ -107,50 +121,53 @@ $(document).ready(function() { socket.onmessage = function(e) { var data = $.extend({}, websocket_msg_default, JSON.parse(e.data)); for (let article of data['articles']) { - modified.push(article.id); - $('input[value="'+article.id+'"]').parent().addClass('inventory_modified'); + var $line = $('input[value="'+article.id+'"]').parent(); + if ($line.find('.finished input').prop("checked")) { + conflicts.push(article.id); + $line.addClass('inventory_modified'); + } } } $('input[type="submit"]').on("click", function(e) { + console.log(e.keyCode); e.preventDefault(); - var conflicts = []; - for (let id of modified) { - var $input = $('input[value="'+id+'"]'); - if ($input.siblings(":last").find('input').val() !== "") { - conflicts.push($input.next().text()); - } - } + conflicts = [...new Set(conflicts)]; //remove duplicates if (conflicts.length) { content = ''; content += "Conflits possibles :" content += '
    '; - for (let article of conflicts) { - content += '
  • '+article+'
  • '; + for (let id of conflicts) { + var name = $('input[value="'+id+'"]').siblings('.name').text(); + content += '
  • '+name+'
  • '; } content += '
' - return $.confirm({ - title: "Confirmer l'inventaire", - content: content, - backgroundDismiss: true, - animation: 'top', - closeAnimation: 'bottom', - keyboardEnabled: true, - confirm: function() { - $('#inventoryform').submit(); - }, - onOpen: function() { - var that = this; - this.$content.find('input').on('keydown', function(e) { - if (e.keyCode == 13) - that.$confirmButton.click(); - }); - }, - }); } else { - $('#inventoryform').submit(); + // Prevent erroneous enter key confirmations + // Kinda complicated to filter if click or enter key... + content="Voulez-vous confirmer l'inventaire ?"; } + + $.confirm({ + title: "Confirmer l'inventaire", + content: content, + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + confirm: function() { + $('#inventoryform').submit(); + }, + onOpen: function() { + var that = this; + this.$content.find('input').on('keydown', function(e) { + if (e.keyCode == 13) + that.$confirmButton.click(); + }); + }, + }); + }); From 31888e33ce54d9962dc2eb40d2366ec758283138 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 30 Mar 2017 13:30:55 -0300 Subject: [PATCH 08/60] simpler jquery selection --- kfet/templates/kfet/inventory_create.html | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 7c611eb6..80f9d4c0 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -39,27 +39,27 @@ {{ form.article }} - - + + - + {% endfor %} {{ formset.management_form }} @@ -88,10 +88,14 @@ $(document).ready(function() { $('input[type="number"]').on('input', function() { var $line = $(this).closest('tr'); var box_capacity = +$line.find('.box_capacity').text(); - var box_cellar = +$line.find('.box_cellar input').val(); - var box_bar = +$line.find('.box_bar input').val(); - var misc = +$line.find('.misc input').val(); - $line.find('.stock_new input').val(box_capacity*(box_cellar +box_bar)+misc); + var box_cellar = $line.find('.box_cellar input').val(); + var box_bar = $line.find('.box_bar input').val(); + var misc = $line.find('.misc input').val(); + if (box_cellar || box_bar || misc) + $line.find('.stock_new input').val( + box_capacity*((+box_cellar) +(+box_bar))+(+misc)); + else + $line.find('.stock_new input').val(''); }); /* @@ -107,6 +111,7 @@ $(document).ready(function() { } }); + /** * Websocket */ From 998838ca3ee6e7125c7f9a652110df8b24a13403 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 30 Mar 2017 13:31:16 -0300 Subject: [PATCH 09/60] Add update button --- kfet/static/kfet/css/index.css | 9 +++++ kfet/templates/kfet/inventory_create.html | 48 ++++++++++++++++++----- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index fa404b5f..643cd52f 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -555,3 +555,12 @@ thead .tooltip { .inventory_modified { background:rgba(236,100,0,0.15); } + +.stock_diff { + padding-left: 5px; + color:#C8102E; +} + +.inventory_update { + display:none; +} diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 80f9d4c0..e88612c7 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -58,6 +58,7 @@ @@ -99,16 +100,30 @@ $(document).ready(function() { }); /* - * Remove warning if .finished is unchecked + * Remove warning and update stock */ + function update_stock($line) { + $line.removeClass('inventory_modified'); + $line.find('.inventory_update').hide(); + + var old_stock = +$line.find('.current_stock').text() + var stock_diff = +$line.find('.stock_diff').text(); + $line.find('.current_stock').text(old_stock + stock_diff); + $line.find('.stock_diff').text(''); + + var id = $line.find('input[type="hidden"]').val(); + conflicts = conflicts.filter(item => item != id); + } + $('.finished input').change(function() { - if (!$(this).is(":checked")) { - var $line = $(this).closest('tr'); - var id = $line.find('input[type="hidden"]').val(); - $(this).closest('tr').removeClass('inventory_modified'); - conflicts = conflicts.filter(item => item != id); - } + var $line = $(this).closest('tr'); + update_stock($line); + }); + + $('.inventory_update button').click(function() { + var $line = $(this).closest('tr'); + update_stock($line); }); @@ -127,9 +142,20 @@ $(document).ready(function() { var data = $.extend({}, websocket_msg_default, JSON.parse(e.data)); for (let article of data['articles']) { var $line = $('input[value="'+article.id+'"]').parent(); - if ($line.find('.finished input').prop("checked")) { + if ($line.find('.finished input').is(":checked")) { conflicts.push(article.id); + //Display warning $line.addClass('inventory_modified'); + + //Realigning input and displaying update button + $line.find('.inventory_update').show(); + + //Displaying stock changes + var stock = $line.find('.current_stock').text(); + $line.find('.stock_diff').text(article.stock - stock); + } else { + // If we haven't counted the article yet, we simply update the expected stock + $line.find('.current_stock').text(article.stock); } } } @@ -144,8 +170,10 @@ $(document).ready(function() { content += "Conflits possibles :" content += '
    '; for (let id of conflicts) { - var name = $('input[value="'+id+'"]').siblings('.name').text(); - content += '
  • '+name+'
  • '; + var $line = $('input[value="'+id+'"]').closest('tr'); + var name = $line.find('.name').text(); + var stock_diff = $line.find('.stock_diff').text(); + content += '
  • '+name+' ('+stock_diff+')
  • '; } content += '
' } else { From 0c212383917d983cdd53f5a98e45475a9082ecce Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 30 Mar 2017 18:39:13 -0300 Subject: [PATCH 10/60] Change misc field when updating --- kfet/templates/kfet/inventory_create.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index e88612c7..1273d869 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -111,6 +111,12 @@ $(document).ready(function() { var stock_diff = +$line.find('.stock_diff').text(); $line.find('.current_stock').text(old_stock + stock_diff); $line.find('.stock_diff').text(''); + + if ($line.find('.stock_new input').val()) { + var old_misc = +$line.find('.misc input').val(); + $line.find('.misc input').val(old_misc + stock_diff) + .trigger('input'); + } var id = $line.find('input[type="hidden"]').val(); conflicts = conflicts.filter(item => item != id); From e54324e9f1a31ce5a9cf22dbb2219f5889fc7332 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 31 Mar 2017 09:50:37 -0300 Subject: [PATCH 11/60] Use col-offset --- kfet/templates/kfet/inventory_create.html | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 1273d869..17ab9dfc 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -48,16 +48,13 @@ From 50cb6e51a14be4ceb5315150091011c98ea0f2ed Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 31 Mar 2017 10:06:02 -0300 Subject: [PATCH 12/60] Use Set() natively --- kfet/templates/kfet/inventory_create.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 17ab9dfc..4fccc2be 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -134,7 +134,7 @@ $(document).ready(function() { * Websocket */ - var conflicts = []; + var conflicts = new Set(); var websocket_msg_default = {'articles':[]} var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws'; @@ -146,7 +146,7 @@ $(document).ready(function() { for (let article of data['articles']) { var $line = $('input[value="'+article.id+'"]').parent(); if ($line.find('.finished input').is(":checked")) { - conflicts.push(article.id); + conflicts.add(article.id); //Display warning $line.addClass('inventory_modified'); @@ -166,9 +166,8 @@ $(document).ready(function() { $('input[type="submit"]').on("click", function(e) { console.log(e.keyCode); e.preventDefault(); - conflicts = [...new Set(conflicts)]; //remove duplicates - if (conflicts.length) { + if (conflicts.size) { content = ''; content += "Conflits possibles :" content += '
    '; From 6ac1241bd38573305acb4dc2bafd14f21c5d5393 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 31 Mar 2017 10:07:42 -0300 Subject: [PATCH 13/60] Listen to submit instead --- kfet/templates/kfet/inventory_create.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 4fccc2be..44780dff 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -163,8 +163,7 @@ $(document).ready(function() { } } - $('input[type="submit"]').on("click", function(e) { - console.log(e.keyCode); + $('#inventoryform').on("submit", function(e) { e.preventDefault(); if (conflicts.size) { From 530aafad19c2a92ef6c6820f9794bf21741243f3 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 31 Mar 2017 11:07:37 -0300 Subject: [PATCH 14/60] Use widget_tweaks --- kfet/forms.py | 8 +------- kfet/templates/kfet/inventory_create.html | 3 ++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 72a18ab6..cee75ab2 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -457,13 +457,7 @@ class InventoryArticleForm(forms.Form): queryset = Article.objects.all(), widget = forms.HiddenInput(), ) - stock_new = forms.IntegerField( - required=False, - widget=forms.NumberInput( - attrs={'class': 'form-control', - 'readonly': '',} - ) - ) + stock_new = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): super(InventoryArticleForm, self).__init__(*args, **kwargs) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 44780dff..b8a1ac08 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -1,5 +1,6 @@ {% extends 'kfet/base.html' %} {% load staticfiles %} +{% load widget_tweaks %} {% block extra_head %} @@ -54,7 +55,7 @@
From 063446efb542ca914e72987c80c2229f029466a1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 1 Apr 2017 00:32:09 -0300 Subject: [PATCH 15/60] Use columns for authentication and submit --- kfet/static/kfet/css/index.css | 4 ++++ kfet/templates/kfet/inventory_create.html | 18 +++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 643cd52f..5a82b5cf 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -32,6 +32,7 @@ textarea { .table { margin-bottom:0; + border-bottom:1px solid #ddd; } .table { @@ -230,6 +231,9 @@ textarea { height:28px; margin:3px 0px; } + .content-center .auth-form { + margin:15px; +} /* * Pages formulaires seuls diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index b8a1ac08..3ae3337e 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -61,17 +61,17 @@ {% endfor %} - {{ formset.management_form }} - {% if not perms.kfet.add_inventory %} - - - {% else %} - - {% endif %} - {% csrf_token %} -
Caisses en arrière Vrac Stock totalCompte terminé
{{ form.category_name }}
{{ form.name }}{{ form.box_capacity }}{{ form.stock_old }} + {{ form.name }}{{ form.box_capacity }}{{ form.stock_old }}
+
+
+
{{ form.stock_new }}
{{ form.name }}{{ form.box_capacity }}{{ form.stock_old }}{{ form.box_capacity }}{{ form.stock_old }}
- +
-
+
-
+
{{ form.stock_new }}
{{ form.stock_new }}
+
-
-
+
-
-
+
-
-
{{ form.stock_new }}
+
{{ form.stock_new }}
-
{{ form.stock_new }}
+
{{ form.stock_new | attr:"readonly"| add_class:"form-control" }}
+ {{ formset.management_form }} + {% if not perms.kfet.add_inventory %} +
+ {% include "kfet/form_authentication_snippet.html" %} +
+ {% endif %} + + {% csrf_token %} +
From 8da832c1f7704b5bc0b935eaea2ddc3dcf023e39 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 1 Apr 2017 00:36:39 -0300 Subject: [PATCH 16/60] Use nice authentication in orders too --- kfet/templates/kfet/order_create.html | 10 ++++++---- kfet/templates/kfet/order_to_inventory.html | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index cbd84ba8..b419621b 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -65,11 +65,13 @@ {% endfor %} - {% if not perms.kfet.add_order %} - - {% endif %} {{ formset.management_form }} - + {% if not perms.kfet.add_inventory %} +
+ {% include "kfet/form_authentication_snippet.html" %} +
+ {% endif %} + diff --git a/kfet/templates/kfet/order_to_inventory.html b/kfet/templates/kfet/order_to_inventory.html index ab107065..ae7bc8af 100644 --- a/kfet/templates/kfet/order_to_inventory.html +++ b/kfet/templates/kfet/order_to_inventory.html @@ -41,11 +41,13 @@ {% endfor %} - {% if not perms.kfet.order_to_inventory %} - - {% endif %} {{ formset.management_form }} - + {% if not perms.kfet.add_inventory %} +
+ {% include "kfet/form_authentication_snippet.html" %} +
+ {% endif %} + From e20ab2f352557c6ae33acd620e8b8f1e41fc3f3d Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 1 Apr 2017 09:18:40 -0300 Subject: [PATCH 17/60] Use set delete() --- kfet/templates/kfet/inventory_create.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 3ae3337e..293bcd6b 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -117,7 +117,7 @@ $(document).ready(function() { } var id = $line.find('input[type="hidden"]').val(); - conflicts = conflicts.filter(item => item != id); + conflicts = conflicts.delete(id); } $('.finished input').change(function() { From 96597aa14693d7ca547769dfc33350d4813c84ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 2 Apr 2017 05:17:26 +0200 Subject: [PATCH 18/60] clean some kfet templates --- kfet/templates/kfet/account_read.html | 45 ++++++++++++--------- kfet/templates/kfet/account_read_title.html | 5 --- kfet/templates/kfet/article_read.html | 32 ++++++++------- 3 files changed, 43 insertions(+), 39 deletions(-) delete mode 100644 kfet/templates/kfet/account_read_title.html diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 50ab7f20..25e48926 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -13,12 +13,17 @@ {% if account.user == request.user %} - {% endif %} @@ -66,22 +71,22 @@ {% if account.user == request.user %}

Statistiques

-
-
-
-

Ma balance

-
-
+
+
+
+

Ma balance

+
-
-
-
-
-

Ma consommation

-
-
+
+
+
+
+
+

Ma consommation

+
-
+
+
{% endif %}
diff --git a/kfet/templates/kfet/account_read_title.html b/kfet/templates/kfet/account_read_title.html deleted file mode 100644 index 6712ed77..00000000 --- a/kfet/templates/kfet/account_read_title.html +++ /dev/null @@ -1,5 +0,0 @@ -{% if account.user == request.user %} - Mon compte -{% else %} - Informations du compte {{ account.trigramme }} -{% endif %} diff --git a/kfet/templates/kfet/article_read.html b/kfet/templates/kfet/article_read.html index 35b484a5..a980cc75 100644 --- a/kfet/templates/kfet/article_read.html +++ b/kfet/templates/kfet/article_read.html @@ -1,6 +1,11 @@ {% extends 'kfet/base.html' %} {% load staticfiles %} +{% block extra_head %} + + +{% endblock %} + {% block title %}Informations sur l'article {{ article }}{% endblock %} {% block content-header-title %}Article - {{ article.name }}{% endblock %} @@ -82,27 +87,26 @@

Statistiques

-
-
-
-

Ventes de {{ article.name }}

-
-
+
+
+
+

Ventes de {{ article.name }}

+
-
+
+
{% endblock %} -{% block extra_head %} - - - {% endblock %} From e8fdd083aae2301240e6906355c2b14cbf295cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 2 Apr 2017 05:34:34 +0200 Subject: [PATCH 19/60] delete unused class-views --- kfet/views.py | 37 +++---------------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 823a205d..14fc0b3b 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2034,43 +2034,12 @@ class JSONResponseMixin(object): return context -class JSONDetailView(JSONResponseMixin, - BaseDetailView): - """ - Returns a DetailView that renders a JSON - """ +class JSONDetailView(JSONResponseMixin, BaseDetailView): + """Returns a DetailView that renders a JSON.""" + def render_to_response(self, context): return self.render_to_json_response(context) -class HybridDetailView(JSONResponseMixin, - SingleObjectTemplateResponseMixin, - BaseDetailView): - """ - Returns a DetailView as an html page except if a JSON file is requested - by the GET method in which case it returns a JSON response. - """ - def render_to_response(self, context): - # Look for a 'format=json' GET argument - if self.request.GET.get('format') == 'json': - return self.render_to_json_response(context) - else: - return super(HybridDetailView, self).render_to_response(context) - - -class HybridListView(JSONResponseMixin, - MultipleObjectTemplateResponseMixin, - BaseListView): - """ - Returns a ListView as an html page except if a JSON file is requested - by the GET method in which case it returns a JSON response. - """ - def render_to_response(self, context): - # Look for a 'format=json' GET argument - if self.request.GET.get('format') == 'json': - return self.render_to_json_response(context) - else: - return super(HybridListView, self).render_to_response(context) - class ObjectResumeStat(JSONDetailView): """ From 78aa5df350a5344dda67f1c329011b399eaed06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 2 Apr 2017 12:55:44 +0200 Subject: [PATCH 20/60] fix template error --- kfet/templates/kfet/article_read.html | 1 - 1 file changed, 1 deletion(-) diff --git a/kfet/templates/kfet/article_read.html b/kfet/templates/kfet/article_read.html index a980cc75..9f2e7128 100644 --- a/kfet/templates/kfet/article_read.html +++ b/kfet/templates/kfet/article_read.html @@ -109,4 +109,3 @@ $(document).ready(function() { ); }); -{% endblock %} From f6022ecf7d4f8d48133123e90b619b7c9101bbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 2 Apr 2017 16:49:41 +0200 Subject: [PATCH 21/60] Add str to Transfer model + PEP8 this model --- kfet/models.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index b4af61c1..18929c4b 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -476,24 +476,29 @@ class TransferGroup(models.Model): related_name = "+", blank = True, null = True, default = None) + class Transfer(models.Model): group = models.ForeignKey( - TransferGroup, on_delete = models.PROTECT, - related_name = "transfers") + TransferGroup, on_delete=models.PROTECT, + related_name="transfers") from_acc = models.ForeignKey( - Account, on_delete = models.PROTECT, - related_name = "transfers_from") + Account, on_delete=models.PROTECT, + related_name="transfers_from") to_acc = models.ForeignKey( - Account, on_delete = models.PROTECT, - related_name = "transfers_to") - amount = models.DecimalField(max_digits = 6, decimal_places = 2) + Account, on_delete=models.PROTECT, + related_name="transfers_to") + amount = models.DecimalField(max_digits=6, decimal_places=2) # Optional canceled_by = models.ForeignKey( - Account, on_delete = models.PROTECT, - null = True, blank = True, default = None, - related_name = "+") + Account, on_delete=models.PROTECT, + null=True, blank=True, default=None, + related_name="+") canceled_at = models.DateTimeField( - null = True, blank = True, default = None) + null=True, blank=True, default=None) + + def __str__(self): + return '{} -> {}: {}€'.format(self.from_acc, self.to_acc, self.amount) + class OperationGroup(models.Model): on_acc = models.ForeignKey( From 87b9db520f5c73be52d6a3e8841217b376544d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 2 Apr 2017 17:03:20 +0200 Subject: [PATCH 22/60] Refactor py base stats and account balance stats New mixin: PkUrlMixin - use with SingleObjectMixin standard django mixin (used by DetailView...) - `get_object` use field declared in `pk_url_kwarg` to get... the object SingleResumeStat - clean (part of) py code AccountStatBalanceList - renamed from `AccountStatBalanceAll` - url modified - add permission checking (only the connected user can get balance stats manifest) - clean py code AccountStatBalance - cleaner filtering management - merge urls using this class - clean py code --- kfet/urls.py | 13 +-- kfet/views.py | 270 +++++++++++++++++++++++++------------------------- 2 files changed, 141 insertions(+), 142 deletions(-) diff --git a/kfet/urls.py b/kfet/urls.py index 94a9eaec..ddd40ce6 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -82,15 +82,12 @@ urlpatterns = [ views.AccountStatLastDay.as_view(), name = 'kfet.account.stat.last.day'), - url('^accounts/(?P.{3})/stat/balance/$', - views.AccountStatBalanceAll.as_view(), - name = 'kfet.account.stat.balance'), - url('^accounts/(?P.{3})/stat/balance/d/(?P\d*)/$', + url(r'^accounts/(?P.{3})/stat/balance/list$', + views.AccountStatBalanceList.as_view(), + name='kfet.account.stat.balance.list'), + url(r'^accounts/(?P.{3})/stat/balance$', views.AccountStatBalance.as_view(), - name = 'kfet.account.stat.balance.days'), - url('^accounts/(?P.{3})/stat/balance/anytime/$', - views.AccountStatBalance.as_view(), - name = 'kfet.account.stat.balance.anytime'), + name='kfet.account.stat.balance'), # ----- # Checkout urls diff --git a/kfet/views.py b/kfet/views.py index 14fc0b3b..b0c90083 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- +from urllib.parse import urlencode + from django.shortcuts import render, get_object_or_404, redirect from django.core.exceptions import PermissionDenied from django.core.cache import cache from django.views.generic import ListView, DetailView, TemplateView -from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin -from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin -from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView +from django.views.generic.detail import BaseDetailView +from django.views.generic.edit import CreateView, UpdateView from django.core.urlresolvers import reverse, reverse_lazy from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin @@ -2041,49 +2042,46 @@ class JSONDetailView(JSONResponseMixin, BaseDetailView): return self.render_to_json_response(context) -class ObjectResumeStat(JSONDetailView): +class PkUrlMixin(object): + + def get_object(self, *args, **kwargs): + get_by = self.kwargs.get(self.pk_url_kwarg) + return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by}) + + +class SingleResumeStat(JSONDetailView): """ Summarize all the stats of an object Handles JSONResponse + """ - context_object_name = '' id_prefix = '' - # nombre de vues à résumer - nb_stat = 2 - # Le combienième est celui par defaut ? - # (entre 0 et nb_stat-1) nb_default = 0 - stat_labels = ['stat_1', 'stat_2'] - stat_urls = ['url_1', 'url_2'] - # sert à renverser les urls - # utile de le surcharger quand l'url prend d'autres arguments que l'id - def get_object_url_kwargs(self, **kwargs): - return {'pk': self.object.id} - - def url_kwargs(self, **kwargs): - return [{}] * self.nb_stat + stats = [] + url_stat = None def get_context_data(self, **kwargs): # On n'hérite pas object_id = self.object.id - url_kwargs = self.url_kwargs() context = {} - stats = {} - for i in range(self.nb_stat): - stats[i] = { - 'label': self.stat_labels[i], - 'btn': "btn_%s_%d_%d" % (self.id_prefix, - object_id, - i), - 'url': reverse(self.stat_urls[i], - kwargs=dict( - self.get_object_url_kwargs(), - **url_kwargs[i] - ), - ), - } - prefix = "%s_%d" % (self.id_prefix, object_id) + stats = [] + prefix = '{}_{}'.format(self.id_prefix, object_id) + for i, stat_def in enumerate(self.stats): + url_pk = getattr(self.object, self.pk_url_kwarg) + url_params_d = stat_def.get('url_params', {}) + if len(url_params_d) > 0: + url_params = '?{}'.format(urlencode(url_params_d)) + else: + url_params = '' + stats.append({ + 'label': stat_def['label'], + 'btn': 'btn_{}_{}'.format(prefix, i), + 'url': '{url}{params}'.format( + url=reverse(self.url_stat, args=[url_pk]), + params=url_params, + ), + }) context['id_prefix'] = prefix context['content_id'] = "content_%s" % prefix context['stats'] = stats @@ -2100,39 +2098,47 @@ ID_PREFIX_ACC_BALANCE = "balance_acc" # Un résumé de toutes les vues ArticleStatBalance # REND DU JSON -class AccountStatBalanceAll(ObjectResumeStat): +class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): model = Account context_object_name = 'account' - trigramme_url_kwarg = 'trigramme' + pk_url_kwarg = 'trigramme' + url_stat = 'kfet.account.stat.balance' id_prefix = ID_PREFIX_ACC_BALANCE - nb_stat = 5 + stats = [ + { + 'label': 'Tout le temps', + }, + { + 'label': '1 an', + 'url_params': {'last_days': 365}, + }, + { + 'label': '6 mois', + 'url_params': {'last_days': 183}, + }, + { + 'label': '3 mois', + 'url_params': {'last_days': 90}, + }, + { + 'label': '30 jours', + 'url_params': {'last_days': 30}, + }, + ] nb_default = 0 - stat_labels = ["Tout le temps", "1 an", "6 mois", "3 mois", "30 jours"] - stat_urls = ['kfet.account.stat.balance.anytime'] \ - + ['kfet.account.stat.balance.days'] * 4 - def get_object(self, **kwargs): - trigramme = self.kwargs.get(self.trigramme_url_kwarg) - return get_object_or_404(Account, trigramme=trigramme) - - def get_object_url_kwargs(self, **kwargs): - return {'trigramme': self.object.trigramme} - - def url_kwargs(self, **kwargs): - context_list = (super(AccountStatBalanceAll, self) - .url_kwargs(**kwargs)) - context_list[1] = {'nb_date': 365} - context_list[2] = {'nb_date': 183} - context_list[3] = {'nb_date': 90} - context_list[4] = {'nb_date': 30} - return context_list + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise PermissionDenied + return obj @method_decorator(login_required) def dispatch(self, *args, **kwargs): - return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs) + return super().dispatch(*args, **kwargs) -class AccountStatBalance(JSONDetailView): +class AccountStatBalance(PkUrlMixin, JSONDetailView): """ Returns a JSON containing the evolution a the personnal balance of a trigramme between timezone.now() and `nb_days` @@ -2141,44 +2147,38 @@ class AccountStatBalance(JSONDetailView): does not takes into account the balance offset """ model = Account - trigramme_url_kwarg = 'trigramme' - nb_date_url_kwargs = 'nb_date' + pk_url_kwarg = 'trigramme' context_object_name = 'account' - id_prefix = "" - def get_object(self, **kwargs): - trigramme = self.kwargs.get(self.trigramme_url_kwarg) - return get_object_or_404(Account, trigramme=trigramme) - - def get_changes_list(self, **kwargs): + def get_changes_list(self, last_days=None, begin_date=None, end_date=None): account = self.object - nb_date = self.kwargs.get(self.nb_date_url_kwargs, None) - end_date = this_morning() - if nb_date is None: - begin_date = timezone.datetime(year=1980, month=1, day=1) - anytime = True - else: - begin_date = this_morning() \ - - timezone.timedelta(days=int(nb_date)) - anytime = False - # On récupère les opérations + + # prepare filters + if end_date is None: + end_date = this_morning() + + if last_days is not None: + begin_date = end_date - timezone.timedelta(days=last_days) + + # prepare querysets # TODO: retirer les opgroup dont tous les op sont annulées - opgroups = list(OperationGroup.objects - .filter(on_acc=account) - .filter(at__gte=begin_date) - .filter(at__lte=end_date)) - # On récupère les transferts reçus - received_transfers = list(Transfer.objects - .filter(to_acc=account) - .filter(canceled_at=None) - .filter(group__at__gte=begin_date) - .filter(group__at__lte=end_date)) - # On récupère les transferts émis - emitted_transfers = list(Transfer.objects - .filter(from_acc=account) - .filter(canceled_at=None) - .filter(group__at__gte=begin_date) - .filter(group__at__lte=end_date)) + opegroups = OperationGroup.objects.filter(on_acc=account) + recv_transfers = Transfer.objects.filter(to_acc=account, + canceled_at=None) + sent_transfers = Transfer.objects.filter(from_acc=account, + canceled_at=None) + + # apply filters + if begin_date is not None: + opegroups = opegroups.filter(at__gte=begin_date) + recv_transfers = recv_transfers.filter(group__at__gte=begin_date) + sent_transfers = sent_transfers.filter(group__at__gte=begin_date) + + if end_date is not None: + opegroups = opegroups.filter(at__lte=end_date) + recv_transfers = recv_transfers.filter(group__at__lte=end_date) + sent_transfers = sent_transfers.filter(group__at__lte=end_date) + # On transforme tout ça en une liste de dictionnaires sous la forme # {'at': date, # 'amount': changement de la balance (négatif si diminue la balance, @@ -2188,72 +2188,74 @@ class AccountStatBalance(JSONDetailView): # sera mis à jour lors d'une # autre passe) # } - actions = [ - # Maintenant (à changer si on gère autre chose que now) - { + actions = [] + if begin_date is not None: + actions.append({ + 'at': begin_date.isoformat(), + 'amount': 0, + 'label': "début", + 'balance': 0, + }) + if end_date is not None: + actions.append({ 'at': end_date.isoformat(), - 'amout': 0, + 'amount': 0, 'label': "actuel", 'balance': 0, - } - ] + [ + }) + + actions += [ { - 'at': op.at.isoformat(), - 'amount': op.amount, - 'label': str(op), + 'at': ope_grp.at.isoformat(), + 'amount': ope_grp.amount, + 'label': str(ope_grp), 'balance': 0, - } for op in opgroups + } for ope_grp in opegroups ] + [ { 'at': tr.group.at.isoformat(), 'amount': tr.amount, - 'label': "%d€: %s -> %s" % (tr.amount, - tr.from_acc.trigramme, - tr.to_acc.trigramme), + 'label': str(tr), 'balance': 0, - } for tr in received_transfers + } for tr in recv_transfers ] + [ { 'at': tr.group.at.isoformat(), 'amount': -tr.amount, - 'label': "%d€: %s -> %s" % (tr.amount, - tr.from_acc.trigramme, - tr.to_acc.trigramme), + 'label': str(tr), 'balance': 0, - } for tr in emitted_transfers + } for tr in sent_transfers ] - if not anytime: - actions += [ - # Date de début : - { - 'at': begin_date.isoformat(), - 'amount': 0, - 'label': "début", - 'balance': 0, - } - ] # Maintenant on trie la liste des actions par ordre du plus récent # an plus ancien et on met à jour la balance actions = sorted(actions, key=lambda k: k['at'], reverse=True) actions[0]['balance'] = account.balance for i in range(len(actions)-1): - actions[i+1]['balance'] = actions[i]['balance'] \ - - actions[i+1]['amount'] + actions[i+1]['balance'] = \ + actions[i]['balance'] - actions[i+1]['amount'] return actions - def get_context_data(self, **kwargs): + def get_context_data(self, *args, **kwargs): context = {} - changes = self.get_changes_list() - nb_days = self.kwargs.get(self.nb_date_url_kwargs, None) - if nb_days is None: - nb_days_string = 'anytime' - else: - nb_days_string = str(int(nb_days)) - context['charts'] = [ { "color": "rgb(255, 99, 132)", - "label": "Balance", - "values": changes } ] + + last_days = self.request.GET.get('last_days', None) + if last_days is not None: + last_days = int(last_days) + begin_date = self.request.GET.get('begin_date', None) + end_date = self.request.GET.get('end_date', None) + + changes = self.get_changes_list( + last_days=last_days, + begin_date=begin_date, end_date=end_date, + ) + + context['charts'] = [{ + "color": "rgb(255, 99, 132)", + "label": "Balance", + "values": changes, + }] context['is_time_chart'] = True - context['min_date'] = changes[len(changes)-1]['at'] + context['min_date'] = changes[-1]['at'] context['max_date'] = changes[0]['at'] # TODO: offset return context @@ -2274,7 +2276,7 @@ ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc" # Un résumé de toutes les vues ArticleStatLast # NE REND PAS DE JSON -class AccountStatLastAll(ObjectResumeStat): +class AccountStatLastAll(SingleResumeStat): model = Account context_object_name = 'account' trigramme_url_kwarg = 'trigramme' @@ -2419,7 +2421,7 @@ ID_PREFIX_ART_LAST_MONTHS = "last_months_art" # Un résumé de toutes les vues ArticleStatLast # NE REND PAS DE JSON -class ArticleStatLastAll(ObjectResumeStat): +class ArticleStatLastAll(SingleResumeStat): model = Article context_object_name = 'article' id_prefix = ID_PREFIX_ART_LAST From 1ee993e1e11a0318de6ae1251eb57e30655c3c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 2 Apr 2017 17:14:36 +0200 Subject: [PATCH 23/60] Add permission check to AccountStatBalance Only connected user can get its balance data --- kfet/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kfet/views.py b/kfet/views.py index b0c90083..58413d80 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2260,6 +2260,12 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): # TODO: offset return context + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise PermissionDenied + return obj + @method_decorator(login_required) def dispatch(self, *args, **kwargs): return super(AccountStatBalance, self).dispatch(*args, **kwargs) From df7e935390aceea21e5afbc9be9ef80f4693f9b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 2 Apr 2017 17:16:05 +0200 Subject: [PATCH 24/60] Clean GET params in ajax calls - Use `data` arg of `$.getJSON` for `format` param - Delete `dictToArray` call on data returned by `SingleResumeStat` class view since this view now returns stats manifest as an array --- kfet/static/kfet/js/statistic.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index 7ab56f1d..e6cc6132 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -42,8 +42,7 @@ $(this).addClass("focus"); // loads data and shows it - $.getJSON(this.stats_target_url + "?format=json", - displayStats); + $.getJSON(this.stats_target_url, {format: 'json'}, displayStats); } function displayStats (data) { @@ -158,7 +157,7 @@ "aria-label": "select-period"}); var to_click; - var context = dictToArray(data.stats); + var context = data.stats; for (var i = 0; i < context.length; i++) { // creates the button @@ -191,7 +190,7 @@ // constructor (function () { - $.getJSON(url + "?format=json", initialize); + $.getJSON(url, {format: 'json'}, initialize); })(); }; })(jQuery); From 48721b7dcafe376aa6a4b3d6d153922fabf96963 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 2 Apr 2017 17:54:13 -0300 Subject: [PATCH 25/60] Reduce graph size --- kfet/static/kfet/css/index.css | 1 + kfet/templates/kfet/account_read.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 563d3839..a65372da 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -105,6 +105,7 @@ textarea { .panel-md-margin{ background-color: white; + overflow:hidden; padding-left: 15px; padding-right: 15px; padding-bottom: 15px; diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 50ab7f20..be27192e 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -70,7 +70,7 @@

Ma balance

-
+
@@ -78,7 +78,7 @@

Ma consommation

-
+
From 31261fd3761aa25be035d08127c2244d6c59a64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 2 Apr 2017 23:38:42 +0200 Subject: [PATCH 26/60] set height canvas graph & fix graph display --- kfet/static/kfet/js/statistic.js | 2 +- kfet/templates/kfet/account_read.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index e6cc6132..f6d21237 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -138,7 +138,7 @@ var prev_chart = content.children(); // creates a blank canvas element and attach it to the DOM - var canvas = $(""); + var canvas = $(""); content.append(canvas); // create the chart diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 8333d0f0..449914f9 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -75,7 +75,7 @@ $(document).ready(function() {

Ma balance

-
+
@@ -83,7 +83,7 @@ $(document).ready(function() {

Ma consommation

-
+
From f585247224180840fb12825bbaa468006e0cd56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 00:40:52 +0200 Subject: [PATCH 27/60] Refactor Account Operations stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit K-Fêt - Statistics New base class - StatScale - create scale, given chunk size (timedelta), start and end times - get labels of - get start and end datetimes of chunks DayStatScale: Scale whose chunks interval is 1 day WeekStatScale: same with 1 week MonthStatScale: same with 1 month AccountStatOperationList: manifest of operations stats of an account - renamed from AccountStatLastAll - updated according to SingleResumeStat AccountStatOperation: - renamed from AccountStatLast - remove scale logic with use of StatScale objects - used scale is given by `scale` and `scale_args` GET params - add filter on operations types with `types` GET param AccountStatLast(Day,Week,Month) are deleted ("merged" in AccountStatOperation) --- kfet/statistic.py | 149 +++++++++++++++++++++++------------- kfet/urls.py | 18 ++--- kfet/views.py | 191 ++++++++++++++++++++++------------------------ 3 files changed, 195 insertions(+), 163 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 09f9e935..5b5e297a 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,70 +1,117 @@ # -*- coding: utf-8 -*- + +from dateutil.relativedelta import relativedelta + from django.utils import timezone from django.db.models import Sum KFET_WAKES_UP_AT = 7 -# donne le nom des jours d'une liste de dates -# dans un dico ordonné -def daynames(dates): - names = {} - for i in dates: - names[i] = dates[i].strftime("%A") - return names + +def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): + return timezone.datetime(year, month, day, hour=start_at) -# donne le nom des semaines une liste de dates -# dans un dico ordonné -def weeknames(dates): - names = {} - for i in dates: - names[i] = dates[i].strftime("Semaine %W") - return names +def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): + kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day) + if dt.hour < start_at: + kfet_dt -= timezone.timedelta(days=1) + return kfet_dt -# donne le nom des mois d'une liste de dates -# dans un dico ordonné -def monthnames(dates): - names = {} - for i in dates: - names[i] = dates[i].strftime("%B") - return names +class StatScale(object): + name = None + step = None + + def __init__(self, n_steps=0, begin=None, end=None, + last=False, std_chunk=True): + self.std_chunk = std_chunk + if last: + end = timezone.now() + + if begin is not None and n_steps != 0: + self.begin = self.get_from(begin) + self.end = self.do_step(self.begin, n_steps=n_steps) + elif end is not None and n_steps != 0: + self.end = self.get_from(end) + self.begin = self.do_step(self.end, n_steps=-n_steps) + elif begin is not None and end is not None: + self.begin = self.get_from(begin) + self.end = self.get_from(end) + else: + raise Exception('Two of these args must be specified: ' + 'n_steps, begin, end; ' + 'or use last and n_steps') + + self.datetimes = self.get_datetimes() + + @staticmethod + def by_name(name): + for cls in StatScale.__subclasses__(): + if cls.name == name: + return cls + raise Exception('scale %s not found' % name) + + def get_from(self, dt): + return self.std_chunk and self.get_chunk_start(dt) or dt + + def __getitem__(self, i): + return self.datetimes[i], self.datetimes[i+1] + + def __len__(self): + return len(self.datetimes) - 1 + + def do_step(self, dt, n_steps=1): + return dt + self.step * n_steps + + def get_datetimes(self): + datetimes = [self.begin] + tmp = self.begin + while tmp <= self.end: + tmp = self.do_step(tmp) + datetimes.append(tmp) + return datetimes + + def get_labels(self, label_fmt=None): + if label_fmt is None: + label_fmt = self.label_fmt + return [begin.strftime(label_fmt) for begin, end in self] + + @classmethod + def get_chunk_start(cls, dt): + dt_kfet = to_kfet_day(dt) + start = dt_kfet - cls.offset_to_chunk_start(dt_kfet) + return start -# rend les dates des nb derniers jours -# dans l'ordre chronologique -# aujourd'hui compris -# nb = 1 : rend hier -def lastdays(nb): - morning = this_morning() - days = {} - for i in range(1, nb+1): - days[i] = morning - timezone.timedelta(days=nb - i + 1) - return days +class DayStatScale(StatScale): + name = 'day' + step = timezone.timedelta(days=1) + label_fmt = '%A' + + @classmethod + def get_chunk_start(cls, dt): + return to_kfet_day(dt) -def lastweeks(nb): - monday_morning = this_monday_morning() - mondays = {} - for i in range(1, nb+1): - mondays[i] = monday_morning \ - - timezone.timedelta(days=7*(nb - i + 1)) - return mondays +class WeekStatScale(StatScale): + name = 'week' + step = timezone.timedelta(days=7) + label_fmt = 'Semaine %W' + + @classmethod + def offset_to_chunk_start(cls, dt): + return timezone.timedelta(days=dt.weekday()) -def lastmonths(nb): - first_month_day = this_first_month_day() - first_days = {} - this_year = first_month_day.year - this_month = first_month_day.month - for i in range(1, nb+1): - month = ((this_month - 1 - (nb - i)) % 12) + 1 - year = this_year + (nb - i) // 12 - first_days[i] = timezone.datetime(year=year, - month=month, - day=1, - hour=KFET_WAKES_UP_AT) - return first_days +class MonthStatScale(StatScale): + name = 'month' + step = relativedelta(months=1) + label_fmt = '%B' + + @classmethod + def get_chunk_start(cls, dt): + return to_kfet_day(dt).replace(day=1) def this_first_month_day(): diff --git a/kfet/urls.py b/kfet/urls.py index ddd40ce6..2eeef513 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -69,18 +69,12 @@ urlpatterns = [ name='kfet.account.negative'), # Account - Statistics - url('^accounts/(?P.{3})/stat/last/$', - views.AccountStatLastAll.as_view(), - name = 'kfet.account.stat.last'), - url('^accounts/(?P.{3})/stat/last/month/$', - views.AccountStatLastMonth.as_view(), - name = 'kfet.account.stat.last.month'), - url('^accounts/(?P.{3})/stat/last/week/$', - views.AccountStatLastWeek.as_view(), - name = 'kfet.account.stat.last.week'), - url('^accounts/(?P.{3})/stat/last/day/$', - views.AccountStatLastDay.as_view(), - name = 'kfet.account.stat.last.day'), + url('^accounts/(?P.{3})/stat/operations/list$', + views.AccountStatOperationList.as_view(), + name='kfet.account.stat.operation.list'), + url('^accounts/(?P.{3})/stat/operations$', + views.AccountStatOperation.as_view(), + name='kfet.account.stat.operation'), url(r'^accounts/(?P.{3})/stat/balance/list$', views.AccountStatBalanceList.as_view(), diff --git a/kfet/views.py b/kfet/views.py index 58413d80..0b62cd2e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import ast from urllib.parse import urlencode from django.shortcuts import render, get_object_or_404, redirect @@ -47,10 +48,10 @@ from decimal import Decimal import django_cas_ng import heapq import statistics -from .statistic import daynames, monthnames, weeknames, \ - lastdays, lastweeks, lastmonths, \ - this_morning, this_monday_morning, this_first_month_day, \ - tot_ventes +from kfet.statistic import DayStatScale, MonthStatScale, StatScale, WeekStatScale +from .statistic import ( + this_morning, this_monday_morning, this_first_month_day, tot_ventes, +) class Home(TemplateView): template_name = "kfet/home.html" @@ -2282,138 +2283,128 @@ ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc" # Un résumé de toutes les vues ArticleStatLast # NE REND PAS DE JSON -class AccountStatLastAll(SingleResumeStat): +class AccountStatOperationList(PkUrlMixin, SingleResumeStat): model = Account context_object_name = 'account' - trigramme_url_kwarg = 'trigramme' + pk_url_kwarg = 'trigramme' id_prefix = ID_PREFIX_ACC_LAST - nb_stat = 3 nb_default = 2 - stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"] - stat_urls = ['kfet.account.stat.last.month', - 'kfet.account.stat.last.week', - 'kfet.account.stat.last.day'] + stats = [ + { + 'label': 'Derniers mois', + 'url_params': dict( + types=[Operation.PURCHASE], + scale=MonthStatScale.name, + scale_args=dict( + last=True, + n_steps=7, + ), + ), + }, + { + 'label': 'Dernières semaines', + 'url_params': dict( + types=[Operation.PURCHASE], + last_days=49, + scale=WeekStatScale.name, + scale_args=dict( + last=True, + n_steps=7, + ), + ), + }, + { + 'label': 'Derniers jours', + 'url_params': dict( + types=[Operation.PURCHASE], + last_days=7, + scale=DayStatScale.name, + scale_args=dict( + last=True, + n_steps=7, + ), + ), + }, + ] + url_stat = 'kfet.account.stat.operation' - def get_object(self, **kwargs): - trigramme = self.kwargs.get(self.trigramme_url_kwarg) - return get_object_or_404(Account, trigramme=trigramme) - - def get_object_url_kwargs(self, **kwargs): - return {'trigramme': self.object.trigramme} + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise PermissionDenied + return obj @method_decorator(login_required) def dispatch(self, *args, **kwargs): - return super(AccountStatLastAll, self).dispatch(*args, **kwargs) + return super().dispatch(*args, **kwargs) -class AccountStatLast(JSONDetailView): +class AccountStatOperation(PkUrlMixin, JSONDetailView): """ Returns a JSON containing the evolution a the personnal consommation of a trigramme at the diffent dates specified """ model = Account - trigramme_url_kwarg = 'trigramme' + pk_url_kwarg = 'trigramme' context_object_name = 'account' - end_date = timezone.now() id_prefix = "" - # doit rendre un dictionnaire des dates - # la première date correspond au début - # la dernière date est la fin de la dernière plage - def get_dates(self, **kwargs): - return {} - - # doit rendre un dictionnaire des labels - # le dernier label ne sera pas utilisé - def get_labels(self, **kwargs): - pass - - def get_object(self, **kwargs): - trigramme = self.kwargs.get(self.trigramme_url_kwarg) - return get_object_or_404(Account, trigramme=trigramme) - - def sort_operations(self, **kwargs): - # On récupère les dates - dates = self.get_dates() - # On ajoute la date de fin - extended_dates = dates.copy() - extended_dates[len(dates)+1] = self.end_date + def get_operations(self, types, scale): # On selectionne les opérations qui correspondent # à l'article en question et qui ne sont pas annulées # puis on choisi pour chaques intervalle les opérations # effectuées dans ces intervalles de temps all_operations = (Operation.objects - .filter(type='purchase') + .filter(type__in=types) .filter(group__on_acc=self.object) .filter(canceled_at=None) ) - operations = {} - for i in dates: - operations[i] = (all_operations - .filter(group__at__gte=extended_dates[i]) - .filter(group__at__lte=extended_dates[i+1]) - ) + operations = [] + for begin, end in scale: + operations.append(all_operations + .filter(group__at__gte=begin, + group__at__lte=end)) return operations def get_context_data(self, **kwargs): context = {} - nb_ventes = {} + + scale = self.request.GET.get('scale', None) + if scale is None: + scale = DayStatScale(n_steps=7, last=True) + else: + scale_cls = StatScale.by_name(scale) + scale_args = self.request.GET.get('scale_args', '{}') + scale = scale_cls(**ast.literal_eval(scale_args)) + print(scale.datetimes) + + types = self.request.GET.get('types', None) + if types is not None: + types = ast.literal_eval(types) + + operations = self.get_operations(types=types, scale=scale) # On récupère les labels des dates - context['labels'] = self.get_labels().copy() + context['labels'] = scale.get_labels() # On compte les opérations - operations = self.sort_operations() - for i in operations: - nb_ventes[i] = tot_ventes(operations[i]) - context['charts'] = [ { "color": "rgb(255, 99, 132)", - "label": "NB items achetés", - "values": nb_ventes } ] + nb_ventes = [] + for chunk in operations: + nb_ventes.append(tot_ventes(chunk)) + + context['labels'] = scale.get_labels() + context['charts'] = [{"color": "rgb(255, 99, 132)", + "label": "NB items achetés", + "values": nb_ventes}] return context + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise PermissionDenied + return obj + @method_decorator(login_required) def dispatch(self, *args, **kwargs): - return super(AccountStatLast, self).dispatch(*args, **kwargs) - - -# Rend les achats pour ce compte des 7 derniers jours -# Aujourd'hui non compris -class AccountStatLastDay(AccountStatLast): - end_date = this_morning() - id_prefix = ID_PREFIX_ACC_LAST_DAYS - - def get_dates(self, **kwargs): - return lastdays(7) - - def get_labels(self, **kwargs): - days = lastdays(7) - return daynames(days) - - -# Rend les achats de ce compte des 7 dernières semaines -# La semaine en cours n'est pas comprise -class AccountStatLastWeek(AccountStatLast): - end_date = this_monday_morning() - id_prefix = ID_PREFIX_ACC_LAST_WEEKS - - def get_dates(self, **kwargs): - return lastweeks(7) - - def get_labels(self, **kwargs): - weeks = lastweeks(7) - return weeknames(weeks) - - -# Rend les achats de ce compte des 7 derniers mois -# Le mois en cours n'est pas compris -class AccountStatLastMonth(AccountStatLast): - end_date = this_monday_morning() - id_prefix = ID_PREFIX_ACC_LAST_MONTHS - - def get_dates(self, **kwargs): - return lastmonths(7) - - def get_labels(self, **kwargs): - months = lastmonths(7) - return monthnames(months) + return super().dispatch(*args, **kwargs) # ------------------------ From c01de558e1e1fd64006fe1559bcc968d9de42725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 03:12:52 +0200 Subject: [PATCH 28/60] Clean Article stats kfet.statistic - delete no longer used defs - new mixin - ScaleMixin - get scale args from GET params - chunkify querysets according to a scale Article stats - use SingleResumeStat for manifest - use ScaleMixin for sales - update urls - update permission required: teamkfet Account stats - update permission required: teamkfet - operations use ScaleMixin - fix manifests urls --- kfet/statistic.py | 93 ++++++++--- kfet/templates/kfet/account_read.html | 4 +- kfet/templates/kfet/article_read.html | 7 +- kfet/urls.py | 20 +-- kfet/views.py | 229 ++++++-------------------- 5 files changed, 132 insertions(+), 221 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 5b5e297a..8dd2e6d0 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import ast + from dateutil.relativedelta import relativedelta from django.utils import timezone @@ -50,7 +52,7 @@ class StatScale(object): for cls in StatScale.__subclasses__(): if cls.name == name: return cls - raise Exception('scale %s not found' % name) + return None def get_from(self, dt): return self.std_chunk and self.get_chunk_start(dt) or dt @@ -114,32 +116,38 @@ class MonthStatScale(StatScale): return to_kfet_day(dt).replace(day=1) -def this_first_month_day(): - now = timezone.now() - first_day = timezone.datetime(year=now.year, - month=now.month, - day=1, - hour=KFET_WAKES_UP_AT) - return first_day +def stat_manifest(scales_def=None, scale_args=None, **url_params): + if scales_def is None: + scales_def = [] + if scale_args is None: + scale_args = {} + return [ + dict( + label=label, + url_params=dict( + scale=cls.name, + scale_args=scale_args, + **url_params, + ), + ) + for label, cls in scales_def + ] -def this_monday_morning(): - now = timezone.now() - monday = now - timezone.timedelta(days=now.isoweekday()-1) - monday_morning = timezone.datetime(year=monday.year, - month=monday.month, - day=monday.day, - hour=KFET_WAKES_UP_AT) - return monday_morning - - -def this_morning(): - now = timezone.now() - morning = timezone.datetime(year=now.year, - month=now.month, - day=now.day, - hour=KFET_WAKES_UP_AT) - return morning +def last_stats_manifest(scales_def=None, scale_args=None, **url_params): + scales_def = [ + ('Derniers mois', MonthStatScale, ), + ('Dernières semaines', WeekStatScale, ), + ('Derniers jours', DayStatScale, ), + ] + if scale_args is None: + scale_args = {} + scale_args.update(dict( + last=True, + n_steps=7, + )) + return stat_manifest(scales_def=scales_def, scale_args=scale_args, + **url_params) # Étant donné un queryset d'operations @@ -147,3 +155,38 @@ def this_morning(): def tot_ventes(queryset): res = queryset.aggregate(Sum('article_nb'))['article_nb__sum'] return res and res or 0 + + +class ScaleMixin(object): + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + + scale_name = self.request.GET.get('scale', None) + scale_args = self.request.GET.get('scale_args', None) + + cls = StatScale.by_name(scale_name) + if cls is None: + scale = self.get_default_scale() + else: + scale_args = self.request.GET.get('scale_args', {}) + if isinstance(scale_args, str): + scale_args = ast.literal_eval(scale_args) + scale = cls(**scale_args) + + self.scale = scale + context['labels'] = scale.get_labels() + return context + + def get_default_scale(self): + return DayStatScale(n_steps=7, last=True) + + def chunkify_qs(self, qs, scale, field=None): + if field is None: + field = 'at' + begin_f = '{}__gte'.format(field) + end_f = '{}__lte'.format(field) + return [ + qs.filter(**{begin_f: begin, end_f: end}) + for begin, end in scale + ] diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 449914f9..3c2ccbcd 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -17,11 +17,11 @@ + +{% endblock %} diff --git a/kfet/urls.py b/kfet/urls.py index 2eeef513..0ffeb84f 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -140,20 +140,14 @@ urlpatterns = [ # Article - Update url('^articles/(?P\d+)/edit$', teamkfet_required(views.ArticleUpdate.as_view()), - name = 'kfet.article.update'), + name='kfet.article.update'), # Article - Statistics - url('^articles/(?P\d+)/stat/last/$', - views.ArticleStatLastAll.as_view(), - name = 'kfet.article.stat.last'), - url('^articles/(?P\d+)/stat/last/month/$', - views.ArticleStatLastMonth.as_view(), - name = 'kfet.article.stat.last.month'), - url('^articles/(?P\d+)/stat/last/week/$', - views.ArticleStatLastWeek.as_view(), - name = 'kfet.article.stat.last.week'), - url('^articles/(?P\d+)/stat/last/day/$', - views.ArticleStatLastDay.as_view(), - name = 'kfet.article.stat.last.day'), + url(r'^articles/(?P\d+)/stat/sales/list$', + views.ArticleStatSalesList.as_view(), + name='kfet.article.stat.sales.list'), + url(r'^articles/(?P\d+)/stat/sales$', + views.ArticleStatSales.as_view(), + name='kfet.article.stat.sales'), # ----- # K-Psul urls diff --git a/kfet/views.py b/kfet/views.py index 0b62cd2e..3b50cc0a 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -48,10 +48,8 @@ from decimal import Decimal import django_cas_ng import heapq import statistics -from kfet.statistic import DayStatScale, MonthStatScale, StatScale, WeekStatScale -from .statistic import ( - this_morning, this_monday_morning, this_first_month_day, tot_ventes, -) +from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes + class Home(TemplateView): template_name = "kfet/home.html" @@ -2155,10 +2153,8 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): account = self.object # prepare filters - if end_date is None: - end_date = this_morning() - if last_days is not None: + end_date = timezone.now() begin_date = end_date - timezone.timedelta(days=last_days) # prepare querysets @@ -2289,43 +2285,7 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat): pk_url_kwarg = 'trigramme' id_prefix = ID_PREFIX_ACC_LAST nb_default = 2 - stats = [ - { - 'label': 'Derniers mois', - 'url_params': dict( - types=[Operation.PURCHASE], - scale=MonthStatScale.name, - scale_args=dict( - last=True, - n_steps=7, - ), - ), - }, - { - 'label': 'Dernières semaines', - 'url_params': dict( - types=[Operation.PURCHASE], - last_days=49, - scale=WeekStatScale.name, - scale_args=dict( - last=True, - n_steps=7, - ), - ), - }, - { - 'label': 'Derniers jours', - 'url_params': dict( - types=[Operation.PURCHASE], - last_days=7, - scale=DayStatScale.name, - scale_args=dict( - last=True, - n_steps=7, - ), - ), - }, - ] + stats = last_stats_manifest(types=[Operation.PURCHASE]) url_stat = 'kfet.account.stat.operation' def get_object(self, *args, **kwargs): @@ -2339,7 +2299,7 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat): return super().dispatch(*args, **kwargs) -class AccountStatOperation(PkUrlMixin, JSONDetailView): +class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): """ Returns a JSON containing the evolution a the personnal consommation of a trigramme at the diffent dates specified @@ -2359,38 +2319,24 @@ class AccountStatOperation(PkUrlMixin, JSONDetailView): .filter(group__on_acc=self.object) .filter(canceled_at=None) ) - operations = [] - for begin, end in scale: - operations.append(all_operations - .filter(group__at__gte=begin, - group__at__lte=end)) - return operations + chunks = self.chunkify_qs(all_operations, scale, field='group__at') + return chunks - def get_context_data(self, **kwargs): - context = {} - - scale = self.request.GET.get('scale', None) - if scale is None: - scale = DayStatScale(n_steps=7, last=True) - else: - scale_cls = StatScale.by_name(scale) - scale_args = self.request.GET.get('scale_args', '{}') - scale = scale_cls(**ast.literal_eval(scale_args)) - print(scale.datetimes) + def get_context_data(self, *args, **kwargs): + old_ctx = super().get_context_data(*args, **kwargs) + context = {'labels': old_ctx['labels']} + scale = self.scale types = self.request.GET.get('types', None) if types is not None: types = ast.literal_eval(types) operations = self.get_operations(types=types, scale=scale) - # On récupère les labels des dates - context['labels'] = scale.get_labels() # On compte les opérations nb_ventes = [] for chunk in operations: nb_ventes.append(tot_ventes(chunk)) - context['labels'] = scale.get_labels() context['charts'] = [{"color": "rgb(255, 99, 132)", "label": "NB items achetés", "values": nb_ventes}] @@ -2418,141 +2364,66 @@ ID_PREFIX_ART_LAST_MONTHS = "last_months_art" # Un résumé de toutes les vues ArticleStatLast # NE REND PAS DE JSON -class ArticleStatLastAll(SingleResumeStat): +class ArticleStatSalesList(SingleResumeStat): model = Article context_object_name = 'article' id_prefix = ID_PREFIX_ART_LAST - nb_stat = 3 nb_default = 2 - stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"] - stat_urls = ['kfet.article.stat.last.month', - 'kfet.article.stat.last.week', - 'kfet.article.stat.last.day'] + url_stat = 'kfet.article.stat.sales' + stats = last_stats_manifest() - @method_decorator(login_required) + @method_decorator(teamkfet_required) def dispatch(self, *args, **kwargs): - return super(ArticleStatLastAll, self).dispatch(*args, **kwargs) + return super().dispatch(*args, **kwargs) -class ArticleStatLast(JSONDetailView): +class ArticleStatSales(ScaleMixin, JSONDetailView): """ Returns a JSON containing the consommation of an article at the diffent dates precised """ model = Article context_object_name = 'article' - end_date = timezone.now() - id_prefix = "" - def render_to_response(self, context): - # Look for a 'format=json' GET argument - if self.request.GET.get('format') == 'json': - return self.render_to_json_response(context) - else: - return super(ArticleStatLast, self).render_to_response(context) + def get_context_data(self, *args, **kwargs): + old_ctx = super().get_context_data(*args, **kwargs) + context = {'labels': old_ctx['labels']} + scale = self.scale - # doit rendre un dictionnaire des dates - # la première date correspond au début - # la dernière date est la fin de la dernière plage - def get_dates(self, **kwargs): - pass - - # doit rendre un dictionnaire des labels - # le dernier label ne sera pas utilisé - def get_labels(self, **kwargs): - pass - - def get_context_data(self, **kwargs): - context = {} - # On récupère les labels des dates - context['labels'] = self.get_labels().copy() - # On récupère les dates - dates = self.get_dates() - # On ajoute la date de fin - extended_dates = dates.copy() - extended_dates[len(dates)+1] = self.end_date # On selectionne les opérations qui correspondent # à l'article en question et qui ne sont pas annulées # puis on choisi pour chaques intervalle les opérations # effectuées dans ces intervalles de temps - all_operations = (Operation.objects - .filter(type='purchase') - .filter(article=self.object) - .filter(canceled_at=None) - ) - operations = {} - for i in dates: - operations[i] = (all_operations - .filter(group__at__gte=extended_dates[i]) - .filter(group__at__lte=extended_dates[i+1]) - ) + all_operations = ( + Operation.objects + .filter(type=Operation.PURCHASE, + article=self.object, + canceled_at=None, + ) + ) + chunks = self.chunkify_qs(all_operations, scale, field='group__at') # On compte les opérations - nb_ventes = {} - nb_accounts = {} - nb_liq = {} - for i in operations: - nb_ventes[i] = tot_ventes(operations[i]) - nb_liq[i] = tot_ventes( - operations[i] - .filter(group__on_acc__trigramme='LIQ') - ) - nb_accounts[i] = tot_ventes( - operations[i] - .exclude(group__on_acc__trigramme='LIQ') - ) - context['charts'] = [ { "color": "rgb(255, 99, 132)", - "label": "Toutes consommations", - "values": nb_ventes }, - { "color": "rgb(54, 162, 235)", - "label": "LIQ", - "values": nb_liq }, - { "color": "rgb(255, 205, 86)", - "label": "Comptes K-Fêt", - "values": nb_accounts } ] + nb_ventes = [] + nb_accounts = [] + nb_liq = [] + for qs in chunks: + nb_ventes.append( + tot_ventes(qs)) + nb_liq.append( + tot_ventes(qs.filter(group__on_acc__trigramme='LIQ'))) + nb_accounts.append( + tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ'))) + context['charts'] = [{"color": "rgb(255, 99, 132)", + "label": "Toutes consommations", + "values": nb_ventes}, + {"color": "rgb(54, 162, 235)", + "label": "LIQ", + "values": nb_liq}, + {"color": "rgb(255, 205, 86)", + "label": "Comptes K-Fêt", + "values": nb_accounts}] return context - @method_decorator(login_required) + @method_decorator(teamkfet_required) def dispatch(self, *args, **kwargs): - return super(ArticleStatLast, self).dispatch(*args, **kwargs) - - -# Rend les ventes des 7 derniers jours -# Aujourd'hui non compris -class ArticleStatLastDay(ArticleStatLast): - end_date = this_morning() - id_prefix = ID_PREFIX_ART_LAST_DAYS - - def get_dates(self, **kwargs): - return lastdays(7) - - def get_labels(self, **kwargs): - days = lastdays(7) - return daynames(days) - - -# Rend les ventes de 7 dernières semaines -# La semaine en cours n'est pas comprise -class ArticleStatLastWeek(ArticleStatLast): - end_date = this_monday_morning() - id_prefix = ID_PREFIX_ART_LAST_WEEKS - - def get_dates(self, **kwargs): - return lastweeks(7) - - def get_labels(self, **kwargs): - weeks = lastweeks(7) - return weeknames(weeks) - - -# Rend les ventes des 7 derniers mois -# Le mois en cours n'est pas compris -class ArticleStatLastMonth(ArticleStatLast): - end_date = this_monday_morning() - id_prefix = ID_PREFIX_ART_LAST_MONTHS - - def get_dates(self, **kwargs): - return lastmonths(7) - - def get_labels(self, **kwargs): - months = lastmonths(7) - return monthnames(months) + return super().dispatch(*args, **kwargs) From d97a7be8196800562b3c4a4bbd7e3e7ff8eb01b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 03:15:07 +0200 Subject: [PATCH 29/60] stats: fix begin of balances graphs - graph begin at first operation or later --- kfet/views.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 3b50cc0a..fdcb3763 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2185,23 +2185,8 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): # sera mis à jour lors d'une # autre passe) # } - actions = [] - if begin_date is not None: - actions.append({ - 'at': begin_date.isoformat(), - 'amount': 0, - 'label': "début", - 'balance': 0, - }) - if end_date is not None: - actions.append({ - 'at': end_date.isoformat(), - 'amount': 0, - 'label': "actuel", - 'balance': 0, - }) - actions += [ + actions = [ { 'at': ope_grp.at.isoformat(), 'amount': ope_grp.amount, From b3a9ad8a964dc3128b404316eb32e92698bc526a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 13:24:04 +0200 Subject: [PATCH 30/60] clean mixin --- kfet/statistic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 8dd2e6d0..2abb4fd8 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -163,7 +163,6 @@ class ScaleMixin(object): context = super().get_context_data(*args, **kwargs) scale_name = self.request.GET.get('scale', None) - scale_args = self.request.GET.get('scale_args', None) cls = StatScale.by_name(scale_name) if cls is None: From 1bb83ccdd70338455da0eecb269cfcda14275786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 15:10:53 +0200 Subject: [PATCH 31/60] simplify StatScale --- kfet/statistic.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 2abb4fd8..1db30d73 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -79,12 +79,6 @@ class StatScale(object): label_fmt = self.label_fmt return [begin.strftime(label_fmt) for begin, end in self] - @classmethod - def get_chunk_start(cls, dt): - dt_kfet = to_kfet_day(dt) - start = dt_kfet - cls.offset_to_chunk_start(dt_kfet) - return start - class DayStatScale(StatScale): name = 'day' @@ -102,8 +96,10 @@ class WeekStatScale(StatScale): label_fmt = 'Semaine %W' @classmethod - def offset_to_chunk_start(cls, dt): - return timezone.timedelta(days=dt.weekday()) + def get_chunk_start(cls, dt): + dt_kfet = to_kfet_day(dt) + offset = timezone.timedelta(days=dt_kfet.weekday()) + return dt_kfet - offset class MonthStatScale(StatScale): From 769c37634d1f9bfd320bd3e26cef4326be1d9d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 15:43:56 +0200 Subject: [PATCH 32/60] delete debug msg --- kfet/templates/kfet/article_read.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/kfet/templates/kfet/article_read.html b/kfet/templates/kfet/article_read.html index 1e0fb650..6fe025f6 100644 --- a/kfet/templates/kfet/article_read.html +++ b/kfet/templates/kfet/article_read.html @@ -102,12 +102,10 @@ From 87bc90ec8bd25ce0ce4a4a0415b2dd44ebdf6181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 16:07:31 +0200 Subject: [PATCH 33/60] Begin/end balance stat graph - Anytime begin at account creation datetime - Others doesn't take care of account creation - Add check to avoid to bug on actions list length --- kfet/views.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index fdcb3763..a82e5cea 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2186,7 +2186,22 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): # autre passe) # } - actions = [ + actions = [] + + actions.append({ + 'at': (begin_date or account.created_at).isoformat(), + 'amount': 0, + 'label': 'début', + 'balance': 0, + }) + actions.append({ + 'at': (end_date or timezone.now()).isoformat(), + 'amount': 0, + 'label': 'fin', + 'balance': 0, + }) + + actions += [ { 'at': ope_grp.at.isoformat(), 'amount': ope_grp.amount, @@ -2210,11 +2225,12 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): ] # Maintenant on trie la liste des actions par ordre du plus récent # an plus ancien et on met à jour la balance - actions = sorted(actions, key=lambda k: k['at'], reverse=True) - actions[0]['balance'] = account.balance - for i in range(len(actions)-1): - actions[i+1]['balance'] = \ - actions[i]['balance'] - actions[i+1]['amount'] + if len(actions) > 1: + actions = sorted(actions, key=lambda k: k['at'], reverse=True) + actions[0]['balance'] = account.balance + for i in range(len(actions)-1): + actions[i+1]['balance'] = \ + actions[i]['balance'] - actions[i+1]['amount'] return actions def get_context_data(self, *args, **kwargs): From b113a57b741bf05aa59ee554a73b1ce40f9aba45 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 3 Apr 2017 11:20:56 -0300 Subject: [PATCH 34/60] Fix update function --- kfet/templates/kfet/inventory_create.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 293bcd6b..29bf2d3e 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -110,14 +110,14 @@ $(document).ready(function() { $line.find('.current_stock').text(old_stock + stock_diff); $line.find('.stock_diff').text(''); - if ($line.find('.stock_new input').val()) { + if ($line.find('.stock_new input').val() && stock_diff) { var old_misc = +$line.find('.misc input').val(); $line.find('.misc input').val(old_misc + stock_diff) .trigger('input'); } var id = $line.find('input[type="hidden"]').val(); - conflicts = conflicts.delete(id); + conflicts.delete(parseInt(id)); } $('.finished input').change(function() { From 40da3bc2995c2df5c464c21dccb2fe74e6be3180 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 3 Apr 2017 11:21:05 -0300 Subject: [PATCH 35/60] Listen on input --- kfet/templates/kfet/inventory_create.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 29bf2d3e..61792c6d 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -164,7 +164,7 @@ $(document).ready(function() { } } - $('#inventoryform').on("submit", function(e) { + $('input[type="submit"]').on("click", function(e) { e.preventDefault(); if (conflicts.size) { From 903da795ab3f7a5399a33cf221ea70d8b19880b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 16:53:28 +0200 Subject: [PATCH 36/60] clean kfet statistic.js - no longer dictToArray where it isn't necessary (because already an array) - fix chart height: - previous charts were causing bugs - height is fixed (even with window resizing) - clean whitespaces --- kfet/static/kfet/js/statistic.js | 37 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index f6d21237..9593c9f7 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -1,10 +1,10 @@ (function($){ window.StatsGroup = function (url, target) { // a class to properly display statictics - + // url : points to an ObjectResumeStat that lists the options through JSON // target : element of the DOM where to put the stats - + var self = this; var element = $(target); var content = $("
"); @@ -22,21 +22,21 @@ return array; } - function handleTimeChart (dict) { + function handleTimeChart (data) { // reads the balance data and put it into chartjs formatting - var data = dictToArray(dict, 0); + chart_data = new Array(); for (var i = 0; i < data.length; i++) { var source = data[i]; - data[i] = { x: new Date(source.at), + chart_data[i] = { x: new Date(source.at), y: source.balance, label: source.label } } - return data; + return chart_data; } - + function showStats () { // CALLBACK : called when a button is selected - + // shows the focus on the correct button buttons.find(".focus").removeClass("focus"); $(this).addClass("focus"); @@ -50,14 +50,14 @@ var chart_datasets = []; var charts = dictToArray(data.charts); - + // are the points indexed by timestamps? var is_time_chart = data.is_time_chart || false; // reads the charts data for (var i = 0; i < charts.length; i++) { var chart = charts[i]; - + // format the data var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1); @@ -77,6 +77,7 @@ var chart_options = { responsive: true, + maintainAspectRatio: false, tooltips: { mode: 'index', intersect: false, @@ -129,25 +130,25 @@ type: 'line', options: chart_options, data: { - labels: dictToArray(data.labels, 1), + labels: (data.labels || []).slice(1), datasets: chart_datasets, } }; // saves the previous charts to be destroyed var prev_chart = content.children(); - + + // clean + prev_chart.remove(); + // creates a blank canvas element and attach it to the DOM - var canvas = $(""); + var canvas = $(""); content.append(canvas); // create the chart var chart = new Chart(canvas, chart_model); - - // clean - prev_chart.remove(); } - + // initialize the interface function initialize (data) { // creates the bar with the buttons @@ -158,7 +159,7 @@ var to_click; var context = data.stats; - + for (var i = 0; i < context.length; i++) { // creates the button var btn_wrapper = $("
", From 6d2e150aa0f54832ac61f56115158510051c0460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 16:56:44 +0200 Subject: [PATCH 37/60] clean align --- kfet/static/kfet/js/statistic.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index 9593c9f7..f210c11d 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -27,9 +27,11 @@ chart_data = new Array(); for (var i = 0; i < data.length; i++) { var source = data[i]; - chart_data[i] = { x: new Date(source.at), - y: source.balance, - label: source.label } + chart_data[i] = { + x: new Date(source.at), + y: source.balance, + label: source.label, + } } return chart_data; } From 10d2b58fa7e01fa1ddbecc969b1aa1c460f8a3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 17:06:32 +0200 Subject: [PATCH 38/60] clean some comments - fix: error if actions are empty in balance stats --- kfet/views.py | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index a82e5cea..17de388e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2049,9 +2049,10 @@ class PkUrlMixin(object): class SingleResumeStat(JSONDetailView): - """ - Summarize all the stats of an object - Handles JSONResponse + """Manifest for a kind of a stat about an object. + + Returns JSON whose payload is an array containing descriptions of a stat: + url to retrieve data, label, ... """ id_prefix = '' @@ -2095,9 +2096,8 @@ class SingleResumeStat(JSONDetailView): ID_PREFIX_ACC_BALANCE = "balance_acc" -# Un résumé de toutes les vues ArticleStatBalance -# REND DU JSON class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): + """Manifest for balance stats of an account.""" model = Account context_object_name = 'account' pk_url_kwarg = 'trigramme' @@ -2138,12 +2138,10 @@ class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): class AccountStatBalance(PkUrlMixin, JSONDetailView): - """ - Returns a JSON containing the evolution a the personnal - balance of a trigramme between timezone.now() and `nb_days` - ago (specified to the view as an argument) - takes into account the Operations and the Transfers - does not takes into account the balance offset + """Datasets of balance of an account. + + Operations and Transfers are taken into account. + """ model = Account pk_url_kwarg = 'trigramme' @@ -2253,8 +2251,9 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): "values": changes, }] context['is_time_chart'] = True - context['min_date'] = changes[-1]['at'] - context['max_date'] = changes[0]['at'] + if len(changes) > 0: + context['min_date'] = changes[-1]['at'] + context['max_date'] = changes[0]['at'] # TODO: offset return context @@ -2278,9 +2277,8 @@ ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc" ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc" -# Un résumé de toutes les vues ArticleStatLast -# NE REND PAS DE JSON class AccountStatOperationList(PkUrlMixin, SingleResumeStat): + """Manifest for operations stats of an account.""" model = Account context_object_name = 'account' pk_url_kwarg = 'trigramme' @@ -2301,10 +2299,7 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat): class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): - """ - Returns a JSON containing the evolution a the personnal - consommation of a trigramme at the diffent dates specified - """ + """Datasets of operations of an account.""" model = Account pk_url_kwarg = 'trigramme' context_object_name = 'account' @@ -2363,9 +2358,8 @@ ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art" ID_PREFIX_ART_LAST_MONTHS = "last_months_art" -# Un résumé de toutes les vues ArticleStatLast -# NE REND PAS DE JSON class ArticleStatSalesList(SingleResumeStat): + """Manifest for sales stats of an article.""" model = Article context_object_name = 'article' id_prefix = ID_PREFIX_ART_LAST @@ -2379,10 +2373,7 @@ class ArticleStatSalesList(SingleResumeStat): class ArticleStatSales(ScaleMixin, JSONDetailView): - """ - Returns a JSON containing the consommation - of an article at the diffent dates precised - """ + """Datasets of sales of an article.""" model = Article context_object_name = 'article' From 32474a6865d233df90be46f19cd783446c37f3b4 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 3 Apr 2017 16:03:22 -0300 Subject: [PATCH 39/60] Don't update input when unchecking --- kfet/templates/kfet/inventory_create.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 61792c6d..1098f1f8 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -101,7 +101,7 @@ $(document).ready(function() { * Remove warning and update stock */ - function update_stock($line) { + function update_stock($line, update_count) { $line.removeClass('inventory_modified'); $line.find('.inventory_update').hide(); @@ -110,7 +110,7 @@ $(document).ready(function() { $line.find('.current_stock').text(old_stock + stock_diff); $line.find('.stock_diff').text(''); - if ($line.find('.stock_new input').val() && stock_diff) { + if ($line.find('.stock_new input').val() && update_count) { var old_misc = +$line.find('.misc input').val(); $line.find('.misc input').val(old_misc + stock_diff) .trigger('input'); @@ -122,12 +122,12 @@ $(document).ready(function() { $('.finished input').change(function() { var $line = $(this).closest('tr'); - update_stock($line); + update_stock($line, false); }); $('.inventory_update button').click(function() { var $line = $(this).closest('tr'); - update_stock($line); + update_stock($line, true); }); From 51acb4e00aae09ed8e7ab7536c92c65ef380e3a9 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 3 Apr 2017 16:05:18 -0300 Subject: [PATCH 40/60] Use new WS class --- kfet/templates/kfet/inventory_create.html | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index 1098f1f8..d8109f8e 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -80,6 +80,8 @@ $(document).ready(function() { 'use strict'; + var conflicts = new Set(); + /** * Autofill new stock from other inputs */ @@ -135,15 +137,7 @@ $(document).ready(function() { * Websocket */ - var conflicts = new Set(); - var websocket_msg_default = {'articles':[]} - - var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws'; - var location_host = window.location.host; - var location_url = window.location.pathname.startsWith('/gestion/') ? location_host + '/gestion' : location_host; - var socket = new ReconnectingWebSocket(websocket_protocol+"://" + location_url + "/ws/k-fet/k-psul/"); - socket.onmessage = function(e) { - var data = $.extend({}, websocket_msg_default, JSON.parse(e.data)); + OperationWebSocket.add_handler(function(data) { for (let article of data['articles']) { var $line = $('input[value="'+article.id+'"]').parent(); if ($line.find('.finished input').is(":checked")) { @@ -162,7 +156,7 @@ $(document).ready(function() { $line.find('.current_stock').text(article.stock); } } - } + }); $('input[type="submit"]').on("click", function(e) { e.preventDefault(); From f13d1072c70fc7ff9bbb9409911fad19445ab7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 3 Apr 2017 22:55:54 +0100 Subject: [PATCH 41/60] Add simple tests for the stat views We check that we can get all the stats views with the appropriate permissions. --- cof/urls.py | 2 +- kfet/tests.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/cof/urls.py b/cof/urls.py index 7ec728da..06b1087a 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -84,7 +84,7 @@ urlpatterns = [ url(r'^k-fet/', include('kfet.urls')), ] -if settings.DEBUG: +if 'debug_toolbar' in settings.INSTALLED_APPS: import debug_toolbar urlpatterns += [ url(r'^__debug__/', include(debug_toolbar.urls)), diff --git a/kfet/tests.py b/kfet/tests.py index 5bea7afa..ffca7a44 100644 --- a/kfet/tests.py +++ b/kfet/tests.py @@ -1,5 +1,70 @@ # -*- coding: utf-8 -*- -from django.test import TestCase +from unittest.mock import patch -# Écrire les tests ici +from django.test import TestCase, Client +from django.contrib.auth.models import User, Permission + +from .models import Account, Article, ArticleCategory + + +class TestStats(TestCase): + @patch('kfet.signals.messages') + def test_user_stats(self, mock_messages): + """ + Checks that we can get the stat-related pages without any problem. + """ + # We setup two users and an article. Only the first user is part of the + # team. + user = User.objects.create(username="Foobar") + user.set_password("foobar") + user.save() + Account.objects.create(trigramme="FOO", cofprofile=user.profile) + perm = Permission.objects.get(codename="is_team") + user.user_permissions.add(perm) + + user2 = User.objects.create(username="Barfoo") + user2.set_password("barfoo") + user2.save() + Account.objects.create(trigramme="BAR", cofprofile=user2.profile) + + article = Article.objects.create( + name="article", + category=ArticleCategory.objects.create(name="C") + ) + + # Each user have its own client + client = Client() + client.login(username="Foobar", password="foobar") + client2 = Client() + client2.login(username="Barfoo", password="barfoo") + + # 1. FOO should be able to get these pages but BAR receives a Forbidden + # response + user_urls = [ + "/k-fet/accounts/FOO/stat/operations/list", + "/k-fet/accounts/FOO/stat/operations?{}".format( + '&'.join(["scale=day", + "types=['purchase']", + "scale_args={'n_steps':+7,+'last':+True}", + "format=json"])), + "/k-fet/accounts/FOO/stat/balance/list", + "/k-fet/accounts/FOO/stat/balance?format=json" + ] + for url in user_urls: + resp = client.get(url) + self.assertEqual(200, resp.status_code) + resp2 = client2.get(url) + self.assertEqual(403, resp2.status_code) + + # 2. FOO is a member of the team and can get these pages but BAR + # receives a Redirect response + articles_urls = [ + "/k-fet/articles/{}/stat/sales/list".format(article.pk), + "/k-fet/articles/{}/stat/sales".format(article.pk) + ] + for url in articles_urls: + resp = client.get(url) + self.assertEqual(200, resp.status_code) + resp2 = client2.get(url) + self.assertEqual(302, resp2.status_code) From df467767f441f55b193ac9a146b97660bf55db55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 4 Apr 2017 01:29:19 +0200 Subject: [PATCH 42/60] fix default GET param 'types' on operations stats --- kfet/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index 17de388e..8f5cdb3a 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2324,7 +2324,9 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): scale = self.scale types = self.request.GET.get('types', None) - if types is not None: + if types is None: + types = [] + else: types = ast.literal_eval(types) operations = self.get_operations(types=types, scale=scale) From 7989a07b5f61d7f4acb5165811b5c4765e43934f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 4 Apr 2017 01:36:19 +0200 Subject: [PATCH 43/60] cleaner fix --- kfet/views.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 8f5cdb3a..69126634 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2305,16 +2305,17 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): context_object_name = 'account' id_prefix = "" - def get_operations(self, types, scale): + def get_operations(self, scale, types=None): # On selectionne les opérations qui correspondent # à l'article en question et qui ne sont pas annulées # puis on choisi pour chaques intervalle les opérations # effectuées dans ces intervalles de temps all_operations = (Operation.objects - .filter(type__in=types) .filter(group__on_acc=self.object) .filter(canceled_at=None) ) + if types is not None: + all_operations = all_operations.filter(type__in==types) chunks = self.chunkify_qs(all_operations, scale, field='group__at') return chunks @@ -2324,9 +2325,7 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): scale = self.scale types = self.request.GET.get('types', None) - if types is None: - types = [] - else: + if types is not None: types = ast.literal_eval(types) operations = self.get_operations(types=types, scale=scale) From 278459e80f2b478a958cdebe263936c466332649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 4 Apr 2017 11:05:49 +0200 Subject: [PATCH 44/60] typo....... --- kfet/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index 69126634..fe174588 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2315,7 +2315,7 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): .filter(canceled_at=None) ) if types is not None: - all_operations = all_operations.filter(type__in==types) + all_operations = all_operations.filter(type__in=types) chunks = self.chunkify_qs(all_operations, scale, field='group__at') return chunks From 72615bf4007cab18199435cba7278ef65c900ac6 Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 4 Apr 2017 16:57:17 +0200 Subject: [PATCH 45/60] small fixes --- kfet/templates/kfet/kpsul.html | 2 +- kfet/views.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 7b907f80..8e1f44f3 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -833,7 +833,7 @@ $(document).ready(function() { if (settings['addcost_for'] && settings['addcost_amount'] && account_data['trigramme'] != settings['addcost_for'] - && article_data['5']) + && article_data[5]) amount_euro -= settings['addcost_amount'] * nb; var reduc_divisor = 1; if (account_data['is_cof']) diff --git a/kfet/views.py b/kfet/views.py index 7a2700d1..b684b90f 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1019,8 +1019,7 @@ def kpsul_perform_operations(request): operation.is_checkout = False if operationgroup.on_acc.is_cof: if is_addcost & operation.article.category.has_addcost: - operation.addcost_amount = operation.addcost_amount \ - / cof_grant_divisor + operation.addcost_amount /= cof_grant_divisor operation.amount = operation.amount / cof_grant_divisor to_articles_stocks[operation.article] -= operation.article_nb else: From 885e40fd05da9e9d9cd44645a3e7c4c332196841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 4 Apr 2017 18:11:15 +0200 Subject: [PATCH 46/60] cleaner scales - References to `Stat` in `Scale` objects are deleted (because scales are independent of stats) - KFET_WAKES_UP_AT is now a time object insted of an hour - Proper use of date, datetime, timedelta, etc (django.utils.timezone provides neither datetime nor timedelta) --- kfet/statistic.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 1db30d73..4ae17959 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,27 +1,29 @@ # -*- coding: utf-8 -*- import ast +from datetime import date, datetime, time, timedelta from dateutil.relativedelta import relativedelta from django.utils import timezone from django.db.models import Sum -KFET_WAKES_UP_AT = 7 +KFET_WAKES_UP_AT = time(7, 0) def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): - return timezone.datetime(year, month, day, hour=start_at) + """datetime wrapper with time offset.""" + return datetime.combine(date(year, month, day), start_at) def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day) - if dt.hour < start_at: - kfet_dt -= timezone.timedelta(days=1) + if dt.time() < start_at: + kfet_dt -= timedelta(days=1) return kfet_dt -class StatScale(object): +class Scale(object): name = None step = None @@ -49,7 +51,7 @@ class StatScale(object): @staticmethod def by_name(name): - for cls in StatScale.__subclasses__(): + for cls in Scale.__subclasses__(): if cls.name == name: return cls return None @@ -80,9 +82,9 @@ class StatScale(object): return [begin.strftime(label_fmt) for begin, end in self] -class DayStatScale(StatScale): +class DayScale(Scale): name = 'day' - step = timezone.timedelta(days=1) + step = timedelta(days=1) label_fmt = '%A' @classmethod @@ -90,19 +92,19 @@ class DayStatScale(StatScale): return to_kfet_day(dt) -class WeekStatScale(StatScale): +class WeekScale(Scale): name = 'week' - step = timezone.timedelta(days=7) + step = timedelta(days=7) label_fmt = 'Semaine %W' @classmethod def get_chunk_start(cls, dt): dt_kfet = to_kfet_day(dt) - offset = timezone.timedelta(days=dt_kfet.weekday()) + offset = timedelta(days=dt_kfet.weekday()) return dt_kfet - offset -class MonthStatScale(StatScale): +class MonthScale(Scale): name = 'month' step = relativedelta(months=1) label_fmt = '%B' @@ -132,9 +134,9 @@ def stat_manifest(scales_def=None, scale_args=None, **url_params): def last_stats_manifest(scales_def=None, scale_args=None, **url_params): scales_def = [ - ('Derniers mois', MonthStatScale, ), - ('Dernières semaines', WeekStatScale, ), - ('Derniers jours', DayStatScale, ), + ('Derniers mois', MonthScale, ), + ('Dernières semaines', WeekScale, ), + ('Derniers jours', DayScale, ), ] if scale_args is None: scale_args = {} @@ -160,7 +162,7 @@ class ScaleMixin(object): scale_name = self.request.GET.get('scale', None) - cls = StatScale.by_name(scale_name) + cls = Scale.by_name(scale_name) if cls is None: scale = self.get_default_scale() else: @@ -174,7 +176,7 @@ class ScaleMixin(object): return context def get_default_scale(self): - return DayStatScale(n_steps=7, last=True) + return DayScale(n_steps=7, last=True) def chunkify_qs(self, qs, scale, field=None): if field is None: From dc07b072aba5808f9da497558d7c8a4224afd4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 4 Apr 2017 20:12:21 +0200 Subject: [PATCH 47/60] Flatten scale args in GET params of stats urls - get_scale_args method of ScaleMixin retrieves useful GET params for Scale object instanciation (by default from request.GET) - it takes into account the type of the scale arg - prefix used for GET param can be modified in stats_manifest funcs and ScaleMixin --- kfet/statistic.py | 87 +++++++++++++++++++++++++++++++++++------------ requirements.txt | 1 + 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 4ae17959..fe948f73 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -import ast from datetime import date, datetime, time, timedelta from dateutil.relativedelta import relativedelta +from dateutil.parser import parse as dateutil_parse from django.utils import timezone from django.db.models import Sum @@ -114,25 +114,29 @@ class MonthScale(Scale): return to_kfet_day(dt).replace(day=1) -def stat_manifest(scales_def=None, scale_args=None, **url_params): +def stat_manifest(scales_def=None, scale_args=None, scale_prefix=None, + **other_url_params): + if scale_prefix is None: + scale_prefix = 'scale_' if scales_def is None: scales_def = [] if scale_args is None: scale_args = {} - return [ - dict( + manifest = [] + for label, cls in scales_def: + url_params = {scale_prefix+'name': cls.name} + url_params.update({scale_prefix+key: value + for key, value in scale_args.items()}) + url_params.update(other_url_params) + manifest.append(dict( label=label, - url_params=dict( - scale=cls.name, - scale_args=scale_args, - **url_params, - ), - ) - for label, cls in scales_def - ] + url_params=url_params, + )) + return manifest -def last_stats_manifest(scales_def=None, scale_args=None, **url_params): +def last_stats_manifest(scales_def=None, scale_args=None, scale_prefix=None, + **url_params): scales_def = [ ('Derniers mois', MonthScale, ), ('Dernières semaines', WeekScale, ), @@ -145,7 +149,7 @@ def last_stats_manifest(scales_def=None, scale_args=None, **url_params): n_steps=7, )) return stat_manifest(scales_def=scales_def, scale_args=scale_args, - **url_params) + scale_prefix=scale_prefix, **url_params) # Étant donné un queryset d'operations @@ -156,20 +160,61 @@ def tot_ventes(queryset): class ScaleMixin(object): + scale_args_prefix = 'scale_' + + def get_scale_args(self, params=None, prefix=None): + """Retrieve scale args from params. + + Should search the same args of Scale constructor. + + Args: + params (dict, optional): Scale args are searched in this. + Default to GET params of request. + prefix (str, optional): Appended at the begin of scale args names. + Default to `self.scale_args_prefix`. + + """ + if params is None: + params = self.request.GET + if prefix is None: + prefix = self.scale_args_prefix + + scale_args = {} + + name = params.get(prefix+'name', None) + if name is not None: + scale_args['name'] = name + + n_steps = params.get(prefix+'n_steps', None) + if n_steps is not None: + scale_args['n_steps'] = int(n_steps) + + begin = params.get(prefix+'begin', None) + if begin is not None: + scale_args['begin'] = dateutil_parse(begin) + + end = params.get(prefix+'send', None) + if end is not None: + scale_args['end'] = dateutil_parse(end) + + last = params.get(prefix+'last', None) + if last is not None: + scale_args['last'] = ( + last in ['true', 'True', '1'] and True or False) + + return scale_args def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - scale_name = self.request.GET.get('scale', None) + scale_args = self.get_scale_args() + scale_name = scale_args.pop('name', None) + scale_cls = Scale.by_name(scale_name) - cls = Scale.by_name(scale_name) - if cls is None: + if scale_cls is None: scale = self.get_default_scale() else: - scale_args = self.request.GET.get('scale_args', {}) - if isinstance(scale_args, str): - scale_args = ast.literal_eval(scale_args) - scale = cls(**scale_args) + scale = scale_cls(**scale_args) self.scale = scale context['labels'] = scale.get_labels() diff --git a/requirements.txt b/requirements.txt index 06f6c46e..ce081588 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ 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 +python-dateutil From ba11aa49dbf52c5b4a2f1f6c70995d2d78483db6 Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 4 Apr 2017 21:36:02 +0200 Subject: [PATCH 48/60] categories are updatable --- kfet/forms.py | 10 +++++ kfet/templates/kfet/category.html | 53 ++++++++++++++++++++++++ kfet/templates/kfet/category_update.html | 17 ++++++++ kfet/urls.py | 8 ++++ kfet/views.py | 38 +++++++++++++++-- 5 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 kfet/templates/kfet/category.html create mode 100644 kfet/templates/kfet/category_update.html diff --git a/kfet/forms.py b/kfet/forms.py index 2b59e1b3..53791456 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -229,6 +229,16 @@ class CheckoutStatementUpdateForm(forms.ModelForm): model = CheckoutStatement exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken'] + +# ----- +# Category +# ----- + +class CategoryForm(forms.ModelForm): + class Meta: + model = ArticleCategory + fields = ['name', 'has_addcost'] + # ----- # Article forms # ----- diff --git a/kfet/templates/kfet/category.html b/kfet/templates/kfet/category.html new file mode 100644 index 00000000..4de44207 --- /dev/null +++ b/kfet/templates/kfet/category.html @@ -0,0 +1,53 @@ +{% extends 'kfet/base.html' %} + +{% block title %}Categories d'articles{% endblock %} +{% block content-header-title %}Categories d'articles{% endblock %} + +{% block content %} + +
+
+
+
+
{{ categories|length }}
+
catégorie{{ categories|length|pluralize }}
+
+
+
+
+ {% include 'kfet/base_messages.html' %} +
+
+

Liste des catégories

+
+ + + + + + + + + + + {% for category in categories %} + + + + + + + {% endfor %} + +
NomNombre d'articlesPeut-être majoré
+ + + + {{ category.name }}{{ category.articles.all|length }}{{ category.has_addcost | yesno:"Oui,Non"}}
+
+
+
+
+
+ +{% endblock %} diff --git a/kfet/templates/kfet/category_update.html b/kfet/templates/kfet/category_update.html new file mode 100644 index 00000000..fb04e12b --- /dev/null +++ b/kfet/templates/kfet/category_update.html @@ -0,0 +1,17 @@ +{% extends 'kfet/base.html' %} + +{% block title %}Édition de la catégorie {{ category.name }}{% endblock %} +{% block content-header-title %}Catégorie {{ category.name }} - Édition{% endblock %} + +{% block content %} + +
+ {% csrf_token %} + {{ form.as_p }} + {% if not perms.kfet.change_articlecategory %} + + {% endif %} + +
+ +{% endblock %} diff --git a/kfet/urls.py b/kfet/urls.py index 0a5c2128..aabbfc3c 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -134,6 +134,14 @@ urlpatterns = [ # Article urls # ----- + # Category - General + url('^categories/$', + teamkfet_required(views.CategoryList.as_view()), + name='kfet.category'), + # Category - Update + url('^categories/(?P\d+)/edit$', + teamkfet_required(views.CategoryUpdate.as_view()), + name='kfet.category.update'), # Article - General url('^articles/$', teamkfet_required(views.ArticleList.as_view()), diff --git a/kfet/views.py b/kfet/views.py index b684b90f..7cb37f28 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + from django.shortcuts import render, get_object_or_404, redirect from django.core.exceptions import PermissionDenied @@ -27,7 +27,7 @@ from kfet.models import ( Account, Checkout, Article, Settings, AccountNegative, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, InventoryArticle, Order, OrderArticle, Operation, OperationGroup, - TransferGroup, Transfer) + TransferGroup, Transfer, ArticleCategory) from kfet.forms import ( AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm, UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm, @@ -37,7 +37,7 @@ from kfet.forms import ( KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm, KPsulOperationFormSet, AddcostForm, FilterHistoryForm, SettingsForm, TransferFormSet, InventoryArticleForm, OrderArticleForm, - OrderArticleToInventoryForm + OrderArticleToInventoryForm, CategoryForm ) from collections import defaultdict from kfet import consumers @@ -720,6 +720,38 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView): form.instance.amount_taken = getAmountTaken(form.instance) return super(CheckoutStatementUpdate, self).form_valid(form) +# ----- +# Category views +# ----- + + +# Category - General +class CategoryList(ListView): + queryset = (ArticleCategory.objects + .prefetch_related('articles') + .order_by('name')) + template_name = 'kfet/category.html' + context_object_name = 'categories' + + +# Category - Update +class CategoryUpdate(SuccessMessageMixin, UpdateView): + model = ArticleCategory + template_name = 'kfet/category_update.html' + form_class = CategoryForm + success_url = reverse_lazy('kfet.category') + success_message = "Informations mises à jour pour la catégorie : %(name)s" + + # Surcharge de la validation + def form_valid(self, form): + # Checking permission + if not self.request.user.has_perm('kfet.change_articlecategory'): + form.add_error(None, 'Permission refusée') + return self.form_invalid(form) + + # Updating + return super(CategoryUpdate, self).form_valid(form) + # ----- # Article views # ----- From 7350006990cd663c9ef370c11efcc72db6e0446f Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 4 Apr 2017 21:48:17 +0200 Subject: [PATCH 49/60] PEP8 --- kfet/urls.py | 26 ++++++++++----------- kfet/views.py | 62 +++++++++++++++++++++++++-------------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/kfet/urls.py b/kfet/urls.py index aabbfc3c..fd9bfe54 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -8,7 +8,7 @@ from kfet.decorators import teamkfet_required urlpatterns = [ url(r'^$', views.Home.as_view(), - name = 'kfet.home'), + name='kfet.home'), url(r'^login/genericteam$', views.login_genericteam, name='kfet.login.genericteam'), url(r'^history$', views.history, @@ -71,26 +71,26 @@ urlpatterns = [ # Account - Statistics url('^accounts/(?P.{3})/stat/last/$', views.AccountStatLastAll.as_view(), - name = 'kfet.account.stat.last'), + name='kfet.account.stat.last'), url('^accounts/(?P.{3})/stat/last/month/$', views.AccountStatLastMonth.as_view(), - name = 'kfet.account.stat.last.month'), + name='kfet.account.stat.last.month'), url('^accounts/(?P.{3})/stat/last/week/$', views.AccountStatLastWeek.as_view(), - name = 'kfet.account.stat.last.week'), + name='kfet.account.stat.last.week'), url('^accounts/(?P.{3})/stat/last/day/$', views.AccountStatLastDay.as_view(), - name = 'kfet.account.stat.last.day'), + name='kfet.account.stat.last.day'), url('^accounts/(?P.{3})/stat/balance/$', views.AccountStatBalanceAll.as_view(), - name = 'kfet.account.stat.balance'), + name='kfet.account.stat.balance'), url('^accounts/(?P.{3})/stat/balance/d/(?P\d*)/$', views.AccountStatBalance.as_view(), - name = 'kfet.account.stat.balance.days'), + name='kfet.account.stat.balance.days'), url('^accounts/(?P.{3})/stat/balance/anytime/$', views.AccountStatBalance.as_view(), - name = 'kfet.account.stat.balance.anytime'), + name='kfet.account.stat.balance.anytime'), # ----- # Checkout urls @@ -157,20 +157,20 @@ urlpatterns = [ # Article - Update url('^articles/(?P\d+)/edit$', teamkfet_required(views.ArticleUpdate.as_view()), - name = 'kfet.article.update'), + name='kfet.article.update'), # Article - Statistics url('^articles/(?P\d+)/stat/last/$', views.ArticleStatLastAll.as_view(), - name = 'kfet.article.stat.last'), + name='kfet.article.stat.last'), url('^articles/(?P\d+)/stat/last/month/$', views.ArticleStatLastMonth.as_view(), - name = 'kfet.article.stat.last.month'), + name='kfet.article.stat.last.month'), url('^articles/(?P\d+)/stat/last/week/$', views.ArticleStatLastWeek.as_view(), - name = 'kfet.article.stat.last.week'), + name='kfet.article.stat.last.week'), url('^articles/(?P\d+)/stat/last/day/$', views.ArticleStatLastDay.as_view(), - name = 'kfet.article.stat.last.day'), + name='kfet.article.stat.last.day'), # ----- # K-Psul urls diff --git a/kfet/views.py b/kfet/views.py index 7cb37f28..4f23a3ae 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -756,24 +756,24 @@ class CategoryUpdate(SuccessMessageMixin, UpdateView): # Article views # ----- -# Article - General +# Article - General class ArticleList(ListView): queryset = (Article.objects - .select_related('category') - .prefetch_related(Prefetch('inventories', - queryset = Inventory.objects.order_by('-at'), - to_attr = 'inventory')) - .order_by('category', '-is_sold', 'name')) + .select_related('category') + .prefetch_related(Prefetch('inventories', + queryset=Inventory.objects.order_by('-at'), + to_attr='inventory')) + .order_by('category', '-is_sold', 'name')) template_name = 'kfet/article.html' context_object_name = 'articles' -# Article - Create +# Article - Create class ArticleCreate(SuccessMessageMixin, CreateView): - model = Article - template_name = 'kfet/article_create.html' - form_class = ArticleForm + model = Article + template_name = 'kfet/article_create.html' + form_class = ArticleForm success_message = 'Nouvel item : %(category)s - %(name)s' # Surcharge de la validation @@ -788,7 +788,7 @@ class ArticleCreate(SuccessMessageMixin, CreateView): # Save des suppliers déjà existant for supplier in form.cleaned_data['suppliers']: SupplierArticle.objects.create( - article = article, supplier = supplier) + article=article, supplier=supplier) # Nouveau supplier supplier_new = form.cleaned_data['supplier_new'].strip() @@ -797,49 +797,49 @@ class ArticleCreate(SuccessMessageMixin, CreateView): name=supplier_new) if created: SupplierArticle.objects.create( - article = article, supplier = supplier) + article=article, supplier=supplier) # Inventaire avec stock initial inventory = Inventory() inventory.by = self.request.user.profile.account_kfet inventory.save() InventoryArticle.objects.create( - inventory = inventory, - article = article, - stock_old = article.stock, - stock_new = article.stock, + inventory=inventory, + article=article, + stock_old=article.stock, + stock_new=article.stock, ) # Creating return super(ArticleCreate, self).form_valid(form) -# Article - Read +# Article - Read class ArticleRead(DetailView): - model = Article + model = Article template_name = 'kfet/article_read.html' context_object_name = 'article' def get_context_data(self, **kwargs): context = super(ArticleRead, self).get_context_data(**kwargs) inventoryarts = (InventoryArticle.objects - .filter(article = self.object) - .select_related('inventory') - .order_by('-inventory__at')) + .filter(article=self.object) + .select_related('inventory') + .order_by('-inventory__at')) context['inventoryarts'] = inventoryarts supplierarts = (SupplierArticle.objects - .filter(article = self.object) - .select_related('supplier') - .order_by('-at')) + .filter(article=self.object) + .select_related('supplier') + .order_by('-at')) context['supplierarts'] = supplierarts return context -# Article - Update +# Article - Update class ArticleUpdate(SuccessMessageMixin, UpdateView): - model = Article - template_name = 'kfet/article_update.html' - form_class = ArticleRestrictForm + model = Article + template_name = 'kfet/article_update.html' + form_class = ArticleRestrictForm success_message = "Informations mises à jour pour l'article : %(name)s" # Surcharge de la validation @@ -855,13 +855,13 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView): for supplier in form.cleaned_data['suppliers']: if supplier not in article.suppliers.all(): SupplierArticle.objects.create( - article = article, supplier = supplier) + article=article, supplier=supplier) # On vire les suppliers désélectionnés for supplier in article.suppliers.all(): if supplier not in form.cleaned_data['suppliers']: SupplierArticle.objects.filter( - article = article, supplier = supplier).delete() + article=article, supplier=supplier).delete() # Nouveau supplier supplier_new = form.cleaned_data['supplier_new'].strip() @@ -870,7 +870,7 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView): name=supplier_new) if created: SupplierArticle.objects.create( - article = article, supplier = supplier) + article=article, supplier=supplier) # Updating return super(ArticleUpdate, self).form_valid(form) From 85ba44c23172e086de68787b2c3d00007c016705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 4 Apr 2017 16:32:46 +0100 Subject: [PATCH 50/60] Tests the redirection using the appropriate method --- kfet/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/tests.py b/kfet/tests.py index ffca7a44..991b2545 100644 --- a/kfet/tests.py +++ b/kfet/tests.py @@ -66,5 +66,5 @@ class TestStats(TestCase): for url in articles_urls: resp = client.get(url) self.assertEqual(200, resp.status_code) - resp2 = client2.get(url) - self.assertEqual(302, resp2.status_code) + resp2 = client2.get(url, follow=True) + self.assertRedirects(resp2, "/") From 3ee9de93d9a458d0bab750774c46ec9b1878a2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 5 Apr 2017 15:34:28 +0200 Subject: [PATCH 51/60] few improvements on article category - add verbose names to ArticleCategory fields - add button to view categories list from articles list - fix article_update template in form validation - improve interface for articlecategory_update - revert vanished urls (happened in merge with master...) --- kfet/migrations/0052_category_addcost.py | 7 ++++++- kfet/models.py | 8 ++++++-- kfet/templates/kfet/article.html | 3 +++ kfet/templates/kfet/article_update.html | 2 +- kfet/templates/kfet/category.html | 2 +- kfet/templates/kfet/category_update.html | 24 ++++++++++++++++-------- kfet/urls.py | 6 ++++++ 7 files changed, 39 insertions(+), 13 deletions(-) diff --git a/kfet/migrations/0052_category_addcost.py b/kfet/migrations/0052_category_addcost.py index 62dab063..83346a1a 100644 --- a/kfet/migrations/0052_category_addcost.py +++ b/kfet/migrations/0052_category_addcost.py @@ -14,6 +14,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='articlecategory', name='has_addcost', - field=models.BooleanField(default=True), + field=models.BooleanField(default=True, help_text="Si oui et qu'une majoration est active, celle-ci sera appliquée aux articles de cette catégorie.", verbose_name='majorée'), + ), + migrations.AlterField( + model_name='articlecategory', + name='name', + field=models.CharField(max_length=45, verbose_name='nom'), ), ] diff --git a/kfet/models.py b/kfet/models.py index bee14156..cb8c324b 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -341,8 +341,12 @@ class CheckoutStatement(models.Model): @python_2_unicode_compatible class ArticleCategory(models.Model): - name = models.CharField(max_length=45) - has_addcost = models.BooleanField(default=True) + name = models.CharField("nom", max_length=45) + has_addcost = models.BooleanField("majorée", default=True, + help_text="Si oui et qu'une majoration " + "est active, celle-ci sera " + "appliquée aux articles de " + "cette catégorie.") def __str__(self): return self.name diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html index 17c831df..123f4cfa 100644 --- a/kfet/templates/kfet/article.html +++ b/kfet/templates/kfet/article.html @@ -16,6 +16,9 @@ Nouvel article + + Catégories +
diff --git a/kfet/templates/kfet/article_update.html b/kfet/templates/kfet/article_update.html index 85a29f6b..a3bfbcc6 100644 --- a/kfet/templates/kfet/article_update.html +++ b/kfet/templates/kfet/article_update.html @@ -12,7 +12,7 @@
-
+ {% csrf_token %} {% include 'kfet/form_snippet.html' with form=form %} {% if not perms.kfet.change_article %} diff --git a/kfet/templates/kfet/category.html b/kfet/templates/kfet/category.html index 4de44207..5393bf59 100644 --- a/kfet/templates/kfet/category.html +++ b/kfet/templates/kfet/category.html @@ -26,7 +26,7 @@ Nom Nombre d'articles - Peut-être majoré + Peut être majorée diff --git a/kfet/templates/kfet/category_update.html b/kfet/templates/kfet/category_update.html index fb04e12b..1a26d001 100644 --- a/kfet/templates/kfet/category_update.html +++ b/kfet/templates/kfet/category_update.html @@ -5,13 +5,21 @@ {% block content %} - - {% csrf_token %} - {{ form.as_p }} - {% if not perms.kfet.change_articlecategory %} - - {% endif %} - -
+{% include "kfet/base_messages.html" %} + +
+
+
+
+ {% csrf_token %} + {% include 'kfet/form_snippet.html' with form=form %} + {% if not perms.kfet.edit_articlecategory %} + {% include 'kfet/form_authentication_snippet.html' %} + {% endif %} + {% include 'kfet/form_submit_snippet.html' with value="Enregistrer"%} + +
+
+
{% endblock %} diff --git a/kfet/urls.py b/kfet/urls.py index a4393f8d..bc1f3370 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -69,6 +69,12 @@ urlpatterns = [ name='kfet.account.negative'), # Account - Statistics + url(r'^accounts/(?P.{3})/stat/operations/list$', + views.AccountStatOperationList.as_view(), + name='kfet.account.stat.operation.list'), + url(r'^accounts/(?P.{3})/stat/operations$', + views.AccountStatOperation.as_view(), + name='kfet.account.stat.operation'), url(r'^accounts/(?P.{3})/stat/balance/list$', views.AccountStatBalanceList.as_view(), From 508e7ec23fcd8bf94fd0d0dbc633e60dd0a66e3c Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 5 Apr 2017 12:00:39 -0300 Subject: [PATCH 52/60] Change traverse and find behavior --- kfet/static/kfet/js/kfet.api.js | 52 +++++++++++++++++++++++++-------- kfet/static/kfet/js/kpsul.js | 51 +++++++++++++++++--------------- 2 files changed, 68 insertions(+), 35 deletions(-) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 9d805fdc..730fab12 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -596,7 +596,7 @@ class ModelForest { get_or_create(data, direction) { var model = this.constructor.models[data.modelname]; - var existing = this.find(data.modelname, data.content.id); + var existing = this.find_node(data.modelname, data.content.id); if (existing) { return existing; } @@ -704,12 +704,40 @@ class ModelForest { return $container; } - traverse(callback) { + /** + * Find if node already exists in given tree + * @param {Models.TreeNode} + */ + find_node(modelname, id) { + var result = null; + function recurse(node) { - callback(node) ; + if (node.modelname === modelname && node.content.id === id) + result = node; for (let child of node.children) - callback(child); + recurse(child); + } + + for (let root of this.roots) + recurse(root); + + return result; + } + + /** + * Performs for each node (in a DFS order) the callback function + * on node.content and node.parent.content, if node has given modelname. + * @param {string} modelname + * @param {function} callback + */ + traverse(modelname, callback) { + function recurse(node) { + if (node.modelname === modelname) + callback(node.content, node.parent && node.parent.content || null) ; + + for (let child of node.children) + recurse(child); } for (let root of this.roots) @@ -718,20 +746,20 @@ class ModelForest { /** * Find instance in tree with given type and id - * @param {string} type + * @param {string} modelname * @param {number} id */ - find(type, id) { + find(modelname, id) { var result = null; - function callback(node) { - if (node.modelname === type && node.content.id == id) - result = node ; + + function callback(content) { + if (content.id == id) + result = content ; } - this.traverse(callback); - - return result ; + this.traverse(modelname, callback); + return result; } } diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js index e6784f7e..af2e4f5f 100644 --- a/kfet/static/kfet/js/kpsul.js +++ b/kfet/static/kfet/js/kpsul.js @@ -456,7 +456,7 @@ class ArticleManager { } get_article(id) { - return this.list.find('article', id).content; + return this.list.find('article', id); } update_data(data) { @@ -498,7 +498,7 @@ class ArticleManager { var id = $(this).parent().attr('id').split('-')[1]; var article = that.list.find('article', id); if (article) - that.validate(article.content); + that.validate(article); }); this._$nb.on('keydown', function(e) { @@ -575,25 +575,23 @@ class ArticleAutocomplete { var lower = prefix.toLowerCase() ; var that = this ; - article_list.traverse(function(node) { - if (node.modelname === 'article' && - node.content.name.toLowerCase() - .startsWith(lower)) { - that.matching['article'].push(node.content); - if (that.matching['category'].indexOf(node.parent.content) == -1) - that.matching['category'].push(node.parent.content); + article_list.traverse('article', function(article, category) { + if (article.name.toLowerCase().startsWith(lower)) { + that.matching.push(article); + if (that.active_categories.indexOf(category) == -1) + that.active_categories.push(category); } }); - if (this.matching['article'].length == 1) { + if (this.matching.length == 1) { if (!backspace) { - this.manager.validate(this.matching['article'][0]) ; + this.manager.validate(this.matching[0]) ; this.showAll() ; } else { this.manager.unset(); this.updateDisplay(); } - } else if (this.matching['article'].length > 1) { + } else if (this.matching.length > 1) { this.manager.unset(); this.updateDisplay() ; if (!backspace) @@ -604,19 +602,23 @@ class ArticleAutocomplete { updateDisplay() { var that = this; - this.manager.list.traverse(function(node) { - if (that.matching[node.modelname].indexOf(node.content) != -1) { - that._$container.find('#'+node.modelname+'-'+node.content.id) - .show(); + this.manager.list.traverse('article', function(article, category) { + if (that.matching.indexOf(article) != -1) { + that._$container.find('#article-'+article.id).show(); } else { - that._$container.find('#'+node.modelname+'-'+node.content.id) - .hide(); + that._$container.find('#article-'+article.id).hide(); + } + + if (that.active_categories.indexOf(category) != -1) { + that._$container.find('#category-'+category.id).show(); + } else { + that._$container.find('#category-'+category.id).hide(); } }); } updatePrefix() { - var lower = this.matching['article'].map(function (article) { + var lower = this.matching.map(function (article) { return article.name.toLowerCase() ; }); @@ -631,14 +633,17 @@ class ArticleAutocomplete { showAll() { var that = this; this.resetMatch(); - this.manager.list.traverse(function(node) { - that.matching[node.modelname].push(node.content); + this.manager.list.traverse('article', function(article) { + that.matching.push(article); + }); + this.manager.list.traverse('category', function(category) { + that.active_categories.push(category); }); this.updateDisplay(); } resetMatch() { - this.matching = {'article' : [], - 'category': []}; + this.matching = []; + this.active_categories = []; } } From 5c422e892a65432a9dc1ab6da491f514992b8cd4 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 5 Apr 2017 12:31:19 -0300 Subject: [PATCH 53/60] Add children fo traverse callback --- kfet/static/kfet/js/kfet.api.js | 7 +++++-- kfet/static/kfet/js/kpsul.js | 27 ++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 730fab12..4f73e20b 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -733,8 +733,11 @@ class ModelForest { */ traverse(modelname, callback) { function recurse(node) { - if (node.modelname === modelname) - callback(node.content, node.parent && node.parent.content || null) ; + if (node.modelname === modelname) { + var parent = node.parent && node.parent.content || null; + var children = node.children.map( (child) => child.content); + callback(node.content, children, parent); + } for (let child of node.children) recurse(child); diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js index af2e4f5f..e1bb2b55 100644 --- a/kfet/static/kfet/js/kpsul.js +++ b/kfet/static/kfet/js/kpsul.js @@ -575,12 +575,9 @@ class ArticleAutocomplete { var lower = prefix.toLowerCase() ; var that = this ; - article_list.traverse('article', function(article, category) { - if (article.name.toLowerCase().startsWith(lower)) { + article_list.traverse('article', function(article) { + if (article.name.toLowerCase().startsWith(lower)) that.matching.push(article); - if (that.active_categories.indexOf(category) == -1) - that.active_categories.push(category); - } }); if (this.matching.length == 1) { @@ -602,14 +599,18 @@ class ArticleAutocomplete { updateDisplay() { var that = this; - this.manager.list.traverse('article', function(article, category) { - if (that.matching.indexOf(article) != -1) { - that._$container.find('#article-'+article.id).show(); - } else { - that._$container.find('#article-'+article.id).hide(); + this.manager.list.traverse('category', function(category, articles) { + var is_active = false; + for (let article of articles) { + if (that.matching.indexOf(article) != -1) { + is_active = true; + that._$container.find('#article-'+article.id).show(); + } else { + that._$container.find('#article-'+article.id).hide(); + } } - if (that.active_categories.indexOf(category) != -1) { + if (is_active) { that._$container.find('#category-'+category.id).show(); } else { that._$container.find('#category-'+category.id).hide(); @@ -636,14 +637,10 @@ class ArticleAutocomplete { this.manager.list.traverse('article', function(article) { that.matching.push(article); }); - this.manager.list.traverse('category', function(category) { - that.active_categories.push(category); - }); this.updateDisplay(); } resetMatch() { this.matching = []; - this.active_categories = []; } } From e4dd434608582abec7db43f968866b54d0c9f96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 5 Apr 2017 23:33:45 +0200 Subject: [PATCH 54/60] no longer use model_to_dict - fix cof status on k-psul --- kfet/views.py | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 5643e1f2..310d06cf 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -17,7 +17,6 @@ from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User, Permission, Group from django.http import JsonResponse, Http404 from django.forms import formset_factory -from django.forms.models import model_to_dict from django.db import transaction from django.db.models import F, Sum, Prefetch, Count from django.db.models.functions import Coalesce @@ -390,13 +389,11 @@ def account_read(request, trigramme): raise PermissionDenied if request.GET.get('format') == 'json': - data = model_to_dict( - account, - fields=['id', 'trigramme', 'firstname', 'lastname', 'email', - 'is_cof', 'promo', 'balance', 'is_frozen', 'departement', - 'nickname', 'trigramme'] - ) - data['name'] = account.name + export_keys = ['id', 'trigramme', 'first_name', 'last_name', 'name', + 'email', 'is_cof', 'promo', 'balance', 'is_frozen', + 'departement', 'nickname'] + print(account.first_name) + data = {k: getattr(account, k) for k in export_keys} return JsonResponse(data) addcosts = ( @@ -663,20 +660,14 @@ class CheckoutRead(JSONResponseMixin, DetailView): def render_to_response(self, context, **kwargs): if self.request.GET.get('format') == 'json': - data = model_to_dict( - context['checkout'], - fields=['id', 'name', 'balance', 'valid_from', 'valid_to'] - ) + export_keys = ['id', 'name', 'balance', 'valid_from', 'valid_to'] + data = {k: getattr(self.object, k) for k in export_keys} if 'laststatement' in context: - last_statement = context['laststatement'] - last_statement_data = model_to_dict( - last_statement, - fields=['id', 'at', 'balance_new', 'balance_old', 'by'] - ) - last_statement_data['by'] = str(last_statement.by) - # ``at`` is not editable, so skipped by ``model_to_dict`` - last_statement_data['at'] = last_statement.at - data['laststatement'] = last_statement_data + last_st = context['laststatement'] + export_keys = ['id', 'at', 'balance_new', 'balance_old'] + last_st_data = {k: getattr(last_st, k) for k in export_keys} + last_st_data['by'] = str(last_st.by) + data['laststatement'] = last_st_data return self.render_to_json_response(data) else: return super().render_to_response(context, **kwargs) From df47bedae1c97dc9288c113bd6af0891ee54c367 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 5 Apr 2017 22:10:21 -0300 Subject: [PATCH 55/60] Change ModelForest inner structure --- kfet/static/kfet/js/kfet.api.js | 180 ++++++++++++++------------------ kfet/static/kfet/js/kpsul.js | 4 +- kfet/views.py | 1 - 3 files changed, 80 insertions(+), 105 deletions(-) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 8fccf247..32d8d9f6 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -450,22 +450,28 @@ class ArticleCategory extends ModelObject { /** * Properties associated to a category - * @default ['id', 'name'] + * @default ['id', 'name', 'has_addcost', 'article'] * @see {@link Models.ModelObject.props|ModelObject.props} */ static get props() { - return ['id', 'name', 'has_addcost']; + return ['id', 'name', 'has_addcost', 'articles']; } /** * Default values for ArticleCategory model instances. - * @default { 'id': 0, 'name': '' } + * @default { 'id': 0, 'name': '', 'has_addcost': true, 'articles': [] } * @see {@link Models.ModelObject.default_data|ModelObject.default_data} */ static get default_data() { - return {'id': 0, 'name': '', 'has_addcost': true}; + return {'id': 0, 'name': '', 'has_addcost': true, 'articles': []}; } + /** + * Verbose name for ArticleCategory model + * @default 'category' + */ + static get verbose_name() { return 'category'; } + /** * @default {@link Formatters.ArticleCategoryFormatter} */ @@ -490,7 +496,7 @@ class ArticleCategory extends ModelObject { class Article extends ModelObject { /** * Properties associated to an article - * @default ['id', 'name'] + * @default ['id', 'name', 'price', 'stock', 'category'] * @see {@link Models.ModelObject.props|ModelObject.props} */ static get props() { @@ -510,6 +516,12 @@ class Article extends ModelObject { }; } + /** + * Verbose name for Article model + * @default 'article' + */ + static get verbose_name() { return 'article'; } + /** * @default {@link Formatters.ArticleFormatter} */ @@ -535,22 +547,6 @@ class Article extends ModelObject { } } - -/** - * Node for ModelForest object - * @memberof Models - */ -class TreeNode { - - constructor(type, content) { - this.modelname = type; - this.content = content; - this.parent = null; - this.children = []; - } -} - - /** * Simple {@link Models.ModelObject} forest. * @memberof Models @@ -558,24 +554,11 @@ class TreeNode { class ModelForest { /** - * Dictionary associating types to classes + * Abstract structure of the forest * @abstract * @type {Object} */ - static get models() { return {}; } - - /** - * Comparison function for nodes - * @abstract - * @param {class} model Model to use for comparison - * @param {Models.TreeNode} a - * @param {Models.TreeNode} b - * @see {@link Models.ModelObject.compare|ModelObject.compare} - */ - static compare(model, a, b) { - return model.compare(a.content, b.content); - } - + static get structure() { return {}; } /** * Creates empty instance and populates it with data if given @@ -594,34 +577,38 @@ class ModelForest { * @param {number} direction */ get_or_create(data, direction) { - var model = this.constructor.models[data.modelname]; + var struct_data = this.constructor.structure[data.modelname]; + var model = struct_data.model; - var existing = this.find_node(data.modelname, data.content.id); + var existing = this.find(data.modelname, data.content.id); if (existing) { return existing; } - var content = new this.constructor.models[data.modelname](data.content); - var node = new TreeNode(data.modelname, content); - - if (data.child_sort) - node.child_sort = data.child_sort + var node = new model(data.content); if (direction <= 0) { - if (data.parent) { - var parent = this.get_or_create(data.parent, -1); - node.parent = parent; - parent.children.push(node); + var parent_name = struct_data.parent; + var parent_data = data.parent; + var parent_struct = this.constructor.structure[parent_name]; + if (parent_data) { + var parent_node = this.get_or_create(parent_data, -1); + node[parent_name] = parent_node; + parent_node[parent_struct.children].push(node); } else { this.roots.push(node); } } - if (direction >= 0 && data.children) { - for (let child_data of data.children) { - var child = this.get_or_create(child_data, 1); - child.parent = node; - node.children.push(child); + if (direction >= 0) { + var child_name = struct_data.children; + var child_struct = this.constructor.structure[child_name]; + if (data.children && data.children.length) { + for (let child_data of data.children) { + var child = this.get_or_create(child_data, 1); + child[child_struct.parent] = node; + node[child_name].push(child); + } } } @@ -654,26 +641,33 @@ class ModelForest { * @param {Object} [options] Options for element render method */ render_element(node, templates, options) { - var template = templates[node.modelname]; + var modelname = node.constructor.verbose_name; + var struct = this.constructor.structure; + + var template = templates[modelname]; var options = options || {} ; var $container = $('
'); - $container.attr('id', node.modelname+'-'+node.content.id); + $container.attr('id', modelname+'-'+node.id); - var $rendered = node.content.display($(template), options); + var $rendered = node.display($(template), options); $container.append($rendered); //dirty - node.children.sort(this.constructor.compare.bind(null, this.constructor.models[node.child_sort])); + var child_name = struct[modelname].children; - for (let child of node.children) { - var $child = this.render_element(child, templates, options); - $container.append($child); + if (child_name) { + node[child_name].sort(struct[modelname].child_sort.compare); + for (let child of node[child_name]) { + var $child = this.render_element(child, templates, options); + $container.append($child); + } } return $container; } + //TODO adapt add_to_container($container, node, templates, options) { var existing = node.parent ; var first_missing = node; @@ -696,7 +690,7 @@ class ModelForest { * @param {Object} [options] Options for element render method */ display($container, templates, options) { - this.roots.sort(this.constructor.compare.bind(null, this.root_sort)); + this.roots.sort(this.roots[0].constructor.compare); for (let root of this.roots) { $container.append(this.render_element(root, templates, options)); } @@ -704,27 +698,6 @@ class ModelForest { return $container; } - /** - * Find if node already exists in given tree - * @param {Models.TreeNode} - */ - find_node(modelname, id) { - var result = null; - - function recurse(node) { - if (node.modelname === modelname && node.content.id === id) - result = node; - - for (let child of node.children) - recurse(child); - } - - for (let root of this.roots) - recurse(root); - - return result; - } - /** * Performs for each node (in a DFS order) the callback function * on node.content and node.parent.content, if node has given modelname. @@ -732,15 +705,17 @@ class ModelForest { * @param {function} callback */ traverse(modelname, callback) { + var struct = this.constructor.structure; function recurse(node) { - if (node.modelname === modelname) { - var parent = node.parent && node.parent.content || null; - var children = node.children.map( (child) => child.content); - callback(node.content, children, parent); + if (node.constructor.verbose_name === modelname) { + callback(node); } - for (let child of node.children) - recurse(child); + var child_name = struct[node.constructor.verbose_name].children; + if (child_name) { + for (let child of node[child_name]) + recurse(child); + } } for (let root of this.roots) @@ -755,9 +730,9 @@ class ModelForest { find(modelname, id) { var result = null; - function callback(content) { - if (content.id == id) - result = content ; + function callback(node) { + if (node.id == id) + result = node ; } this.traverse(modelname, callback); @@ -810,9 +785,19 @@ class ArticleList extends APIModelForest { * @default {'article': Article, 'category': ArticleCategory} */ - static get models() { - return {'article': Article, - 'category': ArticleCategory}; + static get structure() { + return { + 'article': { + 'model': Article, + 'parent': 'category', + }, + 'category': { + 'model': ArticleCategory, + 'children': 'articles', + 'child_sort': Article, + }, + }; + } /** @@ -824,15 +809,6 @@ class ArticleList extends APIModelForest { static get url_model() { return Urls['kfet.kpsul.articles_data'](); } - - /** - * Provides model to sort root objects - * {@see Models.ModelForest.constructor|ModelForest.constructor} - */ - constructor() { - super(); - this.root_sort = ArticleCategory; - } } diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js index e1bb2b55..e4623e44 100644 --- a/kfet/static/kfet/js/kpsul.js +++ b/kfet/static/kfet/js/kpsul.js @@ -599,9 +599,9 @@ class ArticleAutocomplete { updateDisplay() { var that = this; - this.manager.list.traverse('category', function(category, articles) { + this.manager.list.traverse('category', function(category) { var is_active = false; - for (let article of articles) { + for (let article of category.articles) { if (that.matching.indexOf(article) != -1) { is_active = true; that._$container.find('#article-'+article.id).show(); diff --git a/kfet/views.py b/kfet/views.py index 310d06cf..3b3189d0 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1481,7 +1481,6 @@ def kpsul_articles_data(request): 'name': article.category.name, 'has_addcost': article.category.has_addcost, }, - 'child_sort': 'article', } }) return JsonResponse(articlelist, safe=False) From 23d19545a7295cef91669d613902be7cc87dd56f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 5 Apr 2017 22:23:56 -0300 Subject: [PATCH 56/60] Add back root_sort --- kfet/static/kfet/js/kfet.api.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 32d8d9f6..0f6cd91b 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -690,7 +690,7 @@ class ModelForest { * @param {Object} [options] Options for element render method */ display($container, templates, options) { - this.roots.sort(this.roots[0].constructor.compare); + this.roots.sort(this.constructor.root_sort); for (let root of this.roots) { $container.append(this.render_element(root, templates, options)); } @@ -782,8 +782,6 @@ class ArticleList extends APIModelForest { /** * Default structure for ArticleList instances * @abstract - * @default {'article': Article, - 'category': ArticleCategory} */ static get structure() { return { @@ -800,6 +798,14 @@ class ArticleList extends APIModelForest { } + /** + * Comparison function to sort roots + * @default {@link Models.ArticleCategory.compare|ArticleCategory.compare} + */ + static get root_sort() { + return ArticleCategory.compare; + } + /** * Default url to get ArticlList data * @abstract From 9ba13a81ee1ce6dc7c626ececc70586abf451ef2 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 6 Apr 2017 00:10:39 -0300 Subject: [PATCH 57/60] Adapt add_to_container + small improvements --- kfet/static/kfet/js/kfet.api.js | 42 +++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 0f6cd91b..889e2152 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -568,6 +568,19 @@ class ModelForest { this.from(datalist || []); } + /** + * Shortcut functions to get parent and children of a given node + */ + get_parent(node) { + var parent_name = this.constructor.structure[node.constructor.verbose_name].parent; + return node[parent_name]; + } + + get_children(node) { + var child_name = this.constructor.structure[node.constructor.verbose_name].children; + return node[child_name]; + } + /** * Fetches an object from the instance data, or creates it if * it does not exist yet.
@@ -642,7 +655,7 @@ class ModelForest { */ render_element(node, templates, options) { var modelname = node.constructor.verbose_name; - var struct = this.constructor.structure; + var struct_data = this.constructor.structure[modelname]; var template = templates[modelname]; var options = options || {} ; @@ -653,12 +666,11 @@ class ModelForest { var $rendered = node.display($(template), options); $container.append($rendered); - //dirty - var child_name = struct[modelname].children; + var children = this.get_children(node); - if (child_name) { - node[child_name].sort(struct[modelname].child_sort.compare); - for (let child of node[child_name]) { + if (children) { + children.sort(struct_data.child_sort); + for (let child of children) { var $child = this.render_element(child, templates, options); $container.append($child); } @@ -667,14 +679,14 @@ class ModelForest { return $container; } - //TODO adapt add_to_container($container, node, templates, options) { - var existing = node.parent ; + var struct = this.constructor.structure; + var existing = this.get_parent(node) ; var first_missing = node; while (existing && !($container.find('#'+existing.modelname+'-'+existing.id))) { - first_missing = existing ; - existing = existing.parent; + first_missing = existing; + existing = this.get_parent(existing); } var $to_insert = render_element(first_missing, templates, options); @@ -705,15 +717,15 @@ class ModelForest { * @param {function} callback */ traverse(modelname, callback) { - var struct = this.constructor.structure; + var that = this; function recurse(node) { if (node.constructor.verbose_name === modelname) { callback(node); } - var child_name = struct[node.constructor.verbose_name].children; - if (child_name) { - for (let child of node[child_name]) + var children = that.get_children(node); + if (children) { + for (let child of children) recurse(child); } } @@ -792,7 +804,7 @@ class ArticleList extends APIModelForest { 'category': { 'model': ArticleCategory, 'children': 'articles', - 'child_sort': Article, + 'child_sort': Article.compare, }, }; From 73fb3c419ed1d5187f9d4706da59a151749da335 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 9 Apr 2017 11:43:51 -0300 Subject: [PATCH 58/60] Add stop check in traverse --- kfet/static/kfet/js/kfet.api.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 889e2152..caee4e85 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -720,18 +720,21 @@ class ModelForest { var that = this; function recurse(node) { if (node.constructor.verbose_name === modelname) { - callback(node); + if callback(node); + return true; } var children = that.get_children(node); if (children) { for (let child of children) - recurse(child); + if (recurse(child)) + return true; } } for (let root of this.roots) - recurse(root); + if (recurse(root)) + return ; } /** @@ -743,8 +746,10 @@ class ModelForest { var result = null; function callback(node) { - if (node.id == id) + if (node.id == id) { result = node ; + return true; + } } this.traverse(modelname, callback); From 9ad208a1718515fe3e0100b7bd9a959cf2b397b9 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 9 Apr 2017 12:30:15 -0300 Subject: [PATCH 59/60] Change child sort + bugfix grom prev commit --- kfet/static/kfet/js/kfet.api.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index caee4e85..19be0607 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -669,7 +669,11 @@ class ModelForest { var children = this.get_children(node); if (children) { - children.sort(struct_data.child_sort); + if (struct_data.child_sort) + children.sort(struct_data.child_sort); + else + children.sort(children[0].constructor.compare); + for (let child of children) { var $child = this.render_element(child, templates, options); $container.append($child); @@ -702,7 +706,11 @@ class ModelForest { * @param {Object} [options] Options for element render method */ display($container, templates, options) { - this.roots.sort(this.constructor.root_sort); + if (this.constructor.root_sort) + this.roots.sort(this.constructor.root_sort); + else + this.roots.sort(this.roots[0].constructor.compare); + for (let root of this.roots) { $container.append(this.render_element(root, templates, options)); } @@ -720,8 +728,9 @@ class ModelForest { var that = this; function recurse(node) { if (node.constructor.verbose_name === modelname) { - if callback(node); + if (callback(node)) { return true; + } } var children = that.get_children(node); @@ -730,6 +739,8 @@ class ModelForest { if (recurse(child)) return true; } + + return false; } for (let root of this.roots) @@ -809,20 +820,11 @@ class ArticleList extends APIModelForest { 'category': { 'model': ArticleCategory, 'children': 'articles', - 'child_sort': Article.compare, }, }; } - /** - * Comparison function to sort roots - * @default {@link Models.ArticleCategory.compare|ArticleCategory.compare} - */ - static get root_sort() { - return ArticleCategory.compare; - } - /** * Default url to get ArticlList data * @abstract From 323f019c0da4936a16062255f595031e4adb2c52 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 9 Apr 2017 13:35:01 -0300 Subject: [PATCH 60/60] Check if children is non empty --- kfet/static/kfet/js/kfet.api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 19be0607..78289fa3 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -668,7 +668,7 @@ class ModelForest { var children = this.get_children(node); - if (children) { + if (children && children.length) { if (struct_data.child_sort) children.sort(struct_data.child_sort); else