imip-agent

imipweb/data.py

538:72d9432ba43b
2015-05-17 Paul Boddie Fixed end dates broken in recent period-related changes, removing implicit end date adjustment when converting between period types.
     1 #!/usr/bin/env python     2      3 """     4 Web interface data abstractions.     5      6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from datetime import datetime    23 from imiptools.data import get_tzid    24 from imiptools.dates import end_date_to_calendar, \    25                             format_datetime, get_datetime, get_end_of_day, \    26                             to_date    27 from imiptools.period import Period    28     29 class PeriodError(Exception):    30     pass    31     32 class EventPeriod(Period):    33     34     """    35     A simple period plus attribute details, compatible with RecurringPeriod, and    36     intended to represent information obtained from an iCalendar resource.    37     """    38     39     def __init__(self, start, end, start_attr=None, end_attr=None, form_start=None, form_end=None, origin=None):    40     41         """    42         Initialise a period with the given 'start' and 'end' datetimes, together    43         with optional 'start_attr' and 'end_attr' metadata, 'form_start' and    44         'form_end' values provided as textual input, and with an optional    45         'origin' indicating the kind of period this object describes.    46         """    47     48         Period.__init__(self, start, end, origin)    49         self.start_attr = start_attr    50         self.end_attr = end_attr    51         self.form_start = form_start    52         self.form_end = form_end    53     54     def as_tuple(self):    55         return self.start, self.end, self.start_attr, self.end_attr, self.form_start, self.form_end, self.origin    56     57     def __repr__(self):    58         return "EventPeriod(%r, %r, %r, %r, %r, %r, %r)" % self.as_tuple()    59     60     def as_event_period(self):    61         return self    62     63     # Period data methods.    64     65     def get_calendar_end(self):    66         return end_date_to_calendar(Period.get_end(self))    67     68     def get_tzid(self):    69         return get_tzid(self.start_attr, self.end_attr)    70     71     def get_start_item(self):    72         return self.start, self.start_attr    73     74     def get_end_item(self):    75         return self.end, self.end_attr    76     77     # Form data compatibility methods.    78     79     def get_form_start(self):    80         if not self.form_start:    81             self.form_start = self.get_form_date(self.get_start(), self.start_attr)    82         return self.form_start    83     84     def get_form_end(self):    85         if not self.form_end:    86             self.form_end = self.get_form_date(self.get_end(), self.end_attr)    87         return self.form_end    88     89     def as_form_period(self):    90         return FormPeriod(    91             self.get_form_start(),    92             self.get_form_end(),    93             isinstance(self.end, datetime) or self.get_start() != self.get_end(),    94             isinstance(self.start, datetime) or isinstance(self.end, datetime),    95             self.origin    96             )    97     98     def get_form_date(self, dt, attr=None):    99         return FormDate(   100             format_datetime(to_date(dt)),   101             isinstance(dt, datetime) and str(dt.hour) or None,   102             isinstance(dt, datetime) and str(dt.minute) or None,   103             isinstance(dt, datetime) and str(dt.second) or None,   104             attr and attr.get("TZID") or None,   105             dt, attr   106             )   107    108 class FormPeriod:   109    110     "A period whose information originates from a form."   111    112     def __init__(self, start, end, end_enabled=True, times_enabled=True, origin=None):   113         self.start = start   114         self.end = end   115         self.end_enabled = end_enabled   116         self.times_enabled = times_enabled   117         self.origin = origin   118    119     def as_tuple(self):   120         return self.start, self.end, self.end_enabled, self.times_enabled, self.origin   121    122     def __repr__(self):   123         return "FormPeriod(%r, %r, %r, %r, %r)" % self.as_tuple()   124    125     def _get_start(self):   126         return self.start.as_datetime(self.times_enabled), self.start.get_attributes(self.times_enabled)   127    128     def _get_end(self):   129    130         # Handle specified end datetimes.   131    132         if self.end_enabled:   133             dtend = self.end.as_datetime(self.times_enabled)   134             dtend_attr = self.end.get_attributes(self.times_enabled)   135             if not dtend:   136                 return None, None   137    138         # Otherwise, treat the end date as the start date. Datetimes are   139         # handled by making the event occupy the rest of the day.   140    141         else:   142             dtstart, dtstart_attr = self._get_start()   143             if dtstart:   144                 dtend_attr = dtstart_attr   145    146                 if isinstance(dtstart, datetime):   147                     dtend = get_end_of_day(dtstart, dtstart_attr["TZID"])   148                 else:   149                     dtend = dtstart   150             else:   151                 return None, None   152    153         return dtend, dtend_attr   154    155     def as_event_period(self, index=None):   156    157         """   158         Return a converted version of this object as an event period suitable   159         for iCalendar usage. If 'index' is indicated, include it in any error   160         raised in the conversion process.   161         """   162    163         dtstart, dtstart_attr = self._get_start()   164         if not dtstart:   165             raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"])   166    167         dtend, dtend_attr = self._get_end()   168         if not dtend:   169             raise PeriodError(*[index is not None and ("dtend", index) or "dtend"])   170    171         if dtstart > dtend:   172             raise PeriodError(*[   173                 index is not None and ("dtstart", index) or "dtstart",   174                 index is not None and ("dtend", index) or "dtend"   175                 ])   176    177         return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, self.start, self.end, self.origin)   178    179     # Period data methods.   180    181     def get_start(self):   182         dtstart, dtstart_attr = self._get_start()   183         return dtstart   184    185     def get_end(self):   186         dtend, dtend_attr = self._get_end()   187         return dtend   188    189     def get_start_item(self):   190         return self._get_start()   191    192     def get_end_item(self):   193         return self._get_end()   194    195     # Form data methods.   196    197     def get_form_start(self):   198         return self.start   199    200     def get_form_end(self):   201         return self.end   202    203     def as_form_period(self):   204         return self   205    206 class FormDate:   207    208     "Date information originating from form information."   209    210     def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None):   211         self.date = date   212         self.hour = hour   213         self.minute = minute   214         self.second = second   215         self.tzid = tzid   216         self.dt = dt   217         self.attr = attr   218    219     def as_tuple(self):   220         return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr   221    222     def __repr__(self):   223         return "FormDate(%r, %r, %r, %r, %r, %r, %r)" % self.as_tuple()   224    225     def get_component(self, value):   226         return (value or "").rjust(2, "0")[:2]   227    228     def get_hour(self):   229         return self.get_component(self.hour)   230    231     def get_minute(self):   232         return self.get_component(self.minute)   233    234     def get_second(self):   235         return self.get_component(self.second)   236    237     def get_date_string(self):   238         return self.date or ""   239    240     def get_datetime_string(self):   241         if not self.date:   242             return ""   243    244         hour = self.hour; minute = self.minute; second = self.second   245    246         if hour or minute or second:   247             time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second)))   248         else:   249             time = ""   250                251         return "%s%s" % (self.date, time)   252    253     def get_tzid(self):   254         return self.tzid   255    256     def as_datetime(self, with_time=True):   257    258         "Return a datetime for this object."   259    260         # Return any original datetime details.   261    262         if self.dt:   263             return self.dt   264    265         # Otherwise, construct a datetime.   266    267         s, attr = self.as_datetime_item(with_time)   268         if s:   269             return get_datetime(s, attr)   270         else:   271             return None   272    273     def as_datetime_item(self, with_time=True):   274    275         """   276         Return a (datetime string, attr) tuple for the datetime information   277         provided by this object, where both tuple elements will be None if no   278         suitable date or datetime information exists.   279         """   280    281         s = None   282         if with_time:   283             s = self.get_datetime_string()   284             attr = self.get_attributes(True)   285         if not s:   286             s = self.get_date_string()   287             attr = self.get_attributes(False)   288         if not s:   289             return None, None   290         return s, attr   291    292     def get_attributes(self, with_time=True):   293    294         "Return attributes for the date or datetime represented by this object."   295    296         if with_time:   297             return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"}   298         else:   299             return {"VALUE" : "DATE"}   300    301 def event_period_from_period(period):   302     if isinstance(period, EventPeriod):   303         return period   304     elif isinstance(period, FormPeriod):   305         return period.as_event_period()   306     else:   307         dtstart, dtstart_attr = period.get_start_item()   308         dtend, dtend_attr = period.get_end_item()   309         return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, origin=period.origin)   310    311 def form_period_from_period(period):   312     if isinstance(period, EventPeriod):   313         return period.as_form_period()   314     elif isinstance(period, FormPeriod):   315         return period   316     else:   317         return event_period_from_period(period).as_form_period()   318    319 # vim: tabstop=4 expandtab shiftwidth=4