Commits

Andy Mikhailenko  committed 67075e0

ext.events: Removed model HierarchicalPlan; improved model Plan; added command list_plans.

  • Participants
  • Parent commits a4f4a6d

Comments (0)

Files changed (4)

File orgtool/ext/events/__init__.py

 ecosystem. *Event* is the core model for messages, payments, etc.
 """
 from schema import *
-from commands import list_events
+from commands import available_commands
 import admin
 
 
     """Provides a simple CLI for event management.
     """
     assert not conf
-    app.cli_parser.add_commands([list_events], namespace='events')
+    app.cli_parser.add_commands(available_commands, namespace='events')

File orgtool/ext/events/admin.py

 class PlanAdmin(TrackedAdmin):
     namespace = NAMESPACE
     #exclude = ['dates_rrule', 'next_date_time'] + TrackedAdmin.exclude
-    exclude = ['dates_rrule'] + TrackedAdmin.exclude
+    exclude = TrackedAdmin.exclude
     list_names = 'summary', 'is_accomplished'
     search_names = ['summary']
 
     pass
 
 
-@admin.register_for(HierarchicalPlan)
-class HierarchicalPlanAdmin(PlanAdmin):
-    pass
+#@admin.register_for(HierarchicalPlan)
+#class HierarchicalPlanAdmin(PlanAdmin):
+#    pass
 
 
 admin.register(PlanCategory, namespace=NAMESPACE)

File orgtool/ext/events/commands.py

 from argh import alias, arg, command
 from tool import app
 
-from .schema import Event
+from .schema import Event, Plan
 
 
+def _get_plans(query=None):
+    db = app.get_feature('document_storage').default_db
+    plans = Plan.objects(db)
+    if query:
+        plans = plans.where(summary__matches_caseless=query)
+    return plans
+
 @command
 @alias('ls')
-def list_events(query=None, format=u'{date_time} {summary}', date=None):
+def list_events(query=None, plan=None, format=u'{date_time} {summary}', date=None):
     "Displays a list of matching events."
     db = app.get_feature('document_storage').default_db
     events = Event.objects(db)
+    if plan:
+        plans = list(_get_plans(query=plan))
+        events = events.where(plan__contains_any=plans)
     if query:
         events = events.where(summary__matches_caseless=query)
     for event in events:
         yield format.format(**event)
+
+@alias('lsp')
+@arg('-q', '--query')
+def list_plans(args):
+    for p in _get_plans(args.query):
+        yield p
+
+@alias('next')
+@arg('-O', '--no-overdue')
+def list_next_plans(args):
+    pass
+
+#@alias('schedule')
+#@arg('-q', '--query')
+#def schedule
+
+def update_rrules(args):
+    """Updates expected next event dates with regard to recurrence rules and
+    current date.
+    """
+    for plan in _get_plans():
+        new_date = plan.review_next_date()
+        if new_date:
+            yield u'* updating {summary}'.format(**plan)
+    yield 'Done.'
+
+available_commands = (list_events, list_plans, update_rrules)

File orgtool/ext/events/schema.py

 # -*- coding: utf-8 -*-
 
 import datetime
-from dateutil.rrule import rrule
+from dateutil.rrule import rrule, rrulestr
 from werkzeug import cached_property
 
 from doqu import Document
+from doqu import future
+from doqu import validators
 from doqu.ext.fields import Field as f
 
 from orgtool.ext.tracking import TrackedDocument
 
-
-def informal_rrule(*args, **kwargs):
-    "A wrapper for informal_rrule that does lazy import"
-    from orgtool.utils.dates import informal_rrule
-    return informal_rrule(*args, **kwargs)
+#def informal_rrule(*args, **kwargs):
+#    "A wrapper for informal_rrule that does lazy import"
+#    from orgtool.utils.dates import informal_rrule
+#    return informal_rrule(*args, **kwargs)
 
 
 class Plan(TrackedDocument):
     details = f(unicode)
 
     next_date_time = f(datetime.datetime)#, essential=True)
+    '''
     dates_rrule_text = f(unicode)#, essential=True)
-    # TODO: dates_rrule = f(rrule, essential=True, pickled=True)
-    dates_rrule = f(rrule, pickled=True)
+    #dates_rrule = f(rrule, essential=True, pickled=True)
+    dates_rrule = f(rrule, pickled=True)  # TODO: don't pickle, store iCal
+                                          # (rrule can't be serialized to iCal
+                                          # but we don't need that -- we just
+                                          # need to somehow write iCal and only
+                                          # parse it to rrule once we need to
+                                          # generate to sequence of dates;
+                                          # rrule itself is immutable)
+                                          # ...btw, iCal partially overlaps with
+                                          # valid_since and valid_until; what
+                                          # to do?
+                                          # если выносим rrule в отдельные
+                                          # поля, надо убедиться, что rruleset
+                                          # нормально по ним распределяется.
+                                          # какая-то степень сериализации
+                                          # должна быть.
+                                          # но вообще, примеры могут быть
+                                          # настолько сложными (с лаконичной
+                                          # формулировкой в ical), что проще
+                                          # хранить именно в ical и дублировать
+                                          # нужное в доп. полях, если надо.
+    '''
+    ical_rrule = f(unicode)
 
     # accuracy = ...
 
     #repeat_month = f(int)
     #repeat_day = f(int)
     #next_occurence_date_time = f(datetime.datetime)   # FIXME occuRRence
-    is_accomplished = f(bool)#, essential=True)
+
 
     # cancelled: valid_until(cancelling date) + is_accomplished = False
 
     # "materialize" these plans as event reports.
     skip_past_events = f(bool, default=False)
 
+    is_accomplished = f(bool)#, essential=True)
+
+
     def __unicode__(self):
         return u'{summary}'.format(**self)
 
     def save(self, *args, **kwargs):
-        fields = ('dates_rrule_text', 'next_date_time',
-                  'valid_since', 'valid_until')
-        if any(self.is_field_changed(x) for x in fields):
-            self.update_dates_rrule()
+        new_estimate = self.review_next_date()
+        if new_estimate:
+            self.next_date_time = new_estimate
         return super(Plan, self).save(*args, **kwargs)
 
-    def update_dates_rrule(self, last_event=None):
+    @cached_property
+    def _first_event(self):
+        if self.events:
+            events = sorted(self.events, key=lambda x: x['date_time'])
+            return events[0]
+
+    @cached_property
+    def _last_event(self):
+        if self.events:
+            events = sorted(self.events, key=lambda x: x['date_time'],
+                            reverse=True)
+            return events[0]
+
+    def _get_start_date(self):
+        """Returns the date of the first event occurence. It is either the
+        first actual event date, or the explicitly specified date ("valid
+        since"), or current date and time (UTC).
+        """
+        if self._first_event:
+            return self._first_event.date_time
+        return self.valid_since or datetime.datetime.utcnow()
+
+    def _get_next_date(self):
+        """Returns a `datetime.datetime` object representing the next expected
+        occurrence of the planned event. If past unconfirmed events are
+        skipped, this will always be a date in the future.
+        """
+        if not self.rrule:
+            return
+        if not self.skip_past_events and self._last_event:
+            return self.rrule.after(self._last_event.date_time)
+        else:
+            return self.rrule.after(datetime.datetime.utcnow())
+
+    @property
+    def rrule(self):
+        if not self.ical_rrule:
+            return
+        start = self._get_start_date()
+        # FIXME add forceset=True to always have rruleset?
+        return rrulestr(self.ical_rrule, dtstart=start)
+        #return rrule(self.interval, count=self.repeat_count,
+        #             dtstart=self.valid_since, until=self.valid_until)
+
+    def review_next_date(self, new_event_date=None):
+        """Revises expected next occurrence date. Returns a new date if the
+        expectation has changed or `False` if last estimate remains correct.
+        Does not change or save anything.
+
+        :param new_event_date:
+
+            (optional) a `datetime.datetime` object representing a new event
+            being added. If it equals or happens later than the current
+            expectation, the expectation is shifted further.
+
+        """
+        if new_event_date:
+            if not self.next_date_time:
+                return new_event_date
+            if new_event_date <= self.next_date_time:
+                return new_event_date
+        else:
+            rrule_fields = ('ical_rrule', 'valid_since', 'valid_until')
+            if any(future.is_field_changed(self, x) for x in rrule_fields):
+                new_value = self._get_next_date()
+                if new_value != self.next_date_time:
+                    return new_value
+
+    '''
+    def OLD__update_dates_rrule(self, last_event=None):
         # NOTE: we are not trying to be too smart here, just some basic guesses
         # apart from the straightforward logic. Facts will *not* fit the plans
         # most of the time.
             if self.valid_until:
                 self.next_date_time = datetime.datetime.combine(
                     self.valid_until, datetime.time())
+    '''
 
     def is_active(self):
         if self.is_accomplished or self.is_future() or self.is_expired():
 
         .. note::
 
-            "valid_until" is interpreted as "invalid_since".
+            "valid_until" is interpreted as "invalid since".
 
         """
         today = datetime.datetime.utcnow().date()
     def __unicode__(self): return u'{summary}'.format(**self)
 class CategorizedPlan(Plan):
     category = f(PlanCategory, required=True)
+"""
 class HierarchicalPlan(Plan):
     unlocks_plan = f(Plan, essential=True)    # not 'self': any plan will do
 
         today = datetime.date.today()
         valid = self.depends_on.where_not(valid_until__lte=today)
         return valid.where(is_accomplished=False)
+"""
 
 '''
 
     def save(self, *args, **kwargs):
         # TODO: replace with signals; call this only on creation/deletion
         if self.plan:
-            self.plan.update_dates_rrule()
-            self.plan.save()
+            new_est = self.plan.review_next_date(new_event_date=self.date_time)
+            if new_est:
+                self.plan.next_date_time = new_est
+                self.plan.save()
         return super(Event, self).save(*args, **kwargs)
 
     def get_duration(self):