Przeglądaj źródła

- plusiers modif

fanch 1 rok temu
rodzic
commit
7cb9e96ce3

+ 12 - 5
src/baby/app/common/calendar.py

@@ -2,6 +2,7 @@ import datetime
 
 from djangotools.common.types import date_to_string, parse_date
 
+from baby.app.models.const import get_calendar_interpolation
 from baby.app.models.regle import Cycle
 
 
@@ -34,12 +35,14 @@ class MonthCalendar:
         end = end - datetime.timedelta(days=end.day)
         self.end = end + datetime.timedelta(days=6-end.weekday())
 
-        self.cycles = Cycle.from_date_range(user, self.start, self.end)
-
+        self.cycles = Cycle._from_date_range(user, self.start, self.end)
+        # if get_calendar_interpolation(user):
+        #     self.cycles = self.cycles #.resolve_doubtfull()
+        return
     @property
     def prev_month(self):
         start = self.month_date - datetime.timedelta(days=1)
-        start = start - datetime.timedelta(days=start.day)
+        start = start - datetime.timedelta(days=start.day-1)
         return start
 
     @property
@@ -79,7 +82,8 @@ class MonthCalendar:
             while x <= self.end:
                 iterator.append((x, []))
                 x = x + datetime.timedelta(days=1)
-
+        else:
+            iterator = list(iterator)
 
 
         for i, (date, tags) in enumerate(iterator):
@@ -97,10 +101,13 @@ class MonthCalendar:
                 Cycle.OVULATION : Cycle.OVULATION in tags,
                 Cycle.FERTILITE : Cycle.FERTILITE in tags,
                 Cycle.DEBUT : Cycle.DEBUT in tags,
+                Cycle.DEBUT_SIMULATED : Cycle.DEBUT_SIMULATED in tags,
+                Cycle.TODAY : Cycle.TODAY in tags,
                 "classes" : []
             }
 
-            for tag in ["current_month", "not_current_month", Cycle.DEBUT, Cycle.FERTILITE, Cycle.OVULATION]:
+            for tag in ["current_month", "not_current_month", Cycle.DEBUT, Cycle.DEBUT_SIMULATED,
+                        Cycle.FERTILITE, Cycle.OVULATION, Cycle.TODAY]:
                 if day[tag]:
                     day["classes"].append(f"tag-{tag}")
             day["classes"] = " ".join(day["classes"])

+ 8 - 2
src/baby/app/context/base.py

@@ -13,6 +13,9 @@ default_value = {
         "max_period" : 45,
         "average_period_window" : 12,
         "method" : "contraception"
+    },
+    "calendar" : {
+        "interpolation" : True
     }
 }
 
@@ -43,10 +46,13 @@ class UserPrefences(UserPreferenceManager):
     def regle_method(self):
         return self.get("main", )["regle"]["method"]
 
-
     @property
     def ui_theme(self):
-        return self.get("main").value["ui"]["theme"]
+        return self.get("main")["ui"]["theme"]
+
+    @property
+    def calendar_interpolation(self):
+        return self.get("main")["calendar"]["interpolation"]
 
 
 class BaseContextData(ContextData):

+ 63 - 7
src/baby/app/context/edit.py

@@ -1,24 +1,51 @@
 import datetime
 
+from django.db import transaction
 from djangotools.common.types import date_to_string, parse_date
 from baby.app.context.base import BaseContextData, UserPrefences
-from baby.app.models.regle import Regle
+from baby.app.models.regle import Regle, Cycle, Cycles
 
 
-class EditData(BaseContextData):
+class ContextData(BaseContextData):
     page = "edit"
 
+    def _get_cycle_data(self, i, cycle, average):
+        classes = []
+        classes.append("regle-doubtfull" if cycle.doubtfull else "regle-ok")
+        resolved_doubtfull = cycle.resolve_doubtfull(average)
+
+        return {
+            "id" : i,
+            "date" : date_to_string(cycle.start),
+            "fin" : date_to_string(cycle.end),
+            "length" : cycle.length,
+            "classes" :  " ".join(classes),
+            "doubtfull" : cycle.doubtfull,
+            "resolve_doubtfull" : resolved_doubtfull,
+            "resolved_dates" : ",".join([date_to_string(x.start) for x in resolved_doubtfull]),
+        }
 
     def _send(self, user, debut, status=None, message=None):
         fin = datetime.date(debut.year + 1, 1, 1)
 
