|
| 1 | +"""Handlers for specific details of events.""" |
| 2 | + |
| 3 | +from collections import OrderedDict |
| 4 | +from datetime import datetime |
| 5 | +from itertools import chain |
| 6 | +from typing import List |
| 7 | + |
| 8 | +from dateutil.parser import isoparse, parse |
| 9 | + |
| 10 | +from gcalcli.exceptions import ReadonlyError, ReadonlyCheckError |
| 11 | +from gcalcli.utils import is_all_day |
| 12 | + |
| 13 | +FMT_DATE = '%Y-%m-%d' |
| 14 | +FMT_TIME = '%H:%M' |
| 15 | +TODAY = datetime.now().date() |
| 16 | + |
| 17 | +URL_PROPS = OrderedDict([('html_link', 'htmlLink'), |
| 18 | + ('hangout_link', 'hangoutLink')]) |
| 19 | +ENTRY_POINT_PROPS = OrderedDict([('conference_entry_point_type', |
| 20 | + 'entryPointType'), |
| 21 | + ('conference_uri', 'uri')]) |
| 22 | + |
| 23 | + |
| 24 | +def _valid_title(event): |
| 25 | + if 'summary' in event and event['summary'].strip(): |
| 26 | + return event['summary'] |
| 27 | + else: |
| 28 | + return '(No title)' |
| 29 | + |
| 30 | + |
| 31 | +class Handler: |
| 32 | + """Handler for a specific detail of an event.""" |
| 33 | + |
| 34 | + # list of strings for fieldnames provided by this object |
| 35 | + # XXX: py36+: change to `fieldnames: List[str]` |
| 36 | + fieldnames = [] # type: List[str] |
| 37 | + |
| 38 | + @classmethod |
| 39 | + def get(cls, event): |
| 40 | + """Return simple string representation for columnar output.""" |
| 41 | + raise NotImplementedError |
| 42 | + |
| 43 | + @classmethod |
| 44 | + def patch(cls, cal, event, fieldname, value): |
| 45 | + """Patch event from value.""" |
| 46 | + raise NotImplementedError |
| 47 | + |
| 48 | + |
| 49 | +class SingleFieldHandler(Handler): |
| 50 | + """Handler for a detail that only produces one column.""" |
| 51 | + |
| 52 | + @classmethod |
| 53 | + def get(cls, event): |
| 54 | + return [cls._get(event).strip()] |
| 55 | + |
| 56 | + @classmethod |
| 57 | + def patch(cls, cal, event, fieldname, value): |
| 58 | + return cls._patch(event, value) |
| 59 | + |
| 60 | + |
| 61 | +class SimpleSingleFieldHandler(SingleFieldHandler): |
| 62 | + """Handler for single-string details that require no special processing.""" |
| 63 | + |
| 64 | + @classmethod |
| 65 | + def _get(cls, event): |
| 66 | + return event.get(cls.fieldnames[0], '') |
| 67 | + |
| 68 | + @classmethod |
| 69 | + def _patch(cls, event, value): |
| 70 | + event[cls.fieldnames[0]] = value |
| 71 | + |
| 72 | + |
| 73 | +class Time(Handler): |
| 74 | + """Handler for dates and times.""" |
| 75 | + |
| 76 | + fieldnames = ['start_date', 'start_time', 'end_date', 'end_time'] |
| 77 | + |
| 78 | + @classmethod |
| 79 | + def _datetime_to_fields(cls, instant, all_day): |
| 80 | + instant_date = instant.strftime(FMT_DATE) |
| 81 | + |
| 82 | + if all_day: |
| 83 | + instant_time = '' |
| 84 | + else: |
| 85 | + instant_time = instant.strftime(FMT_TIME) |
| 86 | + |
| 87 | + return [instant_date, instant_time] |
| 88 | + |
| 89 | + @classmethod |
| 90 | + def get(cls, event): |
| 91 | + all_day = is_all_day(event) |
| 92 | + |
| 93 | + start_fields = cls._datetime_to_fields(event['s'], all_day) |
| 94 | + end_fields = cls._datetime_to_fields(event['e'], all_day) |
| 95 | + |
| 96 | + return start_fields + end_fields |
| 97 | + |
| 98 | + @classmethod |
| 99 | + def patch(cls, cal, event, fieldname, value): |
| 100 | + instant_name, _, unit = fieldname.partition('_') |
| 101 | + |
| 102 | + assert instant_name in {'start', 'end'} |
| 103 | + |
| 104 | + if unit == 'date': |
| 105 | + instant = event[instant_name] = {} |
| 106 | + instant_date = parse(value, default=TODAY) |
| 107 | + |
| 108 | + instant['date'] = instant_date.isoformat() |
| 109 | + instant['dateTime'] = None # clear any previous non-all-day time |
| 110 | + else: |
| 111 | + assert unit == 'time' |
| 112 | + |
| 113 | + # If the time field is empty, do nothing. |
| 114 | + # This enables all day events. |
| 115 | + if not value.strip(): |
| 116 | + return |
| 117 | + |
| 118 | + # date must be an earlier TSV field than time |
| 119 | + instant = event[instant_name] |
| 120 | + instant_date = isoparse(instant['date']) |
| 121 | + instant_datetime = parse(value, default=instant_date) |
| 122 | + |
| 123 | + instant['date'] = None # clear all-day date |
| 124 | + instant['dateTime'] = instant_datetime.isoformat() |
| 125 | + instant['timeZone'] = cal['timeZone'] |
| 126 | + |
| 127 | + |
| 128 | +class Url(Handler): |
| 129 | + """Handler for HTML and legacy Hangout links.""" |
| 130 | + |
| 131 | + fieldnames = list(URL_PROPS.keys()) |
| 132 | + |
| 133 | + @classmethod |
| 134 | + def get(cls, event): |
| 135 | + return [event.get(prop, '') for prop in URL_PROPS.values()] |
| 136 | + |
| 137 | + @classmethod |
| 138 | + def patch(cls, cal, event, fieldname, value): |
| 139 | + if fieldname == 'html_link': |
| 140 | + raise ReadonlyError(fieldname, |
| 141 | + 'It is not possible to verify that the value ' |
| 142 | + 'has not changed. ' |
| 143 | + 'Remove it from the input.') |
| 144 | + |
| 145 | + prop = URL_PROPS[fieldname] |
| 146 | + |
| 147 | + # Fail if the current value doesn't |
| 148 | + # match the desired patch. This requires an additional API query for |
| 149 | + # each row, so best to avoid attempting to update these fields. |
| 150 | + |
| 151 | + curr_value = event.get(prop, '') |
| 152 | + |
| 153 | + if curr_value != value: |
| 154 | + raise ReadonlyCheckError(fieldname, curr_value, value) |
| 155 | + |
| 156 | + |
| 157 | +class Conference(Handler): |
| 158 | + """Handler for videoconference and teleconference details.""" |
| 159 | + |
| 160 | + fieldnames = list(ENTRY_POINT_PROPS.keys()) |
| 161 | + |
| 162 | + @classmethod |
| 163 | + def get(cls, event): |
| 164 | + if 'conferenceData' not in event: |
| 165 | + return ['', ''] |
| 166 | + |
| 167 | + data = event['conferenceData'] |
| 168 | + |
| 169 | + # only display first entry point for TSV |
| 170 | + # https://github.com/insanum/gcalcli/issues/533 |
| 171 | + entry_point = data['entryPoints'][0] |
| 172 | + |
| 173 | + return [entry_point.get(prop, '') |
| 174 | + for prop in ENTRY_POINT_PROPS.values()] |
| 175 | + |
| 176 | + @classmethod |
| 177 | + def patch(cls, cal, event, fieldname, value): |
| 178 | + if not value: |
| 179 | + return |
| 180 | + |
| 181 | + prop = ENTRY_POINT_PROPS[fieldname] |
| 182 | + |
| 183 | + data = event.setdefault('conferenceData', {}) |
| 184 | + entry_points = data.setdefault('entryPoints', []) |
| 185 | + if not entry_points: |
| 186 | + entry_points.append({}) |
| 187 | + |
| 188 | + entry_point = entry_points[0] |
| 189 | + entry_point[prop] = value |
| 190 | + |
| 191 | + |
| 192 | +class Title(SingleFieldHandler): |
| 193 | + """Handler for title.""" |
| 194 | + |
| 195 | + fieldnames = ['title'] |
| 196 | + |
| 197 | + @classmethod |
| 198 | + def _get(cls, event): |
| 199 | + return _valid_title(event) |
| 200 | + |
| 201 | + @classmethod |
| 202 | + def _patch(cls, event, value): |
| 203 | + event['summary'] = value |
| 204 | + |
| 205 | + |
| 206 | +class Location(SimpleSingleFieldHandler): |
| 207 | + """Handler for location.""" |
| 208 | + |
| 209 | + fieldnames = ['location'] |
| 210 | + |
| 211 | + |
| 212 | +class Description(SimpleSingleFieldHandler): |
| 213 | + """Handler for description.""" |
| 214 | + |
| 215 | + fieldnames = ['description'] |
| 216 | + |
| 217 | + |
| 218 | +class Calendar(SingleFieldHandler): |
| 219 | + """Handler for calendar.""" |
| 220 | + |
| 221 | + fieldnames = ['calendar'] |
| 222 | + |
| 223 | + @classmethod |
| 224 | + def _get(cls, event): |
| 225 | + return event['gcalcli_cal']['summary'] |
| 226 | + |
| 227 | + @classmethod |
| 228 | + def patch(cls, cal, event, fieldname, value): |
| 229 | + curr_value = cal['summary'] |
| 230 | + |
| 231 | + if curr_value != value: |
| 232 | + raise ReadonlyCheckError(fieldname, curr_value, value) |
| 233 | + |
| 234 | + |
| 235 | +class Email(SingleFieldHandler): |
| 236 | + """Handler for emails.""" |
| 237 | + |
| 238 | + fieldnames = ['email'] |
| 239 | + |
| 240 | + @classmethod |
| 241 | + def _get(cls, event): |
| 242 | + return event['creator'].get('email', '') |
| 243 | + |
| 244 | + |
| 245 | +class ID(SimpleSingleFieldHandler): |
| 246 | + """Handler for event ID.""" |
| 247 | + |
| 248 | + fieldnames = ['id'] |
| 249 | + |
| 250 | + |
| 251 | +HANDLERS = OrderedDict([('id', ID), |
| 252 | + ('time', Time), |
| 253 | + ('url', Url), |
| 254 | + ('conference', Conference), |
| 255 | + ('title', Title), |
| 256 | + ('location', Location), |
| 257 | + ('description', Description), |
| 258 | + ('calendar', Calendar), |
| 259 | + ('email', Email)]) |
| 260 | +HANDLERS_READONLY = {Url, Calendar} |
| 261 | + |
| 262 | +FIELD_HANDLERS = dict(chain.from_iterable( |
| 263 | + (((fieldname, handler) |
| 264 | + for fieldname in handler.fieldnames) |
| 265 | + for handler in HANDLERS.values()))) |
| 266 | + |
| 267 | +FIELDNAMES_READONLY = frozenset(fieldname |
| 268 | + for fieldname, handler |
| 269 | + in FIELD_HANDLERS.items() |
| 270 | + if handler in HANDLERS_READONLY) |
| 271 | + |
| 272 | +_DETAILS_WITHOUT_HANDLERS = ['length', 'reminders', 'attendees', |
| 273 | + 'attachments', 'end'] |
| 274 | + |
| 275 | +DETAILS = list(HANDLERS.keys()) + _DETAILS_WITHOUT_HANDLERS |
| 276 | +DETAILS_DEFAULT = {'time', 'title'} |
0 commit comments