Exemple #1
0
class EmailContact(GravatarMixin, Contact):
    """Simple contact represented by a plain e-mail address."""

    slug = 'email'

    id = db.Column(db.Integer,
                   db.ForeignKey('contact.id', ondelete='cascade'),
                   primary_key=True)
    email = db.Column(db.String(100), nullable=False)

    __tablename__ = 'contact_' + slug

    __mapper_args__ = {
        'polymorphic_identity': slug,
    }

    def invitation_hash(self, event):
        attendances = [a for a in self.attendance if a.event == event]
        if not attendances:
            return None

        attendance = attendances[0]
        if not attendance.hash:
            attendance.generate_hash()

        return attendance.hash

    @property
    def identifier(self):
        return self.email
Exemple #2
0
class Contact(db.Model, ContactMixin):
    """Base contact model class."""

    contact_types = {
        'email': u'E-mail',
        'facebook': u'Facebook',
        'google': u'Google',
    }

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    type = db.Column(db.Enum(*contact_types.keys(), name='contact_types'),
                     nullable=False)
    user_id = db.Column(db.Integer(),
                        db.ForeignKey('user.id', ondelete='cascade'),
                        nullable=False)
    user = db.relationship('User',
                           backref=db.backref('contacts',
                                              cascade='all',
                                              lazy='dynamic'))
    belongs_to_user = db.Column(db.Boolean(), nullable=False, default=False)
    is_primary = db.Column(db.Boolean(), nullable=False, default=False)
    attendance = db.relationship('Attendance', cascade='all')

    __mapper_args__ = {
        'polymorphic_on': type,
        'with_polymorphic': '*',
    }

    def set_attendance(self, event, type):
        att = Attendance.query.filter(Attendance.contact_id == self.id)\
            .filter(Attendance.event_id == event.id)\
            .first()
        att = att or Attendance(contact=self, event=event)
        att.type = type

    def set_as_primary(self):
        primary_contact = Contact.query\
            .filter(Contact.user_id == self.user_id)\
            .filter(Contact.is_primary == True)\
            .first()
        if primary_contact:
            primary_contact.is_primary = False
        self.is_primary = True

    def __repr__(self):
        return '<%s %r (%r)>' % (self.__class__.__name__, self.name,
                                 self.identifier)

    def to_dict(self):
        return dict(id=self.id,
                    name=self.name,
                    type=self.type,
                    identifier=self.identifier,
                    avatar=self.avatar)
Exemple #3
0
class Attendance(db.Model):
    """Attendance of contacts to events."""

    types = ['going', 'maybe', 'declined', 'invited']
    types_mapping = {
        # original types
        'going': 'going',
        'maybe': 'maybe',
        'declined': 'declined',
        'invited': 'invited',

        # facebook
        'attending': 'going',
        'unsure': 'maybe',
        'not_replied': 'invited',

        # google:
        'http://schemas.google.com/g/2005#event.accepted': 'going',
        'http://schemas.google.com/g/2005#event.declined': 'declined',
        'http://schemas.google.com/g/2005#event.invited': 'invited',
        'http://schemas.google.com/g/2005#event.tentative': 'maybe',
    }

    event_id = db.Column(db.Integer,
                         db.ForeignKey('event.id', ondelete='cascade'),
                         primary_key=True)
    event = db.relationship('Event')
    contact_id = db.Column(db.Integer,
                           db.ForeignKey('contact.id', ondelete='cascade'),
                           primary_key=True)
    contact = db.relationship('Contact')
    type = db.Column(db.Enum(*types, name='attendance_types'), nullable=False)
    invitation_sent = db.Column(db.Boolean, nullable=False, default=False)
    hash = db.Column(db.String(40), nullable=True)

    def generate_hash(self):
        hash = hashlib.sha1(
            str(self.event_id) + str(self.contact_id) +
            str(uuid.uuid1())).hexdigest()
        self.hash = hash
        return hash

    @classmethod
    def fetch_by_hash_or_404(self, hash):
        return Attendance.query.filter(Attendance.hash == hash).first_or_404()

    def __repr__(self):
        return '<Attendance %r @ %r (%s)>' % (self.contact, self.event,
                                              self.type)