-        regles = Regle.objects.filter(user=user, date__gte=debut, date__lt=fin).order_by("-date")
+        regles = list(Regle.objects.filter(user=user, date__gte=debut, date__lt=fin).order_by("date"))
+        cycles = Cycles(user, start=debut, end=fin)
+        if regles:
+            start =  regles.pop(0)
+            while regles:
+                end = regles.pop(0)
+                cycle = Cycle.create(user, start.date, end.date)
+                cycles.append(cycle)
+                start = end
+
+        periods = cycles.get_periods(False)
+        average = (sum(periods)/len(periods)) if periods else None
         return {
             "year": debut.year,
-            "dates": [{"date": date_to_string(x.date), "id": x.id} for x in regles],
+            "dates": reversed([self._get_cycle_data(i, x, average) for i, x in enumerate(cycles)]),
             "prev": debut.year - 1,
             "next": debut.year + 1,
-            "count": len(regles),
+            "count": len(cycles),
             "title": "Editer les cycles",
             "is_post" : message is not None,
             "message" : message,
@@ -46,13 +73,42 @@ class EditData(BaseContextData):
 
         return self._send(user, debut, status, message)
 
+    def _repair(self, request):
+        src_date = parse_date(request.POST["src"])
+        new_dates = [parse_date(x) for x in request.POST["resolved"].split(",") if x]
+
+        if not new_dates:
+            return self._send(request.user, src_date, False, "Erreur aucune date de résolution n'a été trouvée")
+
+        with transaction.atomic():
+            try:
+                regle = Regle.objects.get(user=request.user, date=src_date)
+                regle.delete()
+            except Regle.DoesNotExist:
+                return self._send(request.user, src_date, False, f"Erreur impossible de trouver un cycle au {src_date}")
+            except Exception:
+                return self._send(request.user, src_date, False, f"Erreur de supprimer le cycle au {src_date}")
+
+            for date in new_dates:
+                try:
+                    Regle.objects.create(user=request.user, date=date)
+                except Exception:
+                    return self._send(request.user, src_date, False, f"Erreur de créer le cycle au {date}")
+
+        return  self._send(request.user, src_date, True, f"Le cycle a été modifié")
+
+
+
+
+
 
 
     def get_context_data(self, request, action_params, **kwargs):
         user = request.user
         debut = datetime.date.today()
-
-        if "delete_date" in getattr(request, "GET", {}):
+        if request.method=="POST":
+            return self._repair(request)
+        elif "delete_date" in getattr(request, "GET", {}):
             return self._delete(user, parse_date(request.GET["delete_date"]))
         else:
             if "year" in getattr(request, "GET", {}):

+ 3 - 1
src/baby/app/context/stats.py

@@ -1,5 +1,7 @@
 import datetime
 
+from djangotools.common.types import parse_date
+
 from baby.app.context.base import UserPrefences, BaseContextData
 from baby.app.models.const import get_average_month
 from baby.app.models.regle import Cycle, Regle
@@ -10,9 +12,9 @@ class ContextData(BaseContextData):
 
 
     def get_context_data(self, request, action_params, **kwargs):
-        today = datetime.date.today()
         user = request.user
         period = request.GET.get("period", request.POST.get("period", "custom"))
+        today = parse_date(request.GET.get("today", request.POST.get("today", datetime.date.today())))
 
         period = {
             "custom" : get_average_month(user),

+ 8 - 1
src/baby/app/models/const.py

@@ -7,4 +7,11 @@ def get_max_period(user):
     return user.pref.regle_max_period
 
 def get_average_month(user):
-    return user.pref.regle_average_period_window
+    return user.pref.regle_average_period_window
+
+def get_method(user):
+    return user.pref.regle_method
+def get_calendar_interpolation(user):
+    return user.pref.calendar_interpolation
+def get_ui_theme(user):
+    return user.pref.ui_theme

+ 130 - 33
src/baby/app/models/regle.py

@@ -5,19 +5,22 @@ from math import ceil
 from django.contrib.auth.models import User
 from django.db import models
 from djangotools.common.types import parse_date, date_to_string
+from djangotools.common.utils import get_today
 
 from baby.app.models.const import get_average_month, get_min_period, get_max_period
 
 
 class Cycles(list):
 
-    def __init__(self, start=None, end=None):
+    def __init__(self, user, start, end):
         super().__init__()
+        self.user = user
         self.start = start
         self.end = end
 
     def iter_dates(self, start=None, end=None):
         if not self: return
+        total = []
         start = start or self.start or self[0].start
         end = end or self.end or self[-1].end
         for cycle in self:
@@ -25,17 +28,51 @@ class Cycles(list):
             for date, cur in cycle:
                 if date<start: continue
                 if date>end: return
+                total.append(date)
                 yield date, cur
+        return
+
+    def get_periods(self, with_doubtfull):
+        return [
+            x.length for x in self
+            if with_doubtfull or not x.doubtfull
+        ]
+
+    def resolve_doubtfull(self):
+        cycles = Cycles(self.user, start=self.start, end=self.end)
+        no_doubtfull_periods = self.get_periods(with_doubtfull=False)
+        average = (sum(no_doubtfull_periods) / len(no_doubtfull_periods)) if no_doubtfull_periods else None
+        for cycle in self:
+            cycles.extend(cycle.resolve_doubtfull(average))
+        return cycles
+
+    def copy(self):
+        ret = Cycles(user=self.user, start=self.start, end=self.end)
+        ret.extend(self)
+        return ret
 
+def get_average_period(user, start, end):
+    cycle = Cycle.from_date_range(user, start, end)
+    cycle = cycle.resolve_doubtfull()
+    periods = cycle.get_periods(False)
+    if not periods: return None
+    return sum(periods) / len(periods)
+
+def get_average_period_from_today(user, monthes, ref):
+    ref = parse_date(ref)
+    start = ref - datetime.timedelta(days=monthes*30)
+    return get_average_period(user, start, ref)
 
 
 class Cycle:
     DEBUT="debut"
     OVULATION="ovulation"
     FERTILITE="fertilite"
+    DEBUT_SIMULATED="debut_simu"
+    TODAY="today"
 
-
-    def __init__(self):
+    def __init__(self, user):
+        self.user = user
         self.start : datetime.date = None
         self.end : datetime.date = None
         self.length : int = None
@@ -47,29 +84,61 @@ class Cycle:
         self.fertility_end : datetime.date = None
         self.ovulation_offset : int = None
         self.ovulation : datetime.date = None
+        self.doubtfull : bool = None
+        self.simulated : bool = None
 
     def as_list(self, with_date=False):
+        today = get_today()
+        def add_tag(ret, offset_day, tag):
+            if with_date:
+                ret[offset_day][1].append(tag)
+            else:
+                ret[offset_day].append(tag)
+
         if with_date:
             ret = [(self.start + datetime.timedelta(days=i), []) for i in range(self.length)]
-            ret[0][1].append(self.DEBUT)
-            if self.length >= 14:
-                ret[self.length - 14][1].append(self.OVULATION)
-            if self.fertility_start_offset:
-                [ret[i - 1][1].append(self.FERTILITE) for i in range(self.fertility_start_offset, self.fertility_end_offset + 1)  if 0 < i < self.length]
         else:
             ret = [[] for _ in range(self.length)]
-            ret[0].append(self.DEBUT)
-            if self.length >= 14:
-                ret[self.length-14].append(self.OVULATION)
-            if self.fertility_start_offset:
-                [ret[i-1].append(self.FERTILITE) for i in range(self.fertility_start_offset, self.fertility_end_offset+1) if 0 > i > self.length]
+
+        add_tag(ret, 0, self.DEBUT_SIMULATED if self.simulated else self.DEBUT)
+        if self.length >= 14 and not self.doubtfull:
+            add_tag(ret, self.length - 14, self.OVULATION)
+
+        if self.fertility_start_offset and not self.doubtfull:
+            [add_tag(ret, i-1, self.FERTILITE) for i in
+             range(self.fertility_start_offset, self.fertility_end_offset + 1) if 0 < i < self.length]
+
+        if self.start <= today <= self.end:
+            offset = (today - self.start).days
+            add_tag(ret, offset, self.TODAY)
+
         return ret
 
+    def resolve_doubtfull(self, average):
+        cycles = Cycles(self.user, self.start, self.end)
+        if not self.doubtfull:
+            cycles.append(self)
+            return cycles
+
+        if average is None or average >= self.length:
+            return cycles
+
+        n_periods = round(self.length / average)
+        period = self.length / n_periods
+        start = self.start
+        for i in range(1, 1 + n_periods - 1):
+            end = self.start + datetime.timedelta(days=round(i * period))
+            cycles.append(Cycle.create(self.user, start, end))
+            start = end
+        cycles.append(Cycle.create(self.user, start, self.end))
+        return cycles
+
     def __iter__(self):
         return iter(self.as_list(with_date=True))
 
     def __repr__(self):
-        return f"<Cycle {date_to_string(self.start)} to {date_to_string(self.end)} ({self.length})>"
+        doubtfull = " (!) " if self.doubtfull else ""
+        return f"<Cycle {date_to_string(self.start)} to {date_to_string(self.end)} ({self.length}){doubtfull}>"
 
     def __str__(self):
         return self.__repr__()
@@ -80,6 +149,8 @@ class Cycle:
             "start" : date_to_string(self.start),
             "end" : date_to_string(self.end),
             "length" : self.length,
+            "doubtfull" : self.doubtfull,
+            "simulated" : self.simulated,
             "ovulation" : date_to_string(self.ovulation),
             "fertility" : [date_to_string(self.fertility_start),date_to_string(self.fertility_end)],
             "ovulation_offset" : self.ovulation_offset,
@@ -109,13 +180,15 @@ class Cycle:
         if self.ovulation_offset is not None:
             self.ovulation = self.start + datetime.timedelta(days=self.ovulation_offset-1)
 
+        self.doubtfull = not (get_min_period(self.user) <= self.length <= get_max_period(self.user))
+
     def _fertility_init(self):
-        self.fertility_start_offset = self.min_cycle and int(self.min_cycle - 18)
-        self.fertility_end_offset = self.max_cycle and ceil(self.max_cycle - 11)
+        self.fertility_start_offset = self.min_cycle and (self.min_cycle - 18)
+        self.fertility_end_offset = self.max_cycle and (self.max_cycle - 11)
         self.ovulation_offset = ceil(self.length - 11)
 
     @classmethod
-    def create(cls, user, start, end=None, length=None, monthes=None):
+    def create(cls, user, start, end=None, length=None, monthes=None, simulated=False):
         filter = partial(Regle.objects.filter, user=user)
         kwargs = {
             "date__lte": start
@@ -133,7 +206,7 @@ class Cycle:
             mini = min(periods)
             maxi = max(periods)
 
-        cycle = cls()
+        cycle = cls(user)
         cycle.start = start
         if end:
             cycle.end = end
@@ -144,55 +217,79 @@ class Cycle:
 
         cycle.min_cycle = mini
         cycle.max_cycle = maxi
+        cycle.simulated = simulated
         cycle._init()
         return cycle
 
     @classmethod
-    def from_day(cls, user, ref_date=None, monthes=None):
-        today = datetime.date.today()
+    def from_day(cls, user, ref_date=None, monthes=None, today=None):
+        today = today or datetime.date.today()
         date = parse_date(ref_date) or today
         regle = Regle.objects.last_regle_before(user, date)
         end = Regle.objects.next_regle_after(user, date)
         monthes = monthes if monthes is not None else get_average_month(user)
 
         kwargs = {"user": user}
-        x = user.pref.regle_max_period
-        if ref_date and ref_date > datetime.date.today():
-            length = Regle.objects.average_period(user, monthes=monthes)
-            ilength = int(length)
+        if ref_date and ref_date > today:
+            length = get_average_period_from_today(user, monthes, today)
+            ilength = round(length)
+            offset =  int((date - regle.date).days / ilength) * ilength
 
-            offset =  int((date - regle.date).days / ilength) * int(ilength)
             kwargs["start"] = regle.date + datetime.timedelta(days=int(offset))
             kwargs["end"] = kwargs["start"] + datetime.timedelta(days=ilength)
+            kwargs["simulated"] = offset >= length
         elif regle:
             kwargs["start"] = regle.date
             if end:
                 kwargs["end"]  = end.date
             else:
-                length = Regle.objects.average_period(user, monthes=monthes)
+                length = get_average_period_from_today(user, monthes, today)
                 if length is not None:
-                    kwargs["length"]  = int(length)
+                    kwargs["length"]  = round(length)
                 else:
                     return None
-        else:
-            return None
+        elif end:
+            length = get_average_period(user, end.date, end.date + datetime.timedelta(days=monthes*12))
+            ilength = round(length)
+            offset =  ceil((end.date - date).days / ilength) * ilength
+            kwargs["start"] = end.date - datetime.timedelta(days=int(offset))
+            kwargs["end"] = kwargs["start"] + datetime.timedelta(days=ilength)
+            kwargs["simulated"] = True
 
         return cls.create(**kwargs)
 
     @classmethod
-    def from_date_range(cls, user, start, end):
+    def _from_date_range(cls, user, start, end):
         if start > end: start, end = end, start
-        cycles = Cycles(start=start, end=end)
+        cycles = Cycles(user, start=start, end=end)
         curr = start
-        while curr <= end:
+        while curr <= end+datetime.timedelta(days=1):
             cycle = cls.from_day(user, curr)
             if  cycle is None:
                 break
             cycles.append(cycle)
-            curr = cycle.end + datetime.timedelta(days=1)
+            curr = cycle.end  + datetime.timedelta(days=1)
 
         return cycles
 
+
+    @classmethod
+    def from_date_range(cls, user, start, end):
+        if start > end: start, end = end, start
+        cycles = Cycles(user, start=start, end=end)
+
+        regles = list(Regle.objects.filter(user=user, date__gte=start, date__lt=end).order_by("date"))
+        if regles:
+            start = regles.pop(0)
+            while regles:
+                end = regles.pop(0)
+                cycle = Cycle.create(user, start.date, end.date)
+                cycles.append(cycle)
+                start = end
+
+        return cycles
+
+
     @classmethod
     def from_month(cls, user, mont_or_date, year):
         if isinstance(mont_or_date, str):

+ 1 - 1
src/baby/app/views/main.py

@@ -3,7 +3,6 @@ from djangotools.views import Route, Router, render_page
 
 pages = [
     ("", "index", "index.html"),
-    ("edit", "edit", "edit.html"),
     ("calendar", "calendar", "calendar.html"),
     ("validate_cycle", "validate_cycle", "validate_cycle.html"),
     ("stats", "stats", "stats.html"),
@@ -11,6 +10,7 @@ pages = [
 
 Router.route("parameters", {"POST", "GET"})(render_page("parameters", "parameters.html", context_to_string=False))
 Router.route("new_cycle", {"POST", "GET"})(render_page("new_cycle", "new_cycle.html", context_to_string=False))
+Router.route("edit", {"POST", "GET"})(render_page("edit", "edit.html", context_to_string=False))
 
 
 for url, context, template in pages:

+ 15 - 6
src/baby/frontend/static/css/calendar/calendar.css

@@ -24,32 +24,41 @@
 
 
 .calendar-day {
-    border: 1px solid #aac;
+    border: 1px solid var(--color-1);
+    font-weight: 500;
 }
 
 .tag-current_month {
-    color: rgb(197, 197, 218);
-    font-weight: 600;
+    color: var(--color-1);
+    font-weight: 700;
 }
 
 .tag-not_current_month {
     font-style: italic;
+    font-weight: 300;
 }
 
 .tag-debut {
-    background-color: rgb(16, 68, 14);
+    background-color: var(--color-3);
 }
 
 .tag-ovulation {
-    background-color: rgb(126, 18, 18) !important;
+    background-color: var(--color-fail) !important;
     
 }
 
 .tag-fertilite {
-    background-color: rgb(138, 103, 39);
+    background-color: var(--color-warning);
     
 }
 
+.tag-debut_simu {
+    background: repeating-linear-gradient(
+      50deg, 
+      rgb(16, 68, 14), rgb(16, 68, 14) 10px, 
+      rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 0) 20px );
+}
+
 .calendar {
     width: 100%;
     text-align: center;

+ 49 - 5
src/baby/frontend/static/css/edit/edit.css

@@ -3,7 +3,6 @@
     cursor: pointer;
     font-size: 24pt;
     margin-left: 10px;
-    border-color: 1px solid #321;
     font-weight: 700;
 }
 
@@ -33,20 +32,65 @@
 
 .regle {
     padding: 5px;
-    background-color: var(--color-5);
+    height: 60px;
+    border-radius: 3px;
+}
+
+.regle-ok {
+    background-color: var(--color-3);
     color: var(--color-4);
-    height: 52px;
 }
 
+.regle-doubtfull {
+    background-color: var(--color-2);
+    color: var(--color-4);
+
+}
+
+
 .regle-content {
-    font-size: 22pt;
+    /*font-size: 22pt;*/
+    float: left;
+}
+
+.regle-title {
+    font-size: 15pt;
+    font-weight: 600;
+    padding-left: 5px;
+    padding-right: 5px;
+    width: fit-content;
+    border-radius: 10px;
+}
+
+
+.regle-subtitle {
+    
+}
+
+.regle-resolve {
+    padding: 5px;
+    display: none;
+    
+}
+
+.regle-resolve-ul {
+    
+}
+
+.regle-resolve-li {
+    
 }
 
 .regle-action {
     float: right;
     cursor: pointer;
     color: var(--color-4);
-    font-size: 22pt;
+    font-size: 24pt;
+    margin-left: 15px;
+    border: 1px solid var(--color-4);
+    border-radius: 3px;
+    width: 50px;
+    text-align: center;
 }
 
 .no-regle {

+ 5 - 2
src/baby/frontend/static/css/main/main.css

@@ -6,6 +6,11 @@
     --color-3: #559663;
     --color-4: #1f1f1f;
     --color-5: #677fb4;
+    --color-6: #acb467;
+
+    --color-ok: var(--color-3);
+    --color-fail: var(--color-2);
+    --color-warning: rgb(146, 151, 86);
 }
 
 
@@ -83,8 +88,6 @@ html {
 
 nav {
     background-color: var(--color-2);
-    color: var(--color-1);
-    border-color: 1px solid var(--color-1);
 }
 
 .nav-titre{

+ 1 - 0
src/baby/frontend/templates/calendar.html

@@ -95,6 +95,7 @@
             </table>
             <div>
                 <span class="lengend tag-debut">Debut</span>
+                <span class="lengend tag-debut_simu">Debut (estimé)</span>
                 <span class="lengend tag-fertilite">Fertilité</span>
                 <span class="lengend tag-ovulation">Ovulation</span>
             </div>

+ 51 - 8
src/baby/frontend/templates/edit.html

@@ -48,12 +48,36 @@
                 {% endif %}
                 {% for regle in context.data.dates %}
                     <div class="regle-wrapper">
-                        <div class="regle">
-                            <span class="regle-content">
-                                {{ regle.date }}
-                            </span>
+                        <div class="regle-title {{regle.classes}}">
+                            {{ regle.date }}
+                        </div>
+                        <div class="regle {{regle.classes}}">
+                            <div class="regle-content">
+                                
+                                <div> Du {{ regle.date }} au {{ regle.fin }}</div>
+                                <div>{{ regle.length }} jours</div>
+                            </div>
+
+                            <a class="regle-action">
+                                <i class="bi-trash" onclick="remove_regle('{{regle.date}}')"></i>
+                            </a>
+                            {% if regle.doubtfull %}
                             <a class="regle-action">
-                                <i class="bi-trash" onclick="remove_regle('{{regle.date}}','{{regle.id}}')"></i>
+                                <i class="bi bi-wrench" onclick="toggle_doubtfull('{{regle.id}}')"></i>
+                            </a>
+                            {% endif %}
+                        </div>
+                        <div class="regle-resolve regle-doubtfull" id="resolve-{{regle.id}}">
+                            Remplacer le cycle par:
+                            <ul class="regle-resolve-ul">
+                                {% for res in regle.resolve_doubtfull %}
+                                    <li class="regle-resolve-li">
+                                        Cycle du {{res.start}} ai {{res.end}} ({{res.length}} jours)
+                                    </li>
+                                {% endfor %}
+                            </ul>
+                            <a class="btn btn-submit" onclick="resolve_doubtfull('{{regle.date}}', '{{regle.resolved_dates}}')">
+                                Remplacer
                             </a>
                         </div>
                     </div>
@@ -80,7 +104,7 @@
                 </div>
             </div>
         </div>
-          
+        <div id="inset_form"></div>
     </main>
 
 
@@ -88,7 +112,7 @@
     <script src="{{context.base_url}}/static/js/vendor/bootstrap.js"></script>
     <script>
         var dest_date = null;
-        function remove_regle(date, id){
+        function remove_regle(date){
             $("#regle-date").html(date)
             dest_date = date;
             $("#staticBackdrop").modal("show")
@@ -101,9 +125,28 @@
         function  remove_message(){
             $(".message-fail").remove();
             $(".message-success").remove();
-            $(".invalid-feedback").show()
+            $(".invalid-feedback").show();
         }   
 
+        function toggle_doubtfull(id){
+            $("#resolve-"+id).toggle(300);
+            console.log("ok", "#resolve-"+id)
+        }
+
+        function resolve_doubtfull(start, resolved) {
+
+            console.log(start, resolved)
+            $('#inset_form').html('<form action="?" name="repair" method="post" style="display:none;">'
+                +'<input type="text" name="repair" value="true" />'
+                +'<input type="text" name="src" value="'+start+'" />'
+                +'<input type="text" name="resolved" value="'+resolved+'" />'
+                +'</form>'
+            );
+
+            document.forms['repair'].submit();
+
+        }
+
     </script>
 </body>
 </html>

+ 1 - 1
src/baby/frontend/templates/stats.html

@@ -87,7 +87,7 @@
             <div class="col-6 col-xl-4 tile">
                 <div class="tile-content-transparent">
                     <div class="tile-title-text">
-                        {{context.data.average}}
+                        {{context.data.average|floatformat:2}}
                     </div>
                     <a class="btn tile-title-info">
                         Cylce moyen 

+ 0 - 0
tests/baby/__init__.py


+ 0 - 0
tests/baby/app/__init__.py


+ 0 - 0
tests/baby/app/context/__init__.py


+ 29 - 0
tests/baby/app/context/test_edit.py

@@ -0,0 +1,29 @@
+import datetime
+import math
+
+import pytest
+from djangotools.common.types import parse_date, date_to_string
+
+from baby.app.common.calendar import MonthCalendar
+from baby.app.context.edit import ContextData as EditContextData
+from tests.util import create_regles
+
+from tests.util import Request
+@pytest.mark.django_db
+class TestEdit:
+    def _req(self, lengthes, delta, get=None, date="01/10/2023"):
+        if get is None:
+            get = {}
+        get.update({"year":  parse_date(date).year})
+        return  Request("GET", create_regles(lengthes, delta, parse_date(date)), get=get)
+
+    def test_nominal_case(self, client):
+        date = parse_date("01/10/2023")
+        content = [25, 24, 50, 26, 27]
+        offset = 1
+        req = self._req(content, offset, date=date)
+        ret = req.test(EditContextData)
+
+        return ret
+
+

+ 45 - 0
tests/baby/app/context/test_stats.py

@@ -0,0 +1,45 @@
+import datetime
+import math
+
+import pytest
+from djangotools.common.types import parse_date, date_to_string
+
+from baby.app.common.calendar import MonthCalendar
+from baby.app.context.stats import ContextData as StatsContextData
+from tests.util import create_regles
+
+from tests.util import Request
+@pytest.mark.django_db
+class TestStats:
+    def _req(self, lengthes, delta, get=None, date="01/10/2023"):
+        if get is None:
+            get = {}
+        get.update({"today":  date_to_string(date)})
+        return  Request("GET", create_regles(lengthes, delta, parse_date(date)), get=get)
+
+    @pytest.mark.parametrize("content,offset", [
+        ([24, 25, 26, 27], 1),
+        ([24, 25, 26, 27, 27], 1),
+        ([24, 25, 26, 27, 24], 1),
+        ([24, 25, 26 ], 1),
+    ])
+    def test_nominal_case(self, client, content, offset):
+        date = parse_date("01/10/2023")
+        print(content, offset)
+        req = self._req(content, offset, date=date)
+        ret = req.test(StatsContextData)
+        data = ret["context"]["data"]
+        start = parse_date(data["start"])
+        end = parse_date(data["end"])
+        mini = data["min"]
+        maxi = data["max"]
+        average = data["average"]
+
+        assert mini == min(content)
+        assert maxi == max(content)
+        assert average == sum(content) / len(content) if content else True
+        assert start == date - datetime.timedelta(days=offset) if content else True
+        assert end == start + datetime.timedelta(days=round(average))
+        print(f"Average = {average}")
+
+

+ 0 - 0
tests/baby/cycle/__init__.py


+ 115 - 0
tests/baby/cycle/test_cycles.py

@@ -0,0 +1,115 @@
+import datetime
+import math
+from functools import partial
+
+import pytest
+from djangotools.common.types import parse_date, date_to_string
+
+from baby.app.common.calendar import MonthCalendar
+from baby.app.context.base import UserPrefences
+from baby.app.context.edit import ContextData as EditContextData
+from baby.app.models.regle import Cycle
+from tests.util import create_regles, prefs
+
+from tests.util import Request
+@pytest.mark.django_db
+class TestCcyles:
+
+    def test_cycles_doubtfull_1(self):
+        date = parse_date("01/10/2023")
+        content = [25, 25, 75, 25, 25]
+        offset = 0
+        start = date - datetime.timedelta(days=sum(content)+offset)
+        cycles = Cycle.from_date_range(create_regles(content, offset, date), start, date+datetime.timedelta(days=1))
+        for i in range(len(content)):
+            assert (content[i] > 40) == cycles[i].doubtfull
+        no_doubt = cycles.resolve_doubtfull()
+
+        assert all([not x.doubtfull for x in no_doubt])
+        assert all([x.length == 25 for x in no_doubt])
+        assert len(no_doubt) == 7
+
+        return cycles
+
+    def test_cycles_doubtfull_2(self):
+        date = parse_date("01/10/2023")
+        content = [25, 26, 27, 50, 24, 26, 28, 29]
+        offset = 0
+        start = date - datetime.timedelta(days=sum(content)+offset)
+        cycles = Cycle.from_date_range(create_regles(content, offset, date), start, date+datetime.timedelta(days=1))
+        for i in range(len(content)):
+            assert (content[i] > 40) == cycles[i].doubtfull
+        no_doubt = cycles.resolve_doubtfull()
+
+        assert all([not x.doubtfull for x in no_doubt])
+        assert len(no_doubt) == 9
+
+        return cycles
+
+
+    def test_cycles_doubtfull_3(self):
+        date = parse_date("01/10/2023")
+        content = [25, 50,]
+        offset = 0
+        start = date - datetime.timedelta(days=sum(content)+offset)
+        cycles = Cycle.from_date_range(create_regles(content, offset, date), start, date+datetime.timedelta(days=1))
+        for i in range(len(content)):
+            assert (content[i] > 40) == cycles[i].doubtfull
+        no_doubt = cycles.resolve_doubtfull()
+
+        assert all([not x.doubtfull for x in no_doubt])
+        assert len(no_doubt) == 3
+
+        return cycles
+
+
+    def test_cycles_doubtfull_4(self):
+        date = parse_date("01/10/2023")
+        content = [50, 25,]
+        offset = 0
+        start = date - datetime.timedelta(days=sum(content)+offset)
+        cycles = Cycle.from_date_range(create_regles(content, offset, date), start, date+datetime.timedelta(days=1))
+        for i in range(len(content)):
+            assert (content[i] > 40) == cycles[i].doubtfull
+        no_doubt = cycles.resolve_doubtfull()
+
+        assert all([not x.doubtfull for x in no_doubt])
+        assert len(no_doubt) == 3
+
+        return cycles
+
+    def test_cycles_doubtfull_5(self):
+        date = parse_date("01/10/2023")
+        content = [50,]
+        offset = 0
+        start = date - datetime.timedelta(days=sum(content)+offset)
+        cycles = Cycle.from_date_range(create_regles(content, offset, date), start, date+datetime.timedelta(days=1))
+        for i in range(len(content)):
+            assert (content[i] > 40) == cycles[i].doubtfull
+        no_doubt = cycles.resolve_doubtfull()
+
+        assert all([not x.doubtfull for x in no_doubt])
+        assert len(no_doubt) == 0
+        return cycles
+
+    def test_cycles_doubtfull_6(self):
+        data = {'main': {"regle" : {
+            "min_period" : 10,
+            "max_period" : 38,
+            "average_period_window" : 12, } }}
+        date = parse_date("01/10/2023")
+
+        content = [25, 25, 40, 24, 26, 27,]
+        offset = 0
+        start = date - datetime.timedelta(days=sum(content)+offset)
+        cycles = Cycle.from_date_range(create_regles(content, offset, date, pref_class=prefs(data)),
+                                       start, date+datetime.timedelta(days=1))
+        for i in range(len(content)):
+            assert (content[i] >= 40) == cycles[i].doubtfull
+        no_doubt = cycles.resolve_doubtfull()
+
+        assert all([not x.doubtfull for x in no_doubt])
+        assert len(no_doubt) == 7
+        return cycles
+
+

+ 26 - 0
tests/test_average_length.py

@@ -0,0 +1,26 @@
+
+
+import datetime
+
+import pytest
+from django.contrib.auth.models import User
+from djangotools.common.types import parse_date
+
+from baby.app.common.calendar import MonthCalendar
+from baby.app.context.stats import ContextData as StatsContextData
+from baby.app.models.regle import Regle, Cycle
+from tests.util import create_regles
+
+
+@pytest.mark.django_db
+class TestAverageLength:
+
+    def test_nominal_case(self, client):
+        StatsContextData
+        ref = parse_date("01/10/2023")
+        user = create_regles([24,25,26,27], 1, ref)
+        cal = MonthCalendar(user, ref)
+        data = cal.get_calendar_dict()
+        js = cal.json
+        print(data)
+

+ 79 - 0
tests/util.py

@@ -0,0 +1,79 @@
+import datetime
+
+from django.contrib.auth.models import User
+from djangotools.common.types import parse_date
+from djangotools.models import UserPreferenceManager
+
+from baby.app.context.base import UserPrefences
+from baby.app.models.regle import Regle
+
+def get_user(user=None, pref_class=UserPrefences):
+    if user is None:
+        try:
+            user = User.objects.create_user("user")
+        except User.DoesNotExist:
+            user = User.objects.get(username="user")
+    if isinstance(pref_class, UserPreferenceManager):
+        user.pref = pref_class
+    if callable(pref_class):
+        user.pref = pref_class(user)
+    else:
+        user.pref = UserPreferenceManagerMocker(user, pref_class)
+    return user
+
+def create_regles(lengthes, delta=0, today=None, user=None, pref_class=UserPrefences):
+    user = get_user(user, pref_class=pref_class)
+
+    if isinstance(today, str):
+        today = parse_date(today)
+    curr =  today or datetime.date.today()
+    if isinstance(delta, int):
+        curr = curr - datetime.timedelta(days=delta)
+    elif isinstance(delta, datetime.timedelta):
+        curr = curr - delta
+    else:
+        raise Exception()
+
+    Regle.objects.create(user, curr)
+    for duration in reversed(lengthes):
+        curr -= datetime.timedelta(days=duration)
+        Regle.objects.create(user, curr)
+
+    return user
+
+
+class UserPreferenceManagerMocker(UserPreferenceManager):
+
+    def __init__(self, user, data, profile="default"):
+        super().__init__(user, profile)
+        self.data = data
+
+    def get(self, key, default=None):
+        return self.data.get(key, default)
+
+def prefs(data, classe=UserPrefences):
+    class Classe(classe):
+        def __init__(self, user, profile="default", data=data):
+            super().__init__(user, profile)
+            self.data = data
+
+        def get(self, key, default=None):
+            return self.data.get(key, default)
+
+    return Classe
+
+class Request:
+
+    def __init__(self, method, user, get=None, post=None, body=None, headers=None, pref_class=UserPrefences):
+        self.method = method
+        self.GET = get or {}
+        self.POST = post or {}
+        self.body = body
+        self.headers = headers if headers is not None else {}
+
+        self.user = get_user(user, pref_class)
+
+
+    def test(self, Context,  **kwargs):
+        ctx = Context()
+        return ctx.get_context(self, **kwargs)

+ 5 - 6
todo

@@ -1,6 +1,5 @@
-- Afficahge des floatant
-    -> Dans Stats / Cycle moyen, faire du .2f
-- Arrondi à travailler quand le cycle moyen n'est pas entier
-    -> actuellement si il est de 25.9, il sera considéré comme 25 et non 26
-- Détecter ou ignorer les règles non marquée
-    - ex 23/05, 12/07, 06/08 (il manque le 17/06)
+
+Moins prioritaire
+    - Récupération des données
+    - Mention légales
+    - Page d'inscription