From 6767ba8e8c925c3917272f848c6ff4ab91226907 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 15:06:55 +0100 Subject: [PATCH 01/11] Rajoute de la doc partout --- kfet/statistic.py | 39 +++++++++++++++++++++++++++++---------- kfet/views.py | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 02171267..f308011e 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -10,12 +10,16 @@ KFET_WAKES_UP_AT = time(7, 0) def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): - """datetime wrapper with time offset.""" + """Étant donné une date, renvoie un objet `datetime` + correspondant au début du 'jour K-Fêt' correspondant.""" naive = datetime.combine(date(year, month, day), start_at) return pytz.timezone("Europe/Paris").localize(naive, is_dst=None) def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): + """ + Retourne le 'jour K-Fêt' correspondant à un objet `datetime` donné + """ kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day) if dt.time() < start_at: kfet_dt -= timedelta(days=1) @@ -23,6 +27,17 @@ def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): class Scale(object): + """ + Classe utilisée pour subdiviser un QuerySet (e.g. des opérations) sur + une échelle de temps donnée, avec un pas de temps fixe. + Cette échelle peut être spécifiée : + - par un début et une fin, + - par un début/une fin et un nombre de subdivisions. + + Si le booléen `std_chunk` est activé, les subdivisions sont standardisées : + on appelle `get_chunk_start` sur toutes les subdivisions (enfin, sur la première). + """ + name = None step = None @@ -92,6 +107,10 @@ class Scale(object): def chunkify_qs(self, qs, field=None): if field is None: field = "at" + """ + Découpe un queryset en subdivisions, avec agrégation optionnelle des résultats + NB : on pourrait faire ça en une requête, au détriment de la lisibilité... + """ begin_f = "{}__gte".format(field) end_f = "{}__lte".format(field) return [qs.filter(**{begin_f: begin, end_f: end}) for begin, end in self] @@ -247,6 +266,13 @@ def last_stats_manifest( scale_prefix=scale_prefix, **url_params ) + """ + Convertit une spécification de scales en arguments GET utilisables par ScaleMixin. + La spécification est de la forme suivante : + - scales_def : liste de champs de la forme (label, scale) + - scale_args : arguments à passer à Scale.__init__ + - other_url_params : paramètres GET supplémentaires + """ # Étant donné un queryset d'operations @@ -260,16 +286,9 @@ 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`. + """ + Récupère les paramètres de subdivision encodés dans une requête GET. """ if params is None: params = self.request.GET diff --git a/kfet/views.py b/kfet/views.py index a04cda24..b9c690dd 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2235,11 +2235,19 @@ class PkUrlMixin(object): class SingleResumeStat(JSONDetailView): - """Manifest for a kind of a stat about an object. + """ + Génère l'interface de sélection pour les statistiques d'un compte/article. + L'interface est constituée d'une série de boutons, qui récupèrent et graphent + des statistiques du même type, sur le même objet mais avec des arguments différents. - Returns JSON whose payload is an array containing descriptions of a stat: - url to retrieve data, label, ... + Attributs : + - url_stat : URL où récupérer les statistiques + - stats : liste de dictionnaires avec les clés suivantes : + - label : texte du bouton + - url_params : paramètres GET à rajouter à `url_stat` + - default : si `True`, graphe à montrer par défaut + On peut aussi définir `stats` dynamiquement, via la fonction `get_stats`. """ id_prefix = "" @@ -2285,7 +2293,8 @@ ID_PREFIX_ACC_BALANCE = "balance_acc" class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): - """Manifest for balance stats of an account.""" + Menu général pour l'historique de balance d'un compte + """ model = Account context_object_name = "account" @@ -2313,10 +2322,11 @@ class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): class AccountStatBalance(PkUrlMixin, JSONDetailView): - """Datasets of balance of an account. - Operations and Transfers are taken into account. + """ + Statistiques (JSON) d'historique de balance d'un compte. + Prend en compte les opérations et transferts sur la période donnée. """ model = Account @@ -2441,7 +2451,10 @@ ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc" class AccountStatOperationList(PkUrlMixin, SingleResumeStat): - """Manifest for operations stats of an account.""" +@method_decorator(login_required, name="dispatch") + """ + Menu général pour l'historique de consommation d'un compte + """ model = Account context_object_name = "account" @@ -2463,7 +2476,10 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat): class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): - """Datasets of operations of an account.""" +@method_decorator(login_required, name="dispatch") + """ + Statistiques (JSON) de consommation (nb d'items achetés) d'un compte. + """ model = Account pk_url_kwarg = "trigramme" @@ -2535,7 +2551,9 @@ ID_PREFIX_ART_LAST_MONTHS = "last_months_art" class ArticleStatSalesList(SingleResumeStat): - """Manifest for sales stats of an article.""" + """ + Menu pour les statistiques de vente d'un article. + """ model = Article context_object_name = "article" @@ -2550,7 +2568,10 @@ class ArticleStatSalesList(SingleResumeStat): class ArticleStatSales(ScaleMixin, JSONDetailView): - """Datasets of sales of an article.""" + """ + Statistiques (JSON) de vente d'un article. + Sépare LIQ et les comptes K-Fêt, et rajoute le total. + """ model = Article context_object_name = "article" From 78ad4402b03bd215bcb360cf64683aa29aa40dae Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 15:10:02 +0100 Subject: [PATCH 02/11] Plus de timezones --- kfet/statistic.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index f308011e..81f81c1d 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,19 +1,17 @@ from datetime import date, datetime, time, timedelta -import pytz from dateutil.parser import parse as dateutil_parse from dateutil.relativedelta import relativedelta from django.db.models import Sum from django.utils import timezone -KFET_WAKES_UP_AT = time(7, 0) +KFET_WAKES_UP_AT = time(5, 0) # La K-Fêt ouvre à 5h (UTC) du matin def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): """Étant donné une date, renvoie un objet `datetime` correspondant au début du 'jour K-Fêt' correspondant.""" - naive = datetime.combine(date(year, month, day), start_at) - return pytz.timezone("Europe/Paris").localize(naive, is_dst=None) + return datetime.combine(date(year, month, day), start_at) def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): From 26bcd729bbccf31a19f8f3126d14f37a36e363ba Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:09:12 +0100 Subject: [PATCH 03/11] Supprime le code mort ou redondant --- kfet/statistic.py | 34 +++++-------------- kfet/views.py | 85 +++++++++++++---------------------------------- 2 files changed, 33 insertions(+), 86 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 81f81c1d..45f8fb65 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -65,7 +65,7 @@ class Scale(object): "or use last and n_steps" ) - self.datetimes = self.get_datetimes() + self._gen_datetimes() @staticmethod def by_name(name): @@ -74,9 +74,6 @@ class Scale(object): return cls return None - 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] @@ -86,13 +83,13 @@ class Scale(object): def do_step(self, dt, n_steps=1): return dt + self.step * n_steps - def get_datetimes(self): + def _gen_datetimes(self): datetimes = [self.begin] tmp = self.begin while tmp < self.end: tmp = self.do_step(tmp) datetimes.append(tmp) - return datetimes + self.datetimes = datetimes def get_labels(self, label_fmt=None): if label_fmt is None: @@ -273,45 +270,32 @@ def last_stats_manifest( """ -# Étant donné un queryset d'operations -# rend la somme des article_nb -def tot_ventes(queryset): - res = queryset.aggregate(Sum("article_nb"))["article_nb__sum"] - return res and res or 0 class ScaleMixin(object): - scale_args_prefix = "scale_" - - def get_scale_args(self, params=None, prefix=None): - + def parse_scale_args(self): """ Récupère les paramètres de subdivision encodés dans une requête GET. """ - 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) + name = self.request.GET.get("scale_name", None) if name is not None: scale_args["name"] = name - n_steps = params.get(prefix + "n_steps", None) + n_steps = self.request.GET.get("scale_n_steps", None) if n_steps is not None: scale_args["n_steps"] = int(n_steps) - begin = params.get(prefix + "begin", None) + begin = self.request.GET.get("scale_begin", None) if begin is not None: scale_args["begin"] = dateutil_parse(begin) - end = params.get(prefix + "send", None) + end = self.request.GET.get("scale_send", None) if end is not None: scale_args["end"] = dateutil_parse(end) - last = params.get(prefix + "last", None) + last = self.request.GET.get("scale_last", None) if last is not None: scale_args["last"] = last in ["true", "True", "1"] and True or False diff --git a/kfet/views.py b/kfet/views.py index b9c690dd..5455be8a 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2199,7 +2199,7 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView): # Vues génériques # --------------- # source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/ -class JSONResponseMixin(object): +class JSONResponseMixin: """ A mixin that can be used to render a JSON response. """ @@ -2228,12 +2228,6 @@ class JSONDetailView(JSONResponseMixin, BaseDetailView): return self.render_to_json_response(context) -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): """ Génère l'interface de sélection pour les statistiques d'un compte/article. @@ -2286,13 +2280,28 @@ class SingleResumeStat(JSONDetailView): return context +class UserAccountMixin: + """ + Mixin qui vérifie que le compte traité par la vue est celui de l'utilisateur·ice + actuel·le. Dans le cas contraire, renvoie un Http404. + """ + + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise Http404 + return obj + + # ----------------------- # Evolution Balance perso # ----------------------- ID_PREFIX_ACC_BALANCE = "balance_acc" -class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): +@method_decorator(login_required, name="dispatch") +class AccountStatBalanceList(UserAccountMixin, SingleResumeStat): + """ Menu général pour l'historique de balance d'un compte """ @@ -2310,20 +2319,11 @@ class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): ] nb_default = 0 - def get_object(self, *args, **kwargs): - obj = super().get_object(*args, **kwargs) - if self.request.user != obj.user: - raise Http404 - return obj - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) -class AccountStatBalance(PkUrlMixin, JSONDetailView): - +@method_decorator(login_required, name="dispatch") +class AccountStatBalance(UserAccountMixin, JSONDetailView): """ Statistiques (JSON) d'historique de balance d'un compte. Prend en compte les opérations et transferts sur la période donnée. @@ -2430,28 +2430,15 @@ 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 Http404 - return obj - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) # ------------------------ # Consommation personnelle # ------------------------ -ID_PREFIX_ACC_LAST = "last_acc" -ID_PREFIX_ACC_LAST_DAYS = "last_days_acc" -ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc" -ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc" -class AccountStatOperationList(PkUrlMixin, SingleResumeStat): @method_decorator(login_required, name="dispatch") +class AccountStatOperationList(UserAccountMixin, SingleResumeStat): """ Menu général pour l'historique de consommation d'un compte """ @@ -2464,19 +2451,11 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat): stats = last_stats_manifest(types=[Operation.PURCHASE]) url_stat = "kfet.account.stat.operation" - def get_object(self, *args, **kwargs): - obj = super().get_object(*args, **kwargs) - if self.request.user != obj.user: - raise Http404 - return obj - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) -class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): + @method_decorator(login_required, name="dispatch") +class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): """ Statistiques (JSON) de consommation (nb d'items achetés) d'un compte. """ @@ -2530,26 +2509,13 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): ] return context - def get_object(self, *args, **kwargs): - obj = super().get_object(*args, **kwargs) - if self.request.user != obj.user: - raise Http404 - return obj - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - # ------------------------ # Article Satistiques Last # ------------------------ -ID_PREFIX_ART_LAST = "last_art" -ID_PREFIX_ART_LAST_DAYS = "last_days_art" -ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art" -ID_PREFIX_ART_LAST_MONTHS = "last_months_art" +@method_decorator(teamkfet_required, name="dispatch") class ArticleStatSalesList(SingleResumeStat): """ Menu pour les statistiques de vente d'un article. @@ -2567,6 +2533,7 @@ class ArticleStatSalesList(SingleResumeStat): return super().dispatch(*args, **kwargs) +@method_decorator(teamkfet_required, name="dispatch") class ArticleStatSales(ScaleMixin, JSONDetailView): """ Statistiques (JSON) de vente d'un article. @@ -2623,7 +2590,3 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): }, ] return context - - @method_decorator(teamkfet_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) From ef35f45ad2aafa638674ce4a1aa6946125d40617 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:11:08 +0100 Subject: [PATCH 04/11] Fusionne deux fonctions `chunkify` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On rajoute de l'agrégation optionnelle dans la fonction. --- kfet/statistic.py | 92 ++++------------------------------------------- kfet/views.py | 42 +++++++--------------- 2 files changed, 18 insertions(+), 116 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 45f8fb65..98bcee32 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -2,7 +2,6 @@ from datetime import date, datetime, time, timedelta from dateutil.parser import parse as dateutil_parse from dateutil.relativedelta import relativedelta -from django.db.models import Sum from django.utils import timezone KFET_WAKES_UP_AT = time(5, 0) # La K-Fêt ouvre à 5h (UTC) du matin @@ -99,97 +98,18 @@ class Scale(object): for i, (begin, end) in enumerate(self) ] - def chunkify_qs(self, qs, field=None): - if field is None: - field = "at" + def chunkify_qs(self, qs, field="at", aggregate=None): """ Découpe un queryset en subdivisions, avec agrégation optionnelle des résultats NB : on pourrait faire ça en une requête, au détriment de la lisibilité... """ begin_f = "{}__gte".format(field) end_f = "{}__lte".format(field) - return [qs.filter(**{begin_f: begin, end_f: end}) for begin, end in self] - - def get_by_chunks(self, qs, field_callback=None, field_db="at"): - """Objects of queryset ranked according to the scale. - - Returns a generator whose each item, corresponding to a scale chunk, - is a generator of objects from qs for this chunk. - - Args: - qs: Queryset of source objects, must be ordered *first* on the - same field returned by `field_callback`. - field_callback: Callable which gives value from an object used - to compare against limits of the scale chunks. - Default to: lambda obj: getattr(obj, field_db) - field_db: Used to filter against `scale` limits. - Default to 'at'. - - Examples: - If queryset `qs` use `values()`, `field_callback` must be set and - could be: `lambda d: d['at']` - If `field_db` use foreign attributes (eg with `__`), it should be - something like: `lambda obj: obj.group.at`. - - """ - if field_callback is None: - - def field_callback(obj): - return getattr(obj, field_db) - - begin_f = "{}__gte".format(field_db) - end_f = "{}__lte".format(field_db) - - qs = qs.filter(**{begin_f: self.begin, end_f: self.end}) - - obj_iter = iter(qs) - - last_obj = None - - def _objects_until(obj_iter, field_callback, end): - """Generator of objects until `end`. - - Ends if objects source is empty or when an object not verifying - field_callback(obj) <= end is met. - - If this object exists, it is stored in `last_obj` which is found - from outer scope. - Also, if this same variable is non-empty when the function is - called, it first yields its content. - - Args: - obj_iter: Source used to get objects. - field_callback: Returned value, when it is called on an object - will be used to test ordering against `end`. - end - - """ - nonlocal last_obj - - if last_obj is not None: - yield last_obj - last_obj = None - - for obj in obj_iter: - if field_callback(obj) <= end: - yield obj - else: - last_obj = obj - return - - for begin, end in self: - # forward last seen object, if it exists, to the right chunk, - # and fill with empty generators for intermediate chunks of scale - if last_obj is not None: - if field_callback(last_obj) > end: - yield iter(()) - continue - - # yields generator for this chunk - # this set last_obj to None if obj_iter reach its end, otherwise - # it's set to the first met object from obj_iter which doesn't - # belong to this chunk - yield _objects_until(obj_iter, field_callback, end) + chunks = [qs.filter(**{begin_f: begin, end_f: end}) for begin, end in self] + if aggregate is None: + return chunks + else: + return [chunk.aggregate(agg=aggregate)["agg"] or 0 for chunk in chunks] class DayScale(Scale): diff --git a/kfet/views.py b/kfet/views.py index 5455be8a..647d78d9 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2465,7 +2465,7 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): context_object_name = "account" id_prefix = "" - def get_operations(self, scale, types=None): + def get_operations(self, 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 @@ -2477,28 +2477,20 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): ) if types is not None: all_operations = all_operations.filter(type__in=types) - chunks = scale.get_by_chunks( - all_operations, - field_db="group__at", - field_callback=(lambda d: d["group__at"]), - ) - return chunks + return all_operations def get_context_data(self, *args, **kwargs): - old_ctx = super().get_context_data(*args, **kwargs) - context = {"labels": old_ctx["labels"]} - scale = self.scale + context = super().get_context_data(*args, **kwargs) 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) + operations = self.get_operations(types=types) # On compte les opérations - nb_ventes = [] - for chunk in operations: - ventes = sum(ope["article_nb"] for ope in chunk) - nb_ventes.append(ventes) + nb_ventes = self.scale.chunkify_qs( + operations, field="group__at", aggregate=Sum("article_nb") + ) context["charts"] = [ { @@ -2558,23 +2550,13 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): liq_only = all_purchases.filter(group__on_acc__trigramme="LIQ") liq_exclude = all_purchases.exclude(group__on_acc__trigramme="LIQ") - chunks_liq = scale.get_by_chunks( - liq_only, field_db="group__at", field_callback=lambda d: d["group__at"] + nb_liq = scale.chunkify_qs( + liq_only, field="group__at", aggregate=Sum("article_nb") ) - chunks_no_liq = scale.get_by_chunks( - liq_exclude, field_db="group__at", field_callback=lambda d: d["group__at"] + nb_accounts = scale.chunkify_qs( + liq_exclude, field="group__at", aggregate=Sum("article_nb") ) - - # On compte les opérations - nb_ventes = [] - nb_accounts = [] - nb_liq = [] - for chunk_liq, chunk_no_liq in zip(chunks_liq, chunks_no_liq): - sum_accounts = sum(ope["article_nb"] for ope in chunk_no_liq) - sum_liq = sum(ope["article_nb"] for ope in chunk_liq) - nb_ventes.append(sum_accounts + sum_liq) - nb_accounts.append(sum_accounts) - nb_liq.append(sum_liq) + nb_ventes = [n1 + n2 for n1, n2 in zip(nb_liq, nb_accounts)] context["charts"] = [ { From 48ad5cd1c711b09359350f1333d7cb0cc5025f66 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:15:15 +0100 Subject: [PATCH 05/11] Misc cleanup On utilise SingleObjectMixin partout, et on simplifie 2-3 trucs --- kfet/statistic.py | 14 ++++++-------- kfet/views.py | 42 +++++++++++++++--------------------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 98bcee32..1578101b 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -125,7 +125,7 @@ class DayScale(Scale): class WeekScale(Scale): name = "week" step = timedelta(days=7) - label_fmt = "Semaine %W" + label_fmt = "%d %b." @classmethod def get_chunk_start(cls, dt): @@ -222,20 +222,18 @@ class ScaleMixin(object): return scale_args def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + # On n'hérite pas - scale_args = self.get_scale_args() + scale_args = self.parse_scale_args() scale_name = scale_args.pop("name", None) scale_cls = Scale.by_name(scale_name) if scale_cls is None: - scale = self.get_default_scale() + self.scale = self.get_default_scale() else: - scale = scale_cls(**scale_args) + self.scale = scale_cls(**scale_args) - self.scale = scale - context["labels"] = scale.get_labels() - return context + return {"labels": self.scale.get_labels()} def get_default_scale(self): return DayScale(n_steps=7, last=True) diff --git a/kfet/views.py b/kfet/views.py index 647d78d9..1dfde369 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2244,7 +2244,6 @@ class SingleResumeStat(JSONDetailView): On peut aussi définir `stats` dynamiquement, via la fonction `get_stats`. """ - id_prefix = "" nb_default = 0 stats = [] @@ -2252,12 +2251,15 @@ class SingleResumeStat(JSONDetailView): def get_context_data(self, **kwargs): # On n'hérite pas - object_id = self.object.id context = {} stats = [] - prefix = "{}_{}".format(self.id_prefix, object_id) - for i, stat_def in enumerate(self.stats): + # On peut avoir récupéré self.object via pk ou slug + if self.pk_url_kwarg in self.kwargs: url_pk = getattr(self.object, self.pk_url_kwarg) + else: + url_pk = getattr(self.object, self.slug_url_kwarg) + + for stat_def in self.get_stats(): url_params_d = stat_def.get("url_params", {}) if len(url_params_d) > 0: url_params = "?{}".format(urlencode(url_params_d)) @@ -2266,17 +2268,13 @@ class SingleResumeStat(JSONDetailView): 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 context["default_stat"] = self.nb_default - context["object_id"] = object_id return context @@ -2296,7 +2294,6 @@ class UserAccountMixin: # ----------------------- # Evolution Balance perso # ----------------------- -ID_PREFIX_ACC_BALANCE = "balance_acc" @method_decorator(login_required, name="dispatch") @@ -2306,10 +2303,9 @@ class AccountStatBalanceList(UserAccountMixin, SingleResumeStat): """ model = Account - context_object_name = "account" - pk_url_kwarg = "trigramme" + slug_url_kwarg = "trigramme" + slug_field = "trigramme" url_stat = "kfet.account.stat.balance" - id_prefix = ID_PREFIX_ACC_BALANCE stats = [ {"label": "Tout le temps"}, {"label": "1 an", "url_params": {"last_days": 365}}, @@ -2330,8 +2326,8 @@ class AccountStatBalance(UserAccountMixin, JSONDetailView): """ model = Account - pk_url_kwarg = "trigramme" - context_object_name = "account" + slug_url_kwarg = "trigramme" + slug_field = "trigramme" def get_changes_list(self, last_days=None, begin_date=None, end_date=None): account = self.object @@ -2444,9 +2440,8 @@ class AccountStatOperationList(UserAccountMixin, SingleResumeStat): """ model = Account - context_object_name = "account" - pk_url_kwarg = "trigramme" - id_prefix = ID_PREFIX_ACC_LAST + slug_url_kwarg = "trigramme" + slug_field = "trigramme" nb_default = 2 stats = last_stats_manifest(types=[Operation.PURCHASE]) url_stat = "kfet.account.stat.operation" @@ -2461,9 +2456,8 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): """ model = Account - pk_url_kwarg = "trigramme" - context_object_name = "account" - id_prefix = "" + slug_url_kwarg = "trigramme" + slug_field = "trigramme" def get_operations(self, types=None): # On selectionne les opérations qui correspondent @@ -2514,15 +2508,10 @@ class ArticleStatSalesList(SingleResumeStat): """ model = Article - context_object_name = "article" - id_prefix = ID_PREFIX_ART_LAST nb_default = 2 url_stat = "kfet.article.stat.sales" stats = last_stats_manifest() - @method_decorator(teamkfet_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) @method_decorator(teamkfet_required, name="dispatch") @@ -2536,8 +2525,7 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): context_object_name = "article" def get_context_data(self, *args, **kwargs): - old_ctx = super().get_context_data(*args, **kwargs) - context = {"labels": old_ctx["labels"]} + context = super().get_context_data(*args, **kwargs) scale = self.scale all_purchases = ( From c66fb7eb6fb8417857aeaa08faca158736e7b120 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:19:06 +0100 Subject: [PATCH 06/11] Simplify statistic.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On supprime des fonctions inutiles, on lint, et on simplifie 2-3 options inutilisées. --- kfet/static/kfet/js/statistic.js | 108 ++++++++++++------------------- 1 file changed, 41 insertions(+), 67 deletions(-) diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index 9baa08c4..23d66efe 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -1,28 +1,15 @@ -(function($){ +(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 = $("
"); var buttons; - function dictToArray (dict, start) { - // converts the dicts returned by JSONResponse to Arrays - // necessary because for..in does not guarantee the order - if (start === undefined) start = 0; - var array = new Array(); - for (var k in dict) { - array[k] = dict[k]; - } - array.splice(0, start); - return array; - } - - function handleTimeChart (data) { + function handleTimeChart(data) { // reads the balance data and put it into chartjs formatting chart_data = new Array(); for (var i = 0; i < data.length; i++) { @@ -36,7 +23,7 @@ return chart_data; } - function showStats () { + function showStats() { // CALLBACK : called when a button is selected // shows the focus on the correct button @@ -44,24 +31,20 @@ $(this).addClass("focus"); // loads data and shows it - $.getJSON(this.stats_target_url, {format: 'json'}, displayStats); + $.getJSON(this.stats_target_url, displayStats); } - function displayStats (data) { + function displayStats(data) { // reads the json data and updates the chart display 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]; - + for (let chart of data.charts) { // format the data - var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 0); + var chart_data = is_time_chart ? handleTimeChart(chart.values) : chart.values; chart_datasets.push( { @@ -76,29 +59,24 @@ // options for chartjs var chart_options = - { - responsive: true, - maintainAspectRatio: false, - tooltips: { - mode: 'index', - intersect: false, - }, - hover: { - mode: 'nearest', - intersect: false, - } - }; + { + responsive: true, + maintainAspectRatio: false, + tooltips: { + mode: 'index', + intersect: false, + }, + hover: { + mode: 'nearest', + intersect: false, + } + }; // additionnal options for time-indexed charts if (is_time_chart) { chart_options['scales'] = { xAxes: [{ type: "time", - display: true, - scaleLabel: { - display: false, - labelString: 'Date' - }, time: { tooltipFormat: 'll HH:mm', displayFormats: { @@ -115,26 +93,19 @@ } }], - yAxes: [{ - display: true, - scaleLabel: { - display: false, - labelString: 'value' - } - }] }; } // global object for the options var chart_model = - { - type: 'line', - options: chart_options, - data: { - labels: data.labels || [], - datasets: chart_datasets, - } - }; + { + type: 'line', + options: chart_options, + data: { + labels: data.labels || [], + datasets: chart_datasets, + } + }; // saves the previous charts to be destroyed var prev_chart = content.children(); @@ -151,23 +122,26 @@ } // initialize the interface - function initialize (data) { + function initialize(data) { // creates the bar with the buttons buttons = $("