123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- import datetime
- from functools import partial
- 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, 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:
- if cycle.end < start: continue
- 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, user):
- self.user = user
- self.start : datetime.date = None
- self.end : datetime.date = None
- self.length : int = None
- self.min_cycle : int = None
- self.max_cycle : int = None
- self.fertility_start_offset : int = None
- self.fertility_start : datetime.date = None
- self.fertility_end_offset : int = None
- 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)]
- else:
- ret = [[] for _ in range(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):
- 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__()
- @property
- def json(self):
- return {
- "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,
- "fertility_offset" : [self.fertility_start_offset, self.fertility_end_offset],
- }
- def _init(self):
- assert self.start
- if not self.end:
- assert self.length
- self.end = self.start + datetime.timedelta(days=self.length)
- elif not self.length:
- assert self.end
- self.length = (self.end - self.start).days
- else:
- return
- self._fertility_init()
- if self.fertility_start_offset is not None:
- self.fertility_start = self.start + datetime.timedelta(days=self.fertility_start_offset-1)
- if self.fertility_end_offset is not None:
- self.fertility_end = self.start + datetime.timedelta(days=self.fertility_end_offset-1)
- 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 (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, simulated=False):
- filter = partial(Regle.objects.filter, user=user)
- kwargs = {
- "date__lte": start
- }
- if monthes != 0:
- monthes = monthes if monthes is not None else get_average_month(user)
- kwargs["date__gte"] = start - datetime.timedelta(days=monthes*30)
- regles = list(filter(**kwargs ).order_by("date"))
- periods = Regle.objects.rgeles_to_period(regles, min_period=get_min_period(user), max_period=get_max_period(user))
- mini = None
- maxi = None
- if periods:
- mini = min(periods)
- maxi = max(periods)
- cycle = cls(user)
- cycle.start = start
- if end:
- cycle.end = end
- elif length:
- cycle.length = length
- else:
- raise ValueError
- 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=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}
- if ref_date and ref_date > today:
- length = get_average_period_from_today(user, monthes, today)
- if length is None:
- return None
- ilength = round(length)
- offset = int((date - regle.date).days / ilength) * 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 = get_average_period_from_today(user, monthes, today)
- if length is not None:
- kwargs["length"] = round(length)
- 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
- if "start" not in kwargs:
- return None
- return cls.create(**kwargs)
- @classmethod
- def _from_date_range(cls, user, start, end):
- if start > end: start, end = end, start
- cycles = Cycles(user, start=start, end=end)
- curr = start
- 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)
- 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):
- mont_or_date = parse_date(mont_or_date)
- if isinstance(mont_or_date, (datetime.date, datetime.datetime)):
- mont_or_date, year = mont_or_date.month, mont_or_date.year
- assert isinstance(mont_or_date, int) and isinstance(year, int)
- if year < 100: year += 2000
- assert 0 < mont_or_date <= 12
- start = datetime.date(year, mont_or_date, 1)
- end = start + datetime.timedelta(days=32)
- end = end - datetime.timedelta(days=end.day)
- return cls.from_date_range(user, start, end)
- class RegleManager(models.Manager):
- def last_year(self, user):
- return self.filter(user=user, date__gte=datetime.date.today() - datetime.timedelta(days=-395))
- def create(self, user, date=None):
- date = parse_date(date)
- if date is None:
- date = datetime.date.today()
- return super().create(user=user, date=date)
- def last_regle_before(self, user, date=None):
- if date is None: date = datetime.date.today()
- regle = self.filter(user=user, date__lte=date).order_by("-date")[:1]
- regle = list(regle)
- return regle[0] if regle else None
- def average_period(self, user, monthes=None, date_min=None, date_max=None):
- kwargs = {"user": user}
- if date_min or date_max:
- if date_min: kwargs["date__gte"] = date_min
- if date_max: kwargs["date__lte"] = date_max
- elif monthes:
- kwargs["date__gte"] = datetime.date.today() - datetime.timedelta(days=monthes*30)
- regles = self.filter(**kwargs).order_by("date")
- data = self.rgeles_to_period(regles)
- if data:
- return sum(data) / len(data)
- return None
- @classmethod
- def rgeles_to_period(cls, data, start_date=False, min_period=None, max_period=None):
- data = iter(data)
- periods = []
- try:
- last = next(data)
- while True:
- curr = next(data)
- dt = curr.date - last.date
- if start_date:
- dt = (last.date, dt)
- if (min_period is not None and dt.days<min_period or
- max_period is not None and dt.days>max_period):
- last=curr
- continue
- periods.append(dt.days)
- last = curr
- except StopIteration:
- pass
- return periods
- def next_regle_after(self, user, date):
- regle = self.filter(user=user, date__gt=date).order_by("date")[:1]
- regle = list(regle)
- return regle[0] if regle else None
- class Regle(models.Model):
- objects = RegleManager()
- user = models.ForeignKey(User, on_delete=models.CASCADE)
- date = models.DateField()
- DoesNotExist : Exception
- class Meta:
- unique_together = ["user", "date"]
- def __repr__(self):
- return f"<Regle {self.user.username} {self.date}>"
- def __str__(self):
- return self.__repr__()
|