Exemple #4
0
class FacebookContact(Contact):
    """Facebook contact represented by a Facebook account."""

    slug = 'facebook'

    id = db.Column(db.Integer,
                   db.ForeignKey('contact.id', ondelete='cascade'),
                   primary_key=True)
    facebook_id = db.Column(db.String(100), nullable=False)
    username = db.Column(db.String(100), nullable=True)

    __tablename__ = 'contact_' + slug

    __mapper_args__ = {
        'polymorphic_identity': slug,
    }

    @property
    def identifier(self):
        return 'facebook.com/' + (self.username or self.facebook_id)

    @property
    def avatar(self):
        return 'https://graph.facebook.com/%s/picture?type=square' % self.facebook_id
Exemple #5
0
class GoogleContact(GravatarMixin, Contact):
    """Google contact represented by a Gmail and/or Google+ account."""

    slug = 'google'

    id = db.Column(db.Integer,
                   db.ForeignKey('contact.id', ondelete='cascade'),
                   primary_key=True)
    email = db.Column(db.String(100), nullable=False)

    __tablename__ = 'contact_' + slug

    __mapper_args__ = {
        'polymorphic_identity': slug,
    }

    @property
    def identifier(self):
        return self.email
Exemple #6
0
class Event(db.Model):
    """Event."""

    default_end_time_offset = 2  # in hours

    id = db.Column(db.Integer, primary_key=True)
    facebook_id = db.Column(db.String(100), nullable=True)
    google_id = db.Column(db.String(100), nullable=True)
    name = db.Column(db.String(200), nullable=False)
    description = db.Column(db.Text())
    venue = db.Column(db.String(200))
    user_id = db.Column(db.Integer(),
                        db.ForeignKey('user.id', ondelete='cascade'),
                        nullable=False)
    user = db.relationship('User',
                           backref=db.backref('events',
                                              cascade='all',
                                              lazy='dynamic'))
    attendance = db.relationship('Attendance', cascade='all')
    created_at = db.Column(db.DateTime(),
                           nullable=False,
                           default=lambda: times.now())
    updated_at = db.Column(db.DateTime(),
                           nullable=False,
                           default=lambda: times.now(),
                           onupdate=lambda: times.now())
    cancelled_at = db.Column(db.DateTime())
    starts_at = db.Column(db.DateTime())
    _ends_at = db.Column('ends_at', db.DateTime())

    __mapper_args__ = {
        'order_by': db.desc(created_at),
    }

    def set_attendance(self, contact, type):
        att = Attendance.query.filter(Attendance.contact_id == contact.id)\
            .filter(Attendance.event_id == self.id)\
            .first()
        if not att:
            att = Attendance(contact=contact, event=self)
            att.type = type
            db.session.add(att)
        else:
            att.type = type

    def set_invitation_sent(self, contact):
        att = Attendance.query.filter(Attendance.contact_id == contact.id)\
            .filter(Attendance.event_id == self.id)\
            .first()
        if att:
            att.invitation_sent = True

    @property
    def contacts(self):
        return Contact.query.join(Attendance)\
            .filter(Attendance.event_id == self.id)

    def is_facebook_involved(self):
        fb_contacts = list(FacebookContact.query.join(Attendance)\
            .filter(Attendance.event_id == self.id))
        return bool(fb_contacts or self.facebook_id)

    def is_google_involved(self):
        g_contacts = list(GoogleContact.query.join(Attendance)\
            .filter(Attendance.event_id == self.id))
        return bool(g_contacts or self.google_id)

    def is_email_involved(self):
        return bool(list(EmailContact.query.join(Attendance)\
            .filter(Attendance.event_id == self.id)))

    @property
    def contacts_facebook_to_invite(self):
        return FacebookContact.query.join(Attendance)\
            .filter(Attendance.event_id == self.id)\
            .filter(FacebookContact.belongs_to_user == False)\
            .filter(Attendance.invitation_sent == False)

    @property
    def contacts_google_to_invite(self):
        return GoogleContact.query.join(Attendance)\
            .filter(Attendance.event_id == self.id)\
            .filter(GoogleContact.belongs_to_user == False)\
            .filter(Attendance.invitation_sent == False)

    @property
    def contacts_email_to_invite(self):
        return EmailContact.query.join(Attendance)\
            .filter(Attendance.event_id == self.id)\
            .filter(EmailContact.belongs_to_user == False)\
            .filter(Attendance.invitation_sent == False)

    @property
    def contacts_invited(self):
        return self.contacts.filter(Attendance.type == 'invited')

    @contacts_invited.setter
    def contacts_invited(self, contacts):
        for contact in contacts:
            self.set_attendance(contact, 'invited')

    @property
    def contacts_invited_ids_str(self):
        return ','.join([c.id for c in self.contacts_invited])

    @contacts_invited_ids_str.setter
    def contacts_invited_ids_str(self, ids_str):
        ids = set(filter(None, ids_str.split(',')))
        self.contacts_invited = list(Contact.query.filter(Contact.id.in_(ids)))

    @property
    def contacts_going(self):
        return self.contacts.filter(Attendance.type == 'going')

    @property
    def contacts_maybe(self):
        return self.contacts.filter(Attendance.type == 'maybe')

    @property
    def contacts_declined(self):
        return self.contacts.filter(Attendance.type == 'declined')

    @property
    def is_cancelled(self):
        return self.cancelled_at is not None

    @property
    def ends_at(self):
        if self._ends_at:
            return self._ends_at
        if self.starts_at:
            return (self.starts_at +
                    datetime.timedelta(hours=self.default_end_time_offset))
        return None

    @ends_at.setter
    def ends_at(self, value):
        self._ends_at = value

    @property
    def verbose_name(self):
        verbose_name = self.name
        if self.is_cancelled:
            verbose_name += ' (cancelled)'
        return verbose_name

    @classmethod
    def fetch_or_404(self, id):
        """Returns event by given ID or aborts the request."""
        return Event.query.filter(Event.id == id).first_or_404()

    def __repr__(self):
        return '<Event %r>' % self.name
