class EventAttendee(BaseMixin, db.Model): __tablename__ = 'event_attendee' # User who is attending user_id = db.Column(None, db.ForeignKey('user.id'), nullable=False) user = db.relationship(User) # Event that the user is attending event_id = db.Column(None, db.ForeignKey('event.id'), nullable=False) event = db.relationship(Event, backref=db.backref('attendees', cascade='all, delete-orphan')) # Status codes: U/known, Y/es, N/o, M/aybe, W/ait-listed status = db.Column(db.Unicode(1), nullable=False, default=u'U') __table_args__ = (db.UniqueConstraint('user_id', 'event_id'), )
class Website(BaseNameMixin, db.Model): __tablename__ = 'website' #: URL to the website url = db.Column(db.Unicode(80), nullable=False, default=u'') #: Theme that this website uses as the default (folders can override) theme = db.Column(db.Unicode(80), nullable=False, default=u'default') #: Typekit code, if used typekit_code = db.Column(db.Unicode(20), nullable=False, default=u'') #: Google Analytics code, if used googleanalytics_code = db.Column(db.Unicode(20), nullable=False, default=u'') _hostnames = db.relationship("Hostname", cascade='all, delete-orphan', backref='website') hostnames = association_proxy('_hostnames', 'name', creator=lambda name: Hostname.get(name=name)) def __init__(self, **kwargs): super(Website, self).__init__(**kwargs) root = Folder(name=u'', title=u'', website=self) self.folders.append(root) #root.pages[0].template = u'index.html' def __repr__(self): return u'<Website %s "%s">' % (self.name, self.title) def folder_ids(self): return [i[0] for i in db.session.query(Folder.id).filter_by(website=self).all()] def url_for(self, action='view'): if action == 'view': # View in event app return url_for('index') elif action == 'list': # Folder listing in admin app return url_for('website', website=self.name) elif action == 'edit': # Edit website settings return url_for('website_edit', website=self.name)
class File(BaseScopedNameMixin, db.Model): __tablename__ = 'file' file_folder_id = db.Column(None, db.ForeignKey('file_folder.id'), nullable=False) file_folder = db.relationship(FileFolder) parent = db.synonym('file_folder') url = db.Column(db.Unicode(250), nullable=False) __table_args__ = (db.UniqueConstraint('file_folder_id', 'name'))
class Folder(BaseScopedNameMixin, db.Model): __tablename__ = 'folder' #: Website this folder is under website_id = db.Column(db.Integer, db.ForeignKey('website.id'), nullable=False) _theme = db.Column("theme", db.Unicode(80), nullable=False, default=u'') website = db.relationship(Website, backref=db.backref('folders', order_by='Folder.name', cascade='all, delete-orphan')) parent = db.synonym('website') __table_args__ = (db.UniqueConstraint('name', 'website_id'), ) @property def theme(self): return self._theme or self.website.theme @theme.setter def theme(self, value): self._theme = value #: Theme used by the folder. Defaults to the website's theme. theme = db.synonym('_theme', descriptor=theme) def __init__(self, **kwargs): super(Folder, self).__init__(**kwargs) #index = Page(name=u'', title=u'Index', folder=self, template=u'page.html') #index.name = u'' # Reset it to a blank #self.pages.append(index) def __repr__(self): return u'<Folder %s at %s>' % (self.name or '(root)', self.website.name) def url_for(self, action='view'): """ Returns a view URL based on the website's URL field. """ if action == 'view': return urljoin(self.website.url, self.name) elif action == 'list': if self.name == u'': return url_for('website', website=self.website.name) else: return url_for('folder', website=self.website.name, folder=self.name) elif action == 'edit': if self.name != u'': return url_for('folder_edit', website=self.website.name, folder=self.name)
class LoginCode(BaseMixin, db.Model): __tablename__ = 'logincode' #: Tracking code to enable users to login to an event website code = db.Column(db.Unicode(22), nullable=False, unique=True) #: Access scope requested scope = db.Column(db.Unicode(250), nullable=False, default=u'') #: User who logged in user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True, default=None) user = db.relationship(User) #: URL on event website to return user to next_url = db.Column(db.Unicode(250), nullable=False) #: Login handler URL on event website return_url = db.Column(db.Unicode(250), nullable=False) def __init__(self, **kwargs): super(LoginCode, self).__init__(**kwargs) self.code = newid()
class Node(BaseScopedNameMixin, db.Model): __tablename__ = 'node' #: Id of the node across sites (staging, production, etc) for import/export uuid = db.Column(db.Unicode(22), unique=True, default=newid, nullable=False) #: User who made this node user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, default=default_user_id) user = db.relationship(User) #: Folder in which this node is located folder_id = db.Column(db.Integer, db.ForeignKey('folder.id'), nullable=False) folder = db.relationship(Folder, backref=db.backref('nodes', order_by='Node.name', cascade='all, delete-orphan')) parent = db.synonym('folder') #: Publication date published_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) #: Type of node, for polymorphic identity type = db.Column('type', db.Unicode(20)) __table_args__ = (db.UniqueConstraint('name', 'folder_id'), ) __mapper_args__ = {'polymorphic_on': type} node_properties = db.relationship( Property, cascade='all, delete-orphan', collection_class=attribute_mapped_collection('name'), backref='node') @property def properties(self): if not hasattr(self, '_cached_properties'): self._cached_properties = _NodeProperties(self.node_properties, node=self) return self._cached_properties @properties.setter def properties(self, value): if not isinstance(value, dict): raise ValueError("Value is not a dictionary") for key in list(self.node_properties.keys()): if key not in value: self.node_properties.pop(key) self._cached_properties = _NodeProperties(value, node=self) def as_json(self): return { 'uuid': self.uuid, 'name': self.name, 'title': self.title, 'created_at': self.created_at.isoformat() + 'Z', 'updated_at': self.updated_at.isoformat() + 'Z', 'published_at': self.published_at.isoformat() + 'Z', 'userid': self.user.userid, 'type': self.type, 'properties': self.properties, } def import_from(self, data): self.uuid = data['uuid'] self.name = data['name'] self.title = data['title'] self.published_at = parse_isoformat(data['published_at']) self.properties = data['properties'] def import_from_internal(self, data): # Only required for nodes that keep internal references to other nodes pass def url_for(self, action='view'): """ Return a URL to this node. """ if action == 'view': if self.folder.name == u'': return url_for('folder', folder=self.name) else: return url_for('node', folder=self.folder.name, node=self.name) elif action == 'edit': return url_for('node_edit', website=self.folder.website.name, folder=self.folder.name, node=self.name) elif action == 'delete': return url_for('node_delete', website=self.folder.website.name, folder=self.folder.name, node=self.name)
class Event(ContentMixin, Node): __tablename__ = 'event' #: Start datetime for the event (in UTC) start_datetime = db.Column(db.DateTime, nullable=False) #: End datetime for the event (in UTC) end_datetime = db.Column(db.DateTime, nullable=False) #: Timezone as a string timezone = db.Column(db.Unicode(32), nullable=False) #: Location name location_name = db.Column(db.Unicode(80), nullable=False, default=u'') #: Location address location_address = db.Column(db.Unicode(250), nullable=False, default=u'') #: Location on map map_id = db.Column(None, db.ForeignKey('map.id'), nullable=True) map = db.relationship(Map, primaryjoin=map_id == Map.id) #: Map marker mapmarker = db.Column(db.Unicode(80), nullable=False, default=u'') #: Venue capacity, if attendance is capped capacity = db.Column(db.Integer, nullable=False, default=0) #: Allow wait-listing? allow_waitlisting = db.Column(db.Boolean, nullable=False, default=False) #: Allow a Maybe response? allow_maybe = db.Column(db.Boolean, nullable=False, default=True) #: Participant list to limit attendees to participant_list_id = db.Column(None, db.ForeignKey('participant_list.id'), nullable=True) participant_list = db.relationship( ParticipantList, primaryjoin=participant_list_id == ParticipantList.id) def _localize_time(self, value): return utc.localize(value).astimezone(timezone(self.timezone)) @property def starts_at(self): return self._localize_time(self.start_datetime) @property def ends_at(self): return self._localize_time(self.end_datetime) def count(self): """Return count of confirmed attendees.""" return len([a for a in self.attendees if a.status == u'Y']) def has_capacity(self): """Does the event have spare capacity for more attendees?""" if self.capacity == 0: return True else: return self.count() < self.capacity def can_rsvp(self, user): """Is this user authorized to participate?""" if self.participant_list: return self.participant_list.has_user(user) else: return True def set_status(self, user, status): """Set RSVP status for user.""" if status not in [u'Y', u'N', u'M']: raise ValueError("Invalid status") if status == u'M' and not self.allow_maybe: raise ValueError(u"A “Maybe” response is not allowed") if not self.can_rsvp(user): raise ValueError("This user cannot participate") attendee = EventAttendee.query.filter_by(event=self, user=user).first() if not attendee: attendee = EventAttendee(event=self, user=user) if status == u'Y' and not self.has_capacity(): if self.allow_waitlisting: status = u'W' else: raise ValueError("This event is over capacity") db.session.add(attendee) attendee.status = status def get_status(self, user): """Get RSVP status for this user.""" attendee = EventAttendee.query.filter_by(event=self, user=user).first() if not attendee: return u'U' else: return attendee.status def url_for(self, action='view'): if action == 'rsvp': base = super(Event, self).url_for('view') if base.endswith('/'): return base + 'rsvp' else: return base + '/rsvp' elif action in ['list', 'csv', 'update']: return url_for('node_action', website=self.folder.website.name, folder=self.folder.name, node=self.name, action=action) else: return super(Event, self).url_for(action) def as_json(self): result = super(ContentMixin, self).as_json() result['start_datetime'] = self.start_datetime.isoformat() + 'Z' result['end_datetime'] = self.end_datetime.isoformat() + 'Z' result['timezone'] = self.timezone result['location_name'] = self.location_name result['location_address'] = self.location_address result['map'] = self.map.uuid if self.map else None result['mapmarker'] = self.mapmarker result['capacity'] = self.capacity result['allow_waitlisting'] = self.allow_waitlisting result['allow_maybe'] = self.allow_maybe result[ 'participant_list'] = self.participant_list.uuid if self.participant_list else None return result def import_from(self, data): super(ContentMixin, self).import_from(data) self.start_datetime = parse_isoformat(data['start_datetime']) self.end_datetime = parse_isoformat(data['end_datetime']) self.timezone = data['timezone'] self.location_name = data['location_name'] self.location_address = data['location_address'] self.mapmarker = data['mapmarker'] self.capacity = data['capacity'] self.allow_waitlisting = data['allow_waitlisting'] self.allow_maybe = data['allow_maybe'] def import_from_internal(self, data): super(ContentMixin, self).import_from_internal(data) if data.get('map'): self.map = Map.query.filter_by(uuid=data['map']).first() if data.get('participant_list'): self.participant_list = ParticipantList.query.filter_by( uuid=data['participant_list']).first()
def revisions(cls): return db.relationship(Revisions, uselist=False)