Example #1
0
class Event(MailSyncBase, HasRevisions, HasPublicID, UpdatedAtMixin,
            DeletedAtMixin):
    """Data for events."""
    API_OBJECT_NAME = 'event'
    API_MODIFIABLE_FIELDS = [
        'title', 'description', 'location', 'when', 'participants', 'busy'
    ]

    namespace_id = Column(ForeignKey(Namespace.id, ondelete='CASCADE'),
                          nullable=False)

    namespace = relationship(Namespace, load_on_pending=True)

    calendar_id = Column(ForeignKey(Calendar.id, ondelete='CASCADE'),
                         nullable=False)
    # Note that we configure a delete cascade, rather than
    # passive_deletes=True, in order to ensure that delete revisions are
    # created for events if their parent calendar is deleted.
    calendar = relationship(Calendar,
                            backref=backref('events', cascade='delete'),
                            load_on_pending=True)

    # A server-provided unique ID.
    uid = Column(String(767, collation='ascii_general_ci'), nullable=False)

    # DEPRECATED
    # TODO(emfree): remove
    provider_name = Column(String(64), nullable=False, default='DEPRECATED')
    source = Column('source', Enum('local', 'remote'), default='local')

    raw_data = Column(Text, nullable=False)

    title = Column(String(TITLE_MAX_LEN), nullable=True)
    # The database column is named differently for legacy reasons.
    owner = Column('owner2', String(OWNER_MAX_LEN), nullable=True)

    description = Column('_description', LONGTEXT, nullable=True)
    location = Column(String(LOCATION_MAX_LEN), nullable=True)
    busy = Column(Boolean, nullable=False, default=True)
    read_only = Column(Boolean, nullable=False)
    reminders = Column(String(REMINDER_MAX_LEN), nullable=True)
    recurrence = Column(Text, nullable=True)
    start = Column(FlexibleDateTime, nullable=False)
    end = Column(FlexibleDateTime, nullable=True)
    all_day = Column(Boolean, nullable=False)
    is_owner = Column(Boolean, nullable=False, default=True)
    last_modified = Column(FlexibleDateTime, nullable=True)
    status = Column('status',
                    Enum(*EVENT_STATUSES),
                    server_default='confirmed')

    # This column is only used for events that are synced from iCalendar
    # files.
    message_id = Column(ForeignKey(Message.id, ondelete='CASCADE'),
                        nullable=True)

    message = relationship(Message,
                           backref=backref('events',
                                           order_by='Event.last_modified',
                                           cascade='all, delete-orphan'))

    __table_args__ = (Index('ix_event_ns_uid_calendar_id', 'namespace_id',
                            'uid', 'calendar_id'), )

    participants = Column(MutableList.as_mutable(BigJSON),
                          default=[],
                          nullable=True)

    # This is only used by the iCalendar invite code. The sequence number
    # stores the version number of the invite.
    sequence_number = Column(Integer, nullable=True)

    discriminator = Column('type', String(30))
    __mapper_args__ = {
        'polymorphic_on': discriminator,
        'polymorphic_identity': 'event'
    }

    @validates('reminders', 'recurrence', 'owner', 'location', 'title',
               'raw_data')
    def validate_length(self, key, value):
        max_len = _LENGTHS[key]
        return value if value is None else value[:max_len]

    @property
    def when(self):
        if self.all_day:
            # Dates are stored as DateTimes so transform to dates here.
            start = arrow.get(self.start).to('utc').date()
            end = arrow.get(self.end).to('utc').date()
            return Date(start) if start == end else DateSpan(start, end)
        else:
            start = self.start
            end = self.end
            return Time(start) if start == end else TimeSpan(start, end)

    @when.setter
    def when(self, when):
        if 'time' in when:
            self.start = self.end = time_parse(when['time'])
            self.all_day = False
        elif 'start_time' in when:
            self.start = time_parse(when['start_time'])
            self.end = time_parse(when['end_time'])
            self.all_day = False
        elif 'date' in when:
            self.start = self.end = date_parse(when['date'])
            self.all_day = True
        elif 'start_date' in when:
            self.start = date_parse(when['start_date'])
            self.end = date_parse(when['end_date'])
            self.all_day = True

    def _merge_participant_attributes(self, left, right):
        """Merge right into left. Right takes precedence unless it's null."""
        for attribute in right.keys():
            # Special cases:
            if right[attribute] is None:
                continue
            elif right[attribute] == '':
                continue
            elif right['status'] == 'noreply':
                continue
            else:
                left[attribute] = right[attribute]

        return left

    def _partial_participants_merge(self, event):
        """Merge the participants from event into self.participants.
        event always takes precedence over self, except if
        a participant in self isn't in event.

        This method is only called by the ical merging code because
        iCalendar attendance updates are partial: an RSVP reply often
        only contains the status of the person that RSVPs.
        It would be very wrong to call this method to merge, say, Google
        Events participants because they handle the merging themselves.
        """

        # We have to jump through some hoops because a participant may
        # not have an email or may not have a name, so we build a hash
        # where we can find both. Also note that we store names in the
        # hash only if the email is None.
        self_hash = {}
        for participant in self.participants:
            email = participant.get('email')
            name = participant.get('name')
            if email is not None:
                self_hash[email] = participant
            elif name is not None:
                # We have a name without an email.
                self_hash[name] = participant

        for participant in event.participants:
            email = participant.get('email')
            name = participant.get('name')

            # This is the tricky part --- we only want to store one entry per
            # participant --- we check if there's an email we already know, if
            # not we create it. Otherwise we use the name. This sorta works
            # because we're merging updates to an event and ical updates
            # always have an email address.
            # - karim
            if email is not None:
                if email in self_hash:
                    self_hash[email] =\
                        self._merge_participant_attributes(self_hash[email],
                                                           participant)
                else:
                    self_hash[email] = participant
            elif name is not None:
                if name in self_hash:
                    self_hash[name] =\
                        self._merge_participant_attributes(self_hash[name],
                                                           participant)
                else:
                    self_hash[name] = participant

        return self_hash.values()

    def update(self, event):
        if event.namespace is not None and event.namespace.id is not None:
            self.namespace_id = event.namespace.id

        if event.calendar is not None and event.calendar.id is not None:
            self.calendar_id = event.calendar.id

        if event.provider_name is not None:
            self.provider_name = event.provider_name

        self.uid = event.uid
        self.raw_data = event.raw_data
        self.title = event.title
        self.description = event.description
        self.location = event.location
        self.start = event.start
        self.end = event.end
        self.all_day = event.all_day
        self.owner = event.owner
        self.is_owner = event.is_owner
        self.read_only = event.read_only
        self.participants = event.participants
        self.busy = event.busy
        self.reminders = event.reminders
        self.recurrence = event.recurrence
        self.last_modified = event.last_modified
        self.message = event.message
        self.status = event.status

        if event.sequence_number is not None:
            self.sequence_number = event.sequence_number

    @property
    def recurring(self):
        if self.recurrence and self.recurrence != '':
            try:
                r = ast.literal_eval(self.recurrence)
                if isinstance(r, str):
                    r = [r]
                return r
            except (ValueError, SyntaxError):
                log.warn('Invalid RRULE entry for event',
                         event_id=self.id,
                         raw_rrule=self.recurrence)
                return []
        return []

    @property
    def organizer_email(self):
        # For historical reasons, the event organizer field is stored as
        # "Owner Name <*****@*****.**>".

        parsed_owner = parseaddr(self.owner)
        if len(parsed_owner) == 0:
            return None

        if parsed_owner[1] == '':
            return None

        return parsed_owner[1]

    @property
    def organizer_name(self):
        parsed_owner = parseaddr(self.owner)

        if len(parsed_owner) == 0:
            return None

        if parsed_owner[0] == '':
            return None

        return parsed_owner[0]

    @property
    def is_recurring(self):
        return self.recurrence is not None

    @property
    def length(self):
        return self.when.delta

    @property
    def cancelled(self):
        return self.status == 'cancelled'

    @cancelled.setter
    def cancelled(self, is_cancelled):
        if is_cancelled:
            self.status = 'cancelled'
        else:
            self.status = 'confirmed'

    @classmethod
    def __new__(cls, *args, **kwargs):
        # Decide whether or not to instantiate a RecurringEvent/Override
        # based on the kwargs we get.
        cls_ = cls
        recurrence = kwargs.get('recurrence')
        master_event_uid = kwargs.get('master_event_uid')
        if recurrence and master_event_uid:
            raise ValueError("Event can't have both recurrence and master UID")
        if recurrence and recurrence != '':
            cls_ = RecurringEvent
        if master_event_uid:
            cls_ = RecurringEventOverride
        return object.__new__(cls_, *args, **kwargs)

    def __init__(self, **kwargs):
        # Allow arguments for all subclasses to be passed to main constructor
        for k in kwargs.keys():
            if not hasattr(type(self), k):
                del kwargs[k]
        super(Event, self).__init__(**kwargs)
