class Presentation(db.Model): """Presentation of a talk.""" __tablename__ = 'presentations' id = db.Column(db.Integer, primary_key=True) slot_id = db.Column(db.Integer, db.ForeignKey('slots.id'), nullable=False) slot = db.relationship('Slot', backref=db.backref('presentation', uselist=False)) talk_id = db.Column(db.Integer, db.ForeignKey('talks.id'), nullable=False) talk = db.relationship('Talk', backref=db.backref('presentation', uselist=False)) def __str__(self): """Return a printable representation.""" return str(self.talk) def is_in_all_rooms(self): """Return whether the instance is in all rooms.""" return self.slot.number_of_rooms == 4 @cached_property def number_of_rooms(self): """Return the number of rooms for the instance.""" return len(self.slot.rooms)
class Room(db.Model): """Room of talks.""" __tablename__ = 'rooms' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) order = db.Column(db.Integer, nullable=False) def __str__(self): """Return a printable representation.""" return self.name
class Category(db.Model): """Talk category.""" __tablename__ = 'categories' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) slug = db.Column(db.String(75), unique=True, nullable=False) def __str__(self): """Return a printable representation.""" return self.name
class Volunteer(db.Model): """Volunteer.""" __tablename__ = 'volunteers' query_class = EventQuery id = db.Column(db.Integer, primary_key=True) event_id = db.Column(db.ForeignKey('events.id')) event = db.relationship('Event', backref='volunteers') user_id = db.Column(db.ForeignKey('users.id')) user = db.relationship('User', uselist=False) def __str__(self): """Return a printable representation.""" return self.user.name
class Duration(db.Model): """Talk duration.""" __tablename__ = 'durations' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) duration = db.Column(db.Integer, nullable=False) inactive = db.Column(db.Boolean, default=False) __mapper_args__ = { 'order_by': (inactive, duration), } def __str__(self): """Return a printable representation.""" return self.name
class Level(db.Model): """Sponsorship level.""" __tablename__ = 'sponsor_levels' query_class = EventQuery id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) description = db.Column(db.Text) order = db.Column(db.Integer, default=0) cost = db.Column(db.String, default=0) # This isn't always money. limit = db.Column(db.Integer, default=0) event_id = db.Column( db.Integer, db.ForeignKey('events.id'), nullable=False) event = db.relationship( 'Event', backref=db.backref('sponsor_levels', lazy='dynamic')) def __str__(self): """Return a printable representation.""" return self.name @cached_property def accepted_sponsors(self): """Return the accepted sponsors for the level.""" return self.sponsors.filter(Sponsor.accepted == True) # NOQA @cached_property def is_sold_out(self): """Return whether the level is sold out.""" return 0 < self.limit <= self.accepted_sponsors.count()
class Announcement(db.Model): """News announcement.""" __tablename__ = 'announcements' query_class = EventQuery id = db.Column(db.Integer, primary_key=True) slug = db.Column(db.String(255), nullable=False) title = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) active = db.Column(db.Boolean, nullable=False) published = db.Column(ArrowType) event_id = db.Column(db.Integer, db.ForeignKey('events.id'), nullable=False) event = db.relationship('Event', backref=db.backref('announcements', lazy='dynamic')) def __str__(self): """Return a printable representation.""" return self.title @observes('title') def _create_slug(self, title): """Create a slug from the title of the announcement.""" self.slug = slugify(self.title)
class AboutPage(db.Model): """About page.""" __tablename__ = 'about_pages' query_class = EventQuery id = db.Column(db.Integer, primary_key=True) # TODO: validate that the navbar_section / slug combination do not conflict # with an existing generated blueprint view route # The navbar_path dictates the location of this menu item in the # navbar hierarchy. navbar_path = db.Column(postgresql.ARRAY(db.String), nullable=False) # A slug may be empty. If it is, the item will be placed at the # root of the navbar hierarchy. slug = db.Column(db.String(255), default='', nullable=False) title = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) active = db.Column(db.Boolean, nullable=False) event_id = db.Column( db.Integer, db.ForeignKey('events.id'), nullable=False, ) event = db.relationship( 'Event', backref=db.backref('about_pages', lazy='dynamic'), ) __table_args__ = ( db.UniqueConstraint( 'navbar_path', 'slug', 'event_id', name='ix_about_pages_navbar_path_slug_event_id', ), ) def __str__(self): """Return a printable representation.""" return self.title @observes('title') def _create_slug(self, title): """Create the slug for the page.""" if not self.slug: self.slug = slugify(self.title) @property def rst_document(self): """Return the full reST document, including the title. The page's title was be used as the document heading, causing any headings defined in the page's content to be used as subheadings. To cut down on potential collisions, ``#`` symbols will be placed on the lines before and after the title. """ lines = ('{divider}', '{page.title}', '{divider}', '{page.content}') return '\n'.join(lines).format( divider='#' * len(self.title), page=self)
class Role(db.Model, RoleMixin): """User role.""" __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True) description = db.Column(db.String(255)) def __str__(self): """Return a printable representation.""" return self.name def __eq__(self, other): return self.name == other or self.name == getattr(other, 'name', None) def __hash__(self): return id(self) def __ne__(self, other): return self.name != other and self.name != getattr(other, 'name', None)
class CallToAction(db.Model): """Call to action.""" __tablename__ = 'calls_to_action' query_class = EventQuery id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) url = db.Column(URLType) active = db.Column(db.Boolean, nullable=False) begins = db.Column(ArrowType) ends = db.Column(ArrowType) event_id = db.Column( db.Integer, db.ForeignKey('events.id'), nullable=False) event = db.relationship( 'Event', backref=db.backref('calls_to_action', lazy='dynamic')) def __str__(self): """Return a printable representation.""" return self.title
class Slot(db.Model): """Time slot.""" __tablename__ = 'slots' id = db.Column(db.Integer, primary_key=True) kind = db.Column( db.Enum('break', 'meal', 'keynote', 'talk', 'tutorial', name='slotkind'), nullable=False, ) content_override = db.Column(db.Text) start = db.Column(db.Time, nullable=False) end = db.Column(db.Time, nullable=False) day_id = db.Column(db.Integer, db.ForeignKey('days.id'), nullable=False) day = db.relationship('Day', backref=db.backref('slots', lazy='dynamic')) rooms = db.relationship( 'Room', secondary=rooms_slots, backref=db.backref('slots', lazy='dynamic'), order_by=Room.order, ) def __str__(self): """Return a printable representation.""" start = self.start.strftime('%I:%M %p') end = self.end.strftime('%I:%M %p') rooms = ', '.join(map(str, self.rooms)) return '{} - {} on {}, {}'.format(start, end, self.day, rooms) @cached_property def duration(self): """Return the duration as a :class:`~datetime.timedelta`.""" return self.end - self.start
class Sponsor(db.Model): """Sponsor.""" __tablename__ = 'sponsors' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) description = db.Column(db.Text) url = db.Column(db.String(255)) logo = db.Column(db.String(255)) contact_name = db.Column(db.String(255)) contact_email = db.Column(db.String(255)) accepted = db.Column(db.Boolean) payment_received = db.Column(db.Boolean) twitter_handle = db.Column(db.String(15)) level_id = db.Column( db.Integer, db.ForeignKey('sponsor_levels.id'), nullable=False, ) level = db.relationship( 'Level', backref=db.backref('sponsors', lazy='dynamic'), ) applicant_id = db.Column( db.Integer, db.ForeignKey('users.id'), nullable=False, ) applicant = db.relationship('User') def __str__(self): """Return a printable representation.""" return self.name @cached_property def slug(self): """Return the slug for the sponsor.""" return slugify(self.name)
def pairwise(iterable): """Return values from ``iterable`` two at a time. Recipe from https://docs.python.org/3/library/itertools.html#itertools-recipes. """ a, b = tee(iterable) next(b, None) return zip(a, b) rooms_slots = db.Table( 'rooms_slots', db.Column('slot_id', db.Integer, db.ForeignKey('slots.id')), db.Column('room_id', db.Integer, db.ForeignKey('rooms.id')), ) class Day(db.Model): """Day of talks.""" __tablename__ = 'days' id = db.Column(db.Integer, primary_key=True) date = db.Column(db.Date) event_id = db.Column(db.Integer, db.ForeignKey('events.id'), nullable=False) event = db.relationship('Event',
class Event(db.Model): """Event.""" __tablename__ = 'events' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), unique=True, nullable=False) slug = db.Column(db.String(75), unique=True, nullable=True) # Event dates begins = db.Column(db.Date) ends = db.Column(db.Date) # Fields to control when the event is active active = db.Column(db.Boolean, nullable=False) activity_begins = db.Column(ArrowType) activity_ends = db.Column(ArrowType) # Proposal window proposals_begin = db.Column(ArrowType) proposals_end = db.Column(ArrowType) # When to publish the talks talk_list_begins = db.Column(ArrowType) # When to publish the schedule talk_schedule_begins = db.Column(ArrowType) # Registration information registration_closed = db.Column( db.Boolean, server_default='false', nullable=False, ) registration_url = db.Column(db.String(255)) registration_begins = db.Column(ArrowType) registration_ends = db.Column(ArrowType) def __str__(self): """Return a printable representation.""" return self.name @cached_property def accepted_talks(self): """Return the accepted :class:`~pygotham.models.Talk` list.""" return self.talks.filter(Talk.status == 'accepted').order_by(Talk.name) @observes('name') def _create_slug(self, title): """Create a slug from the name of the event.""" if not self.slug: self.slug = slugify(self.name) @property def dates(self): """Return the date(s) for the event.""" dates = ['{:%B %d}'.format(self.begins)] if self.begins != self.ends: if self.begins.month == self.ends.month: format = '{:%d}' else: format = '{:%B %d}' dates.append(format.format(self.ends)) return ' - '.join(dates) @property def is_call_for_proposals_active(self): """Return whether the call for proposals for an event is active. The CFP is active when the current :class:`~datetime.datetime` is greater than or equal to :attribute:`~pygotham.events.models.Event.proposals_begin` and less than :attribute:`~pygotham.events.models.Event.proposals_end`. """ now = arrow.utcnow().to(current_app.config['TIME_ZONE']).naive if not self.proposals_begin or now < self.proposals_begin.naive: return False if self.proposals_end and self.proposals_end.naive < now: return False return True @property def is_call_for_proposals_expired(self): """Return whether the call for proposals has expired.""" now = arrow.utcnow().to(current_app.config['TIME_ZONE']).naive return self.proposals_end and self.proposals_end.naive < now @property def is_registration_active(self): """Return whether registration for an event is active. There are several pieces to the logic of whether or not an event's registration is active: - :attribute:`~pygotham.events.models.Event.registration_closed` must be ``False``. - :attribute:`~pygotham.events.models.Event.registration_url` must be set. - :attribute:`~pygotham.events.models.Event.registration_begins` must be earlier than the current date and time. - :attribute:`~pygotham.events.models.Event.registration_ends` must be ``None`` or later than the current date and time. """ if self.registration_closed: return False if not self.registration_url: return False now = arrow.utcnow().to(current_app.config['TIME_ZONE']).naive begins = self.registration_begins if not begins or now < begins.naive: return False ends = self.registration_ends if ends and ends.naive < now: return False return True @property def schedule_is_published(self): """Return whether the schedule for an event is published.""" now = arrow.utcnow().to(current_app.config['TIME_ZONE']).naive talk_schedule_begins = self.talk_schedule_begins if not talk_schedule_begins or talk_schedule_begins.naive > now: return False return True @property def talks_are_published(self): """Return whether the talk list for an event is published.""" now = arrow.utcnow().to(current_app.config['TIME_ZONE']).naive if not self.talk_list_begins or self.talk_list_begins.naive > now: return False return True
class User(db.Model, UserMixin): """User.""" __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), info={'label': 'Name'}) email = db.Column(db.String(255), unique=True, nullable=False) password = db.Column(db.String(130)) active = db.Column(db.Boolean) confirmed_at = db.Column(db.DateTime) last_login_at = db.Column(db.DateTime) current_login_at = db.Column(db.DateTime) last_login_ip = db.Column(db.String(100)) current_login_ip = db.Column(db.String(100)) login_count = db.Column(db.Integer) registered_at = db.Column(db.DateTime) bio = db.Column(db.Text) twitter_handle = db.Column(db.String(15)) roles = db.relationship( 'Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'), ) def __str__(self): """Return a printable representation.""" return self.name or self.email def __eq__(self, other): return self.id == getattr(other, 'id', None) def __hash__(self): return id(self) def __ne__(self, other): return self.id != getattr(other, 'id', None) @cached_property def accepted_talks(self): """Return the user's accepted talks.""" return Talk.query.current.filter(Talk.status == 'accepted', Talk.user == self).order_by(Talk.name) @cached_property def has_accepted_talks(self): """Return whether the user has accepted talks.""" return self.accepted_talks.count() > 0 @cached_property def is_volunteer(self): """Return whether the user has signed up to volunteer.""" volunteers = Volunteer.query.filter( Volunteer.event_id == g.current_event.id, Volunteer.user_id == self.id, ) return db.session.query(volunteers.exists()).scalar()
import string from cached_property import cached_property from flask import g from flask_security import RoleMixin, UserMixin, recoverable from sqlalchemy import event from pygotham.core import db from pygotham.events.models import Volunteer from pygotham.talks.models import Talk __all__ = ('Role', 'User') roles_users = db.Table( 'roles_users', db.Column('user_id', db.Integer, db.ForeignKey('users.id')), db.Column('role_id', db.Integer, db.ForeignKey('roles.id')), ) class Role(db.Model, RoleMixin): """User role.""" __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True) description = db.Column(db.String(255)) def __str__(self): """Return a printable representation."""
class Talk(db.Model): """Talk.""" __tablename__ = 'talks' query_class = EventQuery id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) description = db.Column(db.Text, nullable=False) status = db.Column( db.Enum('draft', 'submitted', 'accepted', 'rejected', name='status'), default='draft', nullable=False, ) level = db.Column( db.Enum('novice', 'intermediate', 'advanced', name='level'), nullable=False, ) type = db.Column( db.Enum('talk', 'tutorial', name='type'), nullable=False, ) duration_id = db.Column(db.ForeignKey('durations.id'), nullable=False) duration = db.relationship('Duration') recording_release = db.Column(db.Boolean, nullable=True) abstract = db.Column(db.Text) additional_requirements = db.Column(db.Text) objectives = db.Column(db.Text) outline = db.Column(db.Text) target_audience = db.Column(db.Text) event_id = db.Column( db.Integer, db.ForeignKey('events.id'), nullable=False, ) event = db.relationship( 'Event', backref=db.backref('talks', lazy='dynamic'), ) category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True) category = db.relationship( 'Category', backref=db.backref('talks', lazy='dynamic'), ) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) user = db.relationship('User', backref=db.backref('talks', lazy='dynamic')) video_url = db.Column(db.String(255)) def __str__(self): """Return a printable representation.""" return self.name @property def is_accepted(self): """Return whether the instance is accepted.""" return self.status == 'accepted' @property def slug(self): """Return a slug for the instance.""" return slugify(self.name, max_length=25)
class Day(db.Model): """Day of talks.""" __tablename__ = 'days' id = db.Column(db.Integer, primary_key=True) date = db.Column(db.Date) event_id = db.Column(db.Integer, db.ForeignKey('events.id'), nullable=False) event = db.relationship('Event', backref=db.backref('days', lazy='dynamic')) def __str__(self): """Return a printable representation.""" return self.date.strftime('%B %d, %Y') @cached_property def rooms(self): """Return the rooms for the day.""" return Room.query.join(rooms_slots, Slot).filter(Slot.day == self).order_by( Room.order).all() def __iter__(self): """Iterate over the schedule for the day.""" if not self.rooms: raise StopIteration def rowspan(start, end): """Find the rowspan for an entry in the schedule table. This uses a binary search for the given end time from a sorted list of start times in order to find the index of the first start time that occurs after the given end time. This method is used to prevent issues that can occur with overlapping start and end times being included in the same list. """ return bisect_left(times, end) - times.index(start) times = sorted({slot.start for slot in self.slots}) # While we typically only care about the start times here, the # list is iterated over two items at a time. Without adding a # final element, the last time slot would be omitted. Any value # could be used here as bisect_left only assumes the list is # sorted, but using a meaningful value feels better. times.append(self.slots[-1].end) slots = db.session.query( Slot.id, Slot.content_override, Slot.kind, Slot.start, Slot.end, func.count(rooms_slots.c.slot_id).label('room_count'), func.min(Room.order).label('order'), ).join(rooms_slots, Room).filter(Slot.day == self).order_by( func.count(rooms_slots.c.slot_id), func.min(Room.order)).group_by(Slot.id, Slot.content_override, Slot.kind, Slot.start, Slot.end).all() for time, next_time in pairwise(times): row = {'time': time, 'slots': []} for slot in slots: if slot.start == time: slot.rowspan = rowspan(slot.start, slot.end) slot.colspan = slot.room_count if not slot.content_override: slot.presentation = Presentation.query.filter( Presentation.slot_id == slot.id).first() row['slots'].append(slot) if row['slots'] or next_time is None: yield row