Exemple #7
0
class User(db.Model, UserMixin, GravatarMixin):
    """User model class."""

    id = db.Column(db.Integer, primary_key=True)
    _name = db.Column('name', db.String(100), nullable=False)
    password_hash = db.Column(db.String(40), nullable=False)
    password_salt = db.Column(db.String(10), nullable=False)
    timezone = db.Column(db.String(100),
                         nullable=False,
                         default=app.config['DEFAULT_TIMEZONE'])

    def _get_contact(self, type):
        return self._get_contacts(type).first()

    def _get_contacts(self, type):
        try:
            return self.contacts.filter(Contact.type == type)\
                .filter(Contact.belongs_to_user == True)\
                .order_by(db.desc(Contact.is_primary))
        except sqlalchemy.orm.exc.DetachedInstanceError:
            return None

    def _set_contact(self, type, field_name, field_value, as_primary=False):
        # no fun when name is missing
        if not self.name:
            raise ValueError("Can't create dependent contacts without name.")

        cls = _contact_type_factory(type)
        try:
            contact = cls.query.filter(cls.belongs_to_user == True)\
                .filter(cls.user == self)\
                .filter(cls.type == type)\
                .filter_by(**{field_name: field_value})\
                .first()
        except sqlalchemy.orm.exc.DetachedInstanceError:
            contact = None

        if not contact:
            contact = cls()
            contact.type = type
            contact.user = self
            setattr(contact, field_name, field_value)

        contact.name = self.name
        contact.belongs_to_user = True

        if as_primary:
            contact.set_as_primary()

    @property
    def email(self):
        return getattr(self._get_contact('email'), 'email', None)

    @property
    def emails(self):
        return [contact.email for contact in self._get_contacts('email')]

    @email.setter
    def email(self, value):
        self._set_contact('email', field_name='email', field_value=value)

    @property
    def primary_email(self):
        return self.email

    @primary_email.setter
    def primary_email(self, value):
        self._set_contact('email',
                          field_name='email',
                          field_value=value,
                          as_primary=True)

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value
        try:
            Contact.query.with_polymorphic(Contact)\
                .filter(Contact.belongs_to_user == True)\
                .filter(Contact.user == self)\
                .update({Contact.name: value})
        except sqlalchemy.orm.exc.DetachedInstanceError:
            pass

    def _generate_salt(self, length=10, chars=string.letters + string.digits):
        """Generates random alphanumeric string of specified length."""
        return ''.join([choice(chars) for i in range(length)])

    def set_password(self, password):
        """New password setter."""
        if isinstance(password, unicode):
            password = password.encode('utf-8')

        salt = self._generate_salt()
        hash = hashlib.sha1(password + salt).hexdigest()

        self.password_hash = hash
        self.password_salt = salt

    password = property(fset=set_password)

    def check_password(self, password):
        """Checks if given password is the same as the one in database."""
        if isinstance(password, unicode):
            password = password.encode('utf-8')

        hash = hashlib.sha1(password + self.password_salt).hexdigest()
        return hash == self.password_hash

    def search_contacts(self, term, limit=5):
        """Searches contacts by a given term."""
        pattern = term + '%'

        # basic name search
        by_name = self.contacts.filter(Contact.name.ilike(pattern))

        # by special attributes
        by_email = self.contacts.filter(
            db.or_(EmailContact.email.ilike(pattern),
                   GoogleContact.email.ilike(pattern)))
        by_id_or_username = self.contacts.filter(
            db.or_(
                db.cast(FacebookContact.user_id, db.String).ilike(pattern),
                FacebookContact.username.ilike(pattern)))

        # union of results
        return by_name.union(by_email, by_id_or_username).order_by(
            Contact.name).limit(limit)

    def event_or_404(self, id):
        """Returns user's event by given ID or aborts the request."""
        return self.events.filter(Event.id == id).first_or_404()

    @property
    def events_active(self):
        return self.events\
            .filter(Event.cancelled_at == None)\
            .order_by(db.desc(Event.updated_at), Event.name)

    @property
    def events_upcoming(self):
        return self.events\
            .filter(Event.starts_at > times.now())\
            .filter(Event.cancelled_at == None)\
            .order_by(Event.starts_at, Event.name)

    @property
    def events_current(self):
        now = times.now()
        return self.events\
            .filter(Event.starts_at < now)\
            .filter(Event.ends_at > now)\
            .filter(Event.cancelled_at == None)\
            .order_by(Event.starts_at, Event.name)

    @property
    def events_past(self):
        return self.events\
            .filter(Event.starts_at < times.now())\
            .filter(Event.cancelled_at == None)\
            .order_by(Event.starts_at, Event.name)

    @property
    def events_cancelled(self):
        return self.events\
            .filter(Event.cancelled_at != None)\
            .order_by(db.desc(Event.cancelled_at), Event.name)

    def delete_contact(self, id):
        Contact.query.with_polymorphic(Contact)\
            .filter(Contact.id == id)\
            .filter(Contact.user == self)\
            .delete()

    def find_facebook_contact(self, facebook_id):
        return FacebookContact.query.filter(FacebookContact.facebook_id == facebook_id)\
            .filter(FacebookContact.user == self)\
            .first()

    def find_email_contact(self, email):
        return self.contacts.filter(
            db.or_(EmailContact.email == email,
                   GoogleContact.email == email)).first()

    def get_contact_or_404(self, id):
        return self.contacts.filter(Contact.id == id).first_or_404()

    @classmethod
    def fetch_by_email(self, email):
        contact = Contact.query.filter(
                db.or_(
                    EmailContact.email == email,
                    GoogleContact.email == email
                )
            )\
            .filter(Contact.belongs_to_user == True)\
            .first()
        return contact.user if contact else None

    def __repr__(self):
        return '<User %r (%r)>' % (self.name, self.email)