Example #2
0
class Event(MailSyncBase, HasRevisions, HasPublicID, UpdatedAtMixin,
            DeletedAtMixin):
    """Data for events."""

    API_OBJECT_NAME = "event"
    API_MODIFIABLE_FIELDS = [
        "title",
        "description",
        "location",
        "when",
        "participants",
        "busy",
    ]

    namespace_id = Column(ForeignKey(Namespace.id, ondelete="CASCADE"),
                          nullable=False)

    namespace = relationship(Namespace, load_on_pending=True)

    calendar_id = Column(ForeignKey(Calendar.id, ondelete="CASCADE"),
                         nullable=False)
    # Note that we configure a delete cascade, rather than
    # passive_deletes=True, in order to ensure that delete revisions are
    # created for events if their parent calendar is deleted.
    calendar = relationship(Calendar,
                            backref=backref("events", cascade="delete"),
                            load_on_pending=True)

    # A server-provided unique ID.
    uid = Column(String(UID_MAX_LEN, collation="ascii_general_ci"),
                 nullable=False)

    # DEPRECATED
    # TODO(emfree): remove
    provider_name = Column(String(64), nullable=False, default="DEPRECATED")
    source = Column("source", Enum("local", "remote"), default="local")

    raw_data = Column(Text, nullable=False)

    title = Column(String(TITLE_MAX_LEN), nullable=True)
    # The database column is named differently for legacy reasons.
    owner = Column("owner2", String(OWNER_MAX_LEN), nullable=True)

    description = Column("_description", LONGTEXT, nullable=True)
    location = Column(String(LOCATION_MAX_LEN), nullable=True)
    busy = Column(Boolean, nullable=False, default=True)
    read_only = Column(Boolean, nullable=False)
    reminders = Column(String(REMINDER_MAX_LEN), nullable=True)
    recurrence = Column(Text, nullable=True)
    start = Column(FlexibleDateTime, nullable=False)
    end = Column(FlexibleDateTime, nullable=True)
    all_day = Column(Boolean, nullable=False)
    is_owner = Column(Boolean, nullable=False, default=True)
    last_modified = Column(FlexibleDateTime, nullable=True)
    status = Column("status",
                    Enum(*EVENT_STATUSES),
                    server_default="confirmed")

    # This column is only used for events that are synced from iCalendar
    # files.
    message_id = Column(ForeignKey(Message.id, ondelete="CASCADE"),
                        nullable=True)

    message = relationship(
        Message,
        backref=backref("events",
                        order_by="Event.last_modified",
                        cascade="all, delete-orphan"),
    )

    __table_args__ = (Index("ix_event_ns_uid_calendar_id", "namespace_id",
                            "uid", "calendar_id"), )

    participants = Column(MutableList.as_mutable(BigJSON),
                          default=[],
                          nullable=True)

    # This is only used by the iCalendar invite code. The sequence number
    # stores the version number of the invite.
    sequence_number = Column(Integer, nullable=True)

    visibility = Column(Enum("private", "public"), nullable=True)

    discriminator = Column("type", String(30))
    __mapper_args__ = {
        "polymorphic_on": discriminator,
        "polymorphic_identity": "event"
    }

    @validates("reminders", "recurrence", "owner", "location", "title", "uid",
               "raw_data")
    def validate_length(self, key, value):
        if value is None:
            return None
        return unicode_safe_truncate(value, MAX_LENS[key])

    @property
    def when(self):
        if self.all_day:
            # Dates are stored as DateTimes so transform to dates here.
            start = arrow.get(self.start).to("utc").date()
            end = arrow.get(self.end).to("utc").date()
            return Date(start) if start == end else DateSpan(start, end)
        else:
            start = self.start
            end = self.end
            return Time(start) if start == end else TimeSpan(start, end)

    @when.setter
    def when(self, when):
        if "time" in when:
            self.start = self.end = time_parse(when["time"])
            self.all_day = False
        elif "start_time" in when:
            self.start = time_parse(when["start_time"])
            self.end = time_parse(when["end_time"])
            self.all_day = False
        elif "date" in when:
            self.start = self.end = date_parse(when["date"])
            self.all_day = True
        elif "start_date" in when:
            self.start = date_parse(when["start_date"])
            self.end = date_parse(when["end_date"])
            self.all_day = True

    def _merge_participant_attributes(self, left, right):
        """Merge right into left. Right takes precedence unless it's null."""
        for attribute in right.keys():
            # Special cases:
            if (right[attribute] is None or right[attribute] == ""
                    or right["status"] == "noreply"):
                continue

            left[attribute] = right[attribute]

        return left

    def _partial_participants_merge(self, event):
        """Merge the participants from event into self.participants.
        event always takes precedence over self, except if
        a participant in self isn't in event.

        This method is only called by the ical merging code because
        iCalendar attendance updates are partial: an RSVP reply often
        only contains the status of the person that RSVPs.
        It would be very wrong to call this method to merge, say, Google
        Events participants because they handle the merging themselves.
        """

        # We have to jump through some hoops because a participant may
        # not have an email or may not have a name, so we build a hash
        # where we can find both. Also note that we store names in the
        # hash only if the email is None.
        self_hash = {}
        for participant in self.participants:
            email = participant.get("email")
            name = participant.get("name")
            if email is not None:
                participant["email"] = participant["email"].lower()
                self_hash[email] = participant
            elif name is not None:
                # We have a name without an email.
                self_hash[name] = participant

        for participant in event.participants:
            email = participant.get("email")
            name = participant.get("name")

            # This is the tricky part --- we only want to store one entry per
            # participant --- we check if there's an email we already know, if
            # not we create it. Otherwise we use the name. This sorta works
            # because we're merging updates to an event and ical updates
            # always have an email address.
            # - karim
            if email is not None:
                participant["email"] = participant["email"].lower()
                if email in self_hash:
                    self_hash[email] = self._merge_participant_attributes(
                        self_hash[email], participant)
                else:
                    self_hash[email] = participant
            elif name is not None:
                if name in self_hash:
                    self_hash[name] = self._merge_participant_attributes(
                        self_hash[name], participant)
                else:
                    self_hash[name] = participant

        return list(self_hash.values())

    def update(self, event):
        if event.namespace is not None and event.namespace.id is not None:
            self.namespace_id = event.namespace.id

        if event.calendar is not None and event.calendar.id is not None:
            self.calendar_id = event.calendar.id

        if event.provider_name is not None:
            self.provider_name = event.provider_name

        self.uid = event.uid
        self.raw_data = event.raw_data
        self.title = event.title
        self.description = event.description
        self.location = event.location
        self.start = event.start
        self.end = event.end
        self.all_day = event.all_day
        self.owner = event.owner
        self.is_owner = event.is_owner
        self.read_only = event.read_only
        self.participants = event.participants
        self.busy = event.busy
        self.reminders = event.reminders
        self.recurrence = event.recurrence
        self.last_modified = event.last_modified
        self.message = event.message
        self.status = event.status
        self.visibility = event.visibility

        if event.sequence_number is not None:
            self.sequence_number = event.sequence_number

    @property
    def recurring(self):
        if self.recurrence and self.recurrence != "":
            try:
                r = ast.literal_eval(self.recurrence)
                if isinstance(r, str):
                    r = [r]
                return r
            except (ValueError, SyntaxError):
                log.warn(
                    "Invalid RRULE entry for event",
                    event_id=self.id,
                    raw_rrule=self.recurrence,
                )
                return []
        return []

    @property
    def organizer_email(self):
        # For historical reasons, the event organizer field is stored as
        # "Owner Name <*****@*****.**>".

        parsed_owner = parseaddr(self.owner)
        if len(parsed_owner) == 0:
            return None

        if parsed_owner[1] == "":
            return None

        return parsed_owner[1]

    @property
    def organizer_name(self):
        parsed_owner = parseaddr(self.owner)

        if len(parsed_owner) == 0:
            return None

        if parsed_owner[0] == "":
            return None

        return parsed_owner[0]

    @property
    def is_recurring(self):
        return self.recurrence is not None

    @property
    def length(self):
        return self.when.delta

    @property
    def cancelled(self):
        return self.status == "cancelled"

    @cancelled.setter
    def cancelled(self, is_cancelled):
        if is_cancelled:
            self.status = "cancelled"
        else:
            self.status = "confirmed"

    @property
    def calendar_event_link(self):
        try:
            return json.loads(self.raw_data)["htmlLink"]
        except (ValueError, KeyError):
            return

    @property
    def emails_from_description(self):
        if self.description:
            return extract_emails_from_text(self.description)
        else:
            return []

    @property
    def emails_from_title(self):
        if self.title:
            return extract_emails_from_text(self.title)
        else:
            return []

    @classmethod
    def create(cls, **kwargs):
        # Decide whether or not to instantiate a RecurringEvent/Override
        # based on the kwargs we get.
        cls_ = cls
        kwargs["__event_created_sanely"] = _EVENT_CREATED_SANELY_SENTINEL
        recurrence = kwargs.get("recurrence")
        master_event_uid = kwargs.get("master_event_uid")
        if recurrence and master_event_uid:
            raise ValueError("Event can't have both recurrence and master UID")
        if recurrence and recurrence != "":
            cls_ = RecurringEvent
        if master_event_uid:
            cls_ = RecurringEventOverride
        return cls_(**kwargs)

    def __init__(self, **kwargs):
        if (not kwargs.pop("__event_created_sanely", None) is
                _EVENT_CREATED_SANELY_SENTINEL):
            raise AssertionError(
                "Use Event.create with appropriate keyword args "
                "instead of constructing Event, RecurringEvent or RecurringEventOverride "
                "directly")

        # Allow arguments for all subclasses to be passed to main constructor
        for k in list(kwargs):
            if not hasattr(type(self), k):
                del kwargs[k]
        super(Event, self).__init__(**kwargs)