|
@@ -5,19 +5,22 @@ from math import ceil
|
|
from django.contrib.auth.models import User
|
|
from django.contrib.auth.models import User
|
|
from django.db import models
|
|
from django.db import models
|
|
from djangotools.common.types import parse_date, date_to_string
|
|
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
|
|
from baby.app.models.const import get_average_month, get_min_period, get_max_period
|
|
|
|
|
|
|
|
|
|
class Cycles(list):
|
|
class Cycles(list):
|
|
|
|
|
|
- def __init__(self, start=None, end=None):
|
|
|
|
|
|
+ def __init__(self, user, start, end):
|
|
super().__init__()
|
|
super().__init__()
|
|
|
|
+ self.user = user
|
|
self.start = start
|
|
self.start = start
|
|
self.end = end
|
|
self.end = end
|
|
|
|
|
|
def iter_dates(self, start=None, end=None):
|
|
def iter_dates(self, start=None, end=None):
|
|
if not self: return
|
|
if not self: return
|
|
|
|
+ total = []
|
|
start = start or self.start or self[0].start
|
|
start = start or self.start or self[0].start
|
|
end = end or self.end or self[-1].end
|
|
end = end or self.end or self[-1].end
|
|
for cycle in self:
|
|
for cycle in self:
|
|
@@ -25,17 +28,51 @@ class Cycles(list):
|
|
for date, cur in cycle:
|
|
for date, cur in cycle:
|
|
if date<start: continue
|
|
if date<start: continue
|
|
if date>end: return
|
|
if date>end: return
|
|
|
|
+ total.append(date)
|
|
yield date, cur
|
|
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:
|
|
class Cycle:
|
|
DEBUT="debut"
|
|
DEBUT="debut"
|
|
OVULATION="ovulation"
|
|
OVULATION="ovulation"
|
|
FERTILITE="fertilite"
|
|
FERTILITE="fertilite"
|
|
|
|
+ DEBUT_SIMULATED="debut_simu"
|
|
|
|
+ TODAY="today"
|
|
|
|
|
|
-
|
|
|
|
- def __init__(self):
|
|
|
|
|
|
+ def __init__(self, user):
|
|
|
|
+ self.user = user
|
|
self.start : datetime.date = None
|
|
self.start : datetime.date = None
|
|
self.end : datetime.date = None
|
|
self.end : datetime.date = None
|
|
self.length : int = None
|
|
self.length : int = None
|
|
@@ -47,29 +84,61 @@ class Cycle:
|
|
self.fertility_end : datetime.date = None
|
|
self.fertility_end : datetime.date = None
|
|
self.ovulation_offset : int = None
|
|
self.ovulation_offset : int = None
|
|
self.ovulation : datetime.date = None
|
|
self.ovulation : datetime.date = None
|
|
|
|
+ self.doubtfull : bool = None
|
|
|
|
+ self.simulated : bool = None
|
|
|
|
|
|
def as_list(self, with_date=False):
|
|
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:
|
|
if with_date:
|
|
ret = [(self.start + datetime.timedelta(days=i), []) for i in range(self.length)]
|
|
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:
|
|
else:
|
|
ret = [[] for _ in range(self.length)]
|
|
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
|
|
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):
|
|
def __iter__(self):
|
|
return iter(self.as_list(with_date=True))
|
|
return iter(self.as_list(with_date=True))
|
|
|
|
|
|
def __repr__(self):
|
|
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):
|
|
def __str__(self):
|
|
return self.__repr__()
|
|
return self.__repr__()
|
|
@@ -80,6 +149,8 @@ class Cycle:
|
|
"start" : date_to_string(self.start),
|
|
"start" : date_to_string(self.start),
|
|
"end" : date_to_string(self.end),
|
|
"end" : date_to_string(self.end),
|
|
"length" : self.length,
|
|
"length" : self.length,
|
|
|
|
+ "doubtfull" : self.doubtfull,
|
|
|
|
+ "simulated" : self.simulated,
|
|
"ovulation" : date_to_string(self.ovulation),
|
|
"ovulation" : date_to_string(self.ovulation),
|
|
"fertility" : [date_to_string(self.fertility_start),date_to_string(self.fertility_end)],
|
|
"fertility" : [date_to_string(self.fertility_start),date_to_string(self.fertility_end)],
|
|
"ovulation_offset" : self.ovulation_offset,
|
|
"ovulation_offset" : self.ovulation_offset,
|
|
@@ -109,13 +180,15 @@ class Cycle:
|
|
if self.ovulation_offset is not None:
|
|
if self.ovulation_offset is not None:
|
|
self.ovulation = self.start + datetime.timedelta(days=self.ovulation_offset-1)
|
|
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):
|
|
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)
|
|
self.ovulation_offset = ceil(self.length - 11)
|
|
|
|
|
|
@classmethod
|
|
@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)
|
|
filter = partial(Regle.objects.filter, user=user)
|
|
kwargs = {
|
|
kwargs = {
|
|
"date__lte": start
|
|
"date__lte": start
|
|
@@ -133,7 +206,7 @@ class Cycle:
|
|
mini = min(periods)
|
|
mini = min(periods)
|
|
maxi = max(periods)
|
|
maxi = max(periods)
|
|
|
|
|
|
- cycle = cls()
|
|
|
|
|
|
+ cycle = cls(user)
|
|
cycle.start = start
|
|
cycle.start = start
|
|
if end:
|
|
if end:
|
|
cycle.end = end
|
|
cycle.end = end
|
|
@@ -144,55 +217,79 @@ class Cycle:
|
|
|
|
|
|
cycle.min_cycle = mini
|
|
cycle.min_cycle = mini
|
|
cycle.max_cycle = maxi
|
|
cycle.max_cycle = maxi
|
|
|
|
+ cycle.simulated = simulated
|
|
cycle._init()
|
|
cycle._init()
|
|
return cycle
|
|
return cycle
|
|
|
|
|
|
@classmethod
|
|
@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
|
|
date = parse_date(ref_date) or today
|
|
regle = Regle.objects.last_regle_before(user, date)
|
|
regle = Regle.objects.last_regle_before(user, date)
|
|
end = Regle.objects.next_regle_after(user, date)
|
|
end = Regle.objects.next_regle_after(user, date)
|
|
monthes = monthes if monthes is not None else get_average_month(user)
|
|
monthes = monthes if monthes is not None else get_average_month(user)
|
|
|
|
|
|
kwargs = {"user": 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["start"] = regle.date + datetime.timedelta(days=int(offset))
|
|
kwargs["end"] = kwargs["start"] + datetime.timedelta(days=ilength)
|
|
kwargs["end"] = kwargs["start"] + datetime.timedelta(days=ilength)
|
|
|
|
+ kwargs["simulated"] = offset >= length
|
|
elif regle:
|
|
elif regle:
|
|
kwargs["start"] = regle.date
|
|
kwargs["start"] = regle.date
|
|
if end:
|
|
if end:
|
|
kwargs["end"] = end.date
|
|
kwargs["end"] = end.date
|
|
else:
|
|
else:
|
|
- length = Regle.objects.average_period(user, monthes=monthes)
|
|
|
|
|
|
+ length = get_average_period_from_today(user, monthes, today)
|
|
if length is not None:
|
|
if length is not None:
|
|
- kwargs["length"] = int(length)
|
|
|
|
|
|
+ kwargs["length"] = round(length)
|
|
else:
|
|
else:
|
|
return None
|
|
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)
|
|
return cls.create(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
@classmethod
|
|
- def from_date_range(cls, user, start, end):
|
|
|
|
|
|
+ def _from_date_range(cls, user, start, end):
|
|
if start > end: start, end = end, start
|
|
if start > end: start, end = end, start
|
|
- cycles = Cycles(start=start, end=end)
|
|
|
|
|
|
+ cycles = Cycles(user, start=start, end=end)
|
|
curr = start
|
|
curr = start
|
|
- while curr <= end:
|
|
|
|
|
|
+ while curr <= end+datetime.timedelta(days=1):
|
|
cycle = cls.from_day(user, curr)
|
|
cycle = cls.from_day(user, curr)
|
|
if cycle is None:
|
|
if cycle is None:
|
|
break
|
|
break
|
|
cycles.append(cycle)
|
|
cycles.append(cycle)
|
|
- curr = cycle.end + datetime.timedelta(days=1)
|
|
|
|
|
|
+ curr = cycle.end + datetime.timedelta(days=1)
|
|
|
|
|
|
return cycles
|
|
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
|
|
@classmethod
|
|
def from_month(cls, user, mont_or_date, year):
|
|
def from_month(cls, user, mont_or_date, year):
|
|
if isinstance(mont_or_date, str):
|
|
if isinstance(mont_or_date, str):
|