class UserSetting(JSONSettingsBase, db.Model):
    """User-specific settings"""
    __table_args__ = (db.Index(None, 'user_id', 'module',
                               'name'), db.Index(None, 'user_id', 'module'),
                      db.UniqueConstraint('user_id', 'module', 'name'),
                      db.CheckConstraint('module = lower(module)',
                                         'lowercase_module'),
                      db.CheckConstraint('name = lower(name)',
                                         'lowercase_name'), {
                                             'schema': 'users'
                                         })

    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        nullable=False,
                        index=True)

    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('_all_settings',
                                              lazy='dynamic',
                                              cascade='all, delete-orphan'))

    @return_ascii
    def __repr__(self):
        return '<UserSetting({}, {}, {}, {!r})>'.format(
            self.user_id, self.module, self.name, self.value)
 def __table_args__(cls):
     return (db.Index('ix_uq_applications_name_lower',
                      db.func.lower(cls.name),
                      unique=True),
             db.Index(None,
                      cls.system_app_type,
                      unique=True,
                      postgresql_where=db.text(
                          'system_app_type != {}'.format(
                              SystemAppType.none.value))), {
                                  'schema': 'oauth'
                              })
示例#3
0
 def __table_args__(cls):
     return (db.Index('ix_uq_contribution_types_event_id_name_lower',
                      cls.event_id,
                      db.func.lower(cls.name),
                      unique=True), {
                          'schema': 'events'
                      })
示例#4
0
 def __auto_table_args(cls):
     args = [
         db.Index('ix_{}_title_fts'.format(cls.__tablename__),
                  db.func.to_tsvector('simple', cls.title),
                  postgresql_using='gin')
     ]
     if cls.title_required:
         args.append(db.CheckConstraint("title != ''", 'valid_title'))
     return tuple(args)
示例#5
0
def _make_uniques(allowed_link_types, extra_criteria=None):
    for link_type in allowed_link_types:
        where = ['link_type = {}'.format(link_type.value)]
        if extra_criteria is not None:
            where += list(extra_criteria)
        yield db.Index(None,
                       *_columns_for_types[link_type],
                       unique=True,
                       postgresql_where=db.text(' AND '.join(where)))
 def __table_args__(cls):
     return (db.Index('ix_timetable_entries_start_dt_desc',
                      cls.start_dt.desc()),
             _make_check(TimetableEntryType.SESSION_BLOCK,
                         'session_block_id'),
             _make_check(TimetableEntryType.CONTRIBUTION,
                         'contribution_id'),
             _make_check(TimetableEntryType.BREAK, 'break_id'),
             db.CheckConstraint(
                 "type != {} OR parent_id IS NULL".format(
                     TimetableEntryType.SESSION_BLOCK), 'valid_parent'), {
                         'schema': 'events'
                     })
示例#7
0
class UserEmail(db.Model):
    __tablename__ = 'emails'
    __table_args__ = (
        db.CheckConstraint('email = lower(email)', 'lowercase_email'),
        db.Index(None,
                 'email',
                 unique=True,
                 postgresql_where=db.text('NOT is_user_deleted')),
        db.Index(
            None,
            'user_id',
            unique=True,
            postgresql_where=db.text('is_primary AND NOT is_user_deleted')), {
                'schema': 'users'
            })

    #: the unique id of the email address
    id = db.Column(db.Integer, primary_key=True)
    #: the id of the associated user
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        nullable=False,
                        index=True)
    #: the email address
    email = db.Column(db.String, nullable=False, index=True)
    #: if the email is the user's primary email
    is_primary = db.Column(db.Boolean, nullable=False, default=False)
    #: if the user is marked as deleted (e.g. due to a merge). DO NOT use this flag when actually deleting an email
    is_user_deleted = db.Column(db.Boolean, nullable=False, default=False)

    # relationship backrefs:
    # - user (User._all_emails)

    @return_ascii
    def __repr__(self):
        return '<UserEmail({}, {}, {})>'.format(self.id, self.email,
                                                self.is_primary, self.user)
示例#8
0
 def __auto_table_args():
     return (db.Index(None, 'event_id', 'module',
                      'name'), db.Index(None, 'event_id', 'module'), {
                          'schema': 'events'
                      })
class SubContribution(DescriptionMixin, AttachedItemsMixin, AttachedNotesMixin,
                      db.Model):
    __tablename__ = 'subcontributions'
    __table_args__ = (db.Index(None,
                               'friendly_id',
                               'contribution_id',
                               unique=True), {
                                   'schema': 'events'
                               })

    PRELOAD_EVENT_ATTACHED_ITEMS = True
    PRELOAD_EVENT_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'subcontribution_id'
    possible_render_modes = {RenderMode.html, RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(db.Integer, primary_key=True)
    #: The human-friendly ID for the sub-contribution
    friendly_id = db.Column(db.Integer,
                            nullable=False,
                            default=_get_next_friendly_id)
    contribution_id = db.Column(db.Integer,
                                db.ForeignKey('events.contributions.id'),
                                index=True,
                                nullable=False)
    position = db.Column(db.Integer,
                         nullable=False,
                         default=_get_next_position)
    title = db.Column(db.String, nullable=False)
    duration = db.Column(db.Interval, nullable=False)
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)

    #: External references associated with this contribution
    references = db.relationship('SubContributionReference',
                                 lazy=True,
                                 cascade='all, delete-orphan',
                                 backref=db.backref('subcontribution',
                                                    lazy=True))
    #: Persons associated with this contribution
    person_links = db.relationship('SubContributionPersonLink',
                                   lazy=True,
                                   cascade='all, delete-orphan',
                                   backref=db.backref('subcontribution',
                                                      lazy=True))

    # relationship backrefs:
    # - attachment_folders (AttachmentFolder.subcontribution)
    # - contribution (Contribution.subcontributions)
    # - legacy_mapping (LegacySubContributionMapping.subcontribution)
    # - note (EventNote.subcontribution)

    def __init__(self, **kwargs):
        # explicitly initialize this relationship with None to avoid
        # an extra query to check whether there is an object associated
        # when assigning a new one (e.g. during cloning)
        kwargs.setdefault('note', None)
        super(SubContribution, self).__init__(**kwargs)

    @property
    def event(self):
        return self.contribution.event

    @locator_property
    def locator(self):
        return dict(self.contribution.locator, subcontrib_id=self.id)

    @property
    def is_protected(self):
        return self.contribution.is_protected

    @property
    def session(self):
        """Convenience property so all event entities have it"""
        return self.contribution.session if self.contribution.session_id is not None else None

    @property
    def timetable_entry(self):
        """Convenience property so all event entities have it"""
        return self.contribution.timetable_entry

    @property
    def speakers(self):
        return self.person_links

    @speakers.setter
    def speakers(self, value):
        self.person_links = value.keys()

    @property
    def location_parent(self):
        return self.contribution

    def get_access_list(self):
        return self.contribution.get_access_list()

    def get_manager_list(self, recursive=False):
        return self.contribution.get_manager_list(recursive=recursive)

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', is_deleted=False, _text=self.title)

    def can_access(self, user, **kwargs):
        return self.contribution.can_access(user, **kwargs)

    def can_manage(self, user, role=None, **kwargs):
        return self.contribution.can_manage(user, role, **kwargs)
示例#10
0
 def __table_args__(cls):
     return (db.Index('ix_uq_groups_name_lower',
                      db.func.lower(cls.name),
                      unique=True), {
                          'schema': 'users'
                      })
示例#11
0
class Registration(db.Model):
    """Somebody's registration for an event through a registration form"""
    __tablename__ = 'registrations'
    __table_args__ = (db.CheckConstraint('email = lower(email)', 'lowercase_email'),
                      db.Index(None, 'friendly_id', 'event_id', unique=True,
                               postgresql_where=db.text('NOT is_deleted')),
                      db.Index(None, 'registration_form_id', 'user_id', unique=True,
                               postgresql_where=db.text('NOT is_deleted AND (state NOT IN (3, 4))')),
                      db.Index(None, 'registration_form_id', 'email', unique=True,
                               postgresql_where=db.text('NOT is_deleted AND (state NOT IN (3, 4))')),
                      db.ForeignKeyConstraint(['event_id', 'registration_form_id'],
                                              ['event_registration.forms.event_id', 'event_registration.forms.id']),
                      {'schema': 'event_registration'})

    #: The ID of the object
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: The unguessable ID for the object
    uuid = db.Column(
        UUID,
        index=True,
        unique=True,
        nullable=False,
        default=lambda: unicode(uuid4())
    )
    #: The human-friendly ID for the object
    friendly_id = db.Column(
        db.Integer,
        nullable=False,
        default=_get_next_friendly_id
    )
    #: The ID of the event
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=False
    )
    #: The ID of the registration form
    registration_form_id = db.Column(
        db.Integer,
        db.ForeignKey('event_registration.forms.id'),
        index=True,
        nullable=False
    )
    #: The ID of the user who registered
    user_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=True
    )
    #: The ID of the latest payment transaction associated with this registration
    transaction_id = db.Column(
        db.Integer,
        db.ForeignKey('events.payment_transactions.id'),
        index=True,
        unique=True,
        nullable=True
    )
    #: The state a registration is in
    state = db.Column(
        PyIntEnum(RegistrationState),
        nullable=False,
    )
    #: The base registration fee (that is not specific to form items)
    base_price = db.Column(
        db.Numeric(8, 2),  # max. 999999.99
        nullable=False,
        default=0
    )
    #: The price modifier applied to the final calculated price
    price_adjustment = db.Column(
        db.Numeric(8, 2),  # max. 999999.99
        nullable=False,
        default=0
    )
    #: Registration price currency
    currency = db.Column(
        db.String,
        nullable=False
    )
    #: The date/time when the registration was recorded
    submitted_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc,
    )
    #: The email of the registrant
    email = db.Column(
        db.String,
        nullable=False
    )
    #: The first name of the registrant
    first_name = db.Column(
        db.String,
        nullable=False
    )
    #: The last name of the registrant
    last_name = db.Column(
        db.String,
        nullable=False
    )
    #: If the registration has been deleted
    is_deleted = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: The unique token used in tickets
    ticket_uuid = db.Column(
        UUID,
        index=True,
        unique=True,
        nullable=False,
        default=lambda: unicode(uuid4())
    )
    #: Whether the person has checked in. Setting this also sets or clears
    #: `checked_in_dt`.
    checked_in = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: The date/time when the person has checked in
    checked_in_dt = db.Column(
        UTCDateTime,
        nullable=True
    )

    #: The Event containing this registration
    event = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'registrations',
            lazy='dynamic'
        )
    )
    # The user linked to this registration
    user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'registrations',
            lazy='dynamic'
            # XXX: a delete-orphan cascade here would delete registrations when NULLing the user
        )
    )
    #: The latest payment transaction associated with this registration
    transaction = db.relationship(
        'PaymentTransaction',
        lazy=True,
        foreign_keys=[transaction_id],
        post_update=True
    )
    #: The registration this data is associated with
    data = db.relationship(
        'RegistrationData',
        lazy=True,
        cascade='all, delete-orphan',
        backref=db.backref(
            'registration',
            lazy=True
        )
    )

    # relationship backrefs:
    # - invitation (RegistrationInvitation.registration)
    # - legacy_mapping (LegacyRegistrationMapping.registration)
    # - registration_form (RegistrationForm.registrations)
    # - transactions (PaymentTransaction.registration)

    @classmethod
    def get_all_for_event(cls, event):
        """Retrieve all registrations in all registration forms of an event."""
        from fossir.modules.events.registration.models.forms import RegistrationForm
        return Registration.find_all(Registration.is_active, ~RegistrationForm.is_deleted,
                                     RegistrationForm.event_id == event.id, _join=Registration.registration_form)

    @hybrid_property
    def is_active(self):
        return not self.is_cancelled and not self.is_deleted

    @is_active.expression
    def is_active(cls):
        return ~cls.is_cancelled & ~cls.is_deleted

    @hybrid_property
    def is_cancelled(self):
        return self.state in (RegistrationState.rejected, RegistrationState.withdrawn)

    @is_cancelled.expression
    def is_cancelled(self):
        return self.state.in_((RegistrationState.rejected, RegistrationState.withdrawn))

    @locator_property
    def locator(self):
        return dict(self.registration_form.locator, registration_id=self.id)

    @locator.registrant
    def locator(self):
        """A locator suitable for 'display' pages.

        It includes the UUID of the registration unless the current
        request doesn't contain the uuid and the registration is tied
        to the currently logged-in user.
        """
        loc = self.registration_form.locator
        if (not self.user or not has_request_context() or self.user != session.user or
                request.args.get('token') == self.uuid):
            loc['token'] = self.uuid
        return loc

    @locator.uuid
    def locator(self):
        """A locator that uses uuid instead of id"""
        return dict(self.registration_form.locator, token=self.uuid)

    @property
    def can_be_modified(self):
        regform = self.registration_form
        return regform.is_modification_open and regform.is_modification_allowed(self)

    @property
    def data_by_field(self):
        return {x.field_data.field_id: x for x in self.data}

    @property
    def billable_data(self):
        return [data for data in self.data if data.price]

    @property
    def full_name(self):
        """Returns the user's name in 'Firstname Lastname' notation."""
        return self.get_full_name(last_name_first=False)

    @property
    def display_full_name(self):
        """Return the full name using the user's preferred name format."""
        return format_display_full_name(session.user, self)

    @property
    def is_ticket_blocked(self):
        """Check whether the ticket is blocked by a plugin"""
        return any(values_from_signal(signals.event.is_ticket_blocked.send(self), single_value=True))

    @property
    def is_paid(self):
        """Returns whether the registration has been paid for."""
        paid_states = {TransactionStatus.successful, TransactionStatus.pending}
        return self.transaction is not None and self.transaction.status in paid_states

    @property
    def payment_dt(self):
        """The date/time when the registration has been paid for"""
        return self.transaction.timestamp if self.is_paid else None

    @property
    def price(self):
        """The total price of the registration.

        This includes the base price, the field-specific price, and
        the custom price adjustment for the registrant.

        :rtype: Decimal
        """
        # we convert the calculated price (float) to a string to avoid this:
        # >>> Decimal(100.1)
        # Decimal('100.099999999999994315658113919198513031005859375')
        # >>> Decimal('100.1')
        # Decimal('100.1')
        calc_price = Decimal(str(sum(data.price for data in self.data)))
        base_price = self.base_price or Decimal('0')
        price_adjustment = self.price_adjustment or Decimal('0')
        return (base_price + price_adjustment + calc_price).max(0)

    @property
    def summary_data(self):
        """Export registration data nested in sections and fields"""

        def _fill_from_regform():
            for section in self.registration_form.sections:
                if not section.is_visible:
                    continue
                summary[section] = OrderedDict()
                for field in section.fields:
                    if not field.is_visible:
                        continue
                    summary[section][field] = field_summary[field]

        def _fill_from_registration():
            for field, data in field_summary.iteritems():
                section = field.parent
                summary.setdefault(section, OrderedDict())
                if field not in summary[section]:
                    summary[section][field] = data

        summary = OrderedDict()
        field_summary = {x.field_data.field: x for x in self.data}
        _fill_from_regform()
        _fill_from_registration()
        return summary

    @property
    def has_files(self):
        return any(item.storage_file_id is not None for item in self.data)

    @property
    def sections_with_answered_fields(self):
        return [x for x in self.registration_form.sections
                if any(child.id in self.data_by_field for child in x.children)]

    @classproperty
    @classmethod
    def order_by_name(cls):
        return db.func.lower(cls.last_name), db.func.lower(cls.first_name), cls.friendly_id

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'registration_form_id', 'email', 'state',
                           user_id=None, is_deleted=False, _text=self.full_name)

    def get_full_name(self, last_name_first=True, last_name_upper=False, abbrev_first_name=False):
        """Returns the user's in the specified notation.

        If not format options are specified, the name is returned in
        the 'Lastname, Firstname' notation.

        Note: Do not use positional arguments when calling this method.
        Always use keyword arguments!

        :param last_name_first: if "lastname, firstname" instead of
                                "firstname lastname" should be used
        :param last_name_upper: if the last name should be all-uppercase
        :param abbrev_first_name: if the first name should be abbreviated to
                                  use only the first character
        """
        return format_full_name(self.first_name, self.last_name,
                                last_name_first=last_name_first, last_name_upper=last_name_upper,
                                abbrev_first_name=abbrev_first_name)

    def get_personal_data(self):
        personal_data = {}
        for data in self.data:
            field = data.field_data.field
            if field.personal_data_type is not None and data.data:
                personal_data[field.personal_data_type.name] = data.friendly_data
        # might happen with imported legacy registrations (missing personal data)
        personal_data.setdefault('first_name', self.first_name)
        personal_data.setdefault('last_name', self.last_name)
        personal_data.setdefault('email', self.email)
        return personal_data

    def _render_price(self, price):
        return format_currency(price, self.currency, locale=session.lang or 'en_GB')

    def render_price(self):
        return self._render_price(self.price)

    def render_base_price(self):
        return self._render_price(self.base_price)

    def render_price_adjustment(self):
        return self._render_price(self.price_adjustment)

    def sync_state(self, _skip_moderation=True):
        """Sync the state of the registration"""
        initial_state = self.state
        regform = self.registration_form
        invitation = self.invitation
        moderation_required = (regform.moderation_enabled and not _skip_moderation and
                               (not invitation or not invitation.skip_moderation))
        with db.session.no_autoflush:
            payment_required = regform.event.has_feature('payment') and self.price and not self.is_paid
        if self.state is None:
            if moderation_required:
                self.state = RegistrationState.pending
            elif payment_required:
                self.state = RegistrationState.unpaid
            else:
                self.state = RegistrationState.complete
        elif self.state == RegistrationState.unpaid:
            if not self.price:
                self.state = RegistrationState.complete
        elif self.state == RegistrationState.complete:
            if payment_required:
                self.state = RegistrationState.unpaid
        if self.state != initial_state:
            signals.event.registration_state_updated.send(self, previous_state=initial_state)

    def update_state(self, approved=None, paid=None, rejected=None, _skip_moderation=False):
        """Update the state of the registration for a given action

        The accepted kwargs are the possible actions. ``True`` means that the
        action occured and ``False`` that it was reverted.
        """
        if sum(action is not None for action in (approved, paid, rejected)) > 1:
            raise Exception("More than one action specified")
        initial_state = self.state
        regform = self.registration_form
        invitation = self.invitation
        moderation_required = (regform.moderation_enabled and not _skip_moderation and
                               (not invitation or not invitation.skip_moderation))
        with db.session.no_autoflush:
            payment_required = regform.event.has_feature('payment') and self.price
        if self.state == RegistrationState.pending:
            if approved and payment_required:
                self.state = RegistrationState.unpaid
            elif approved:
                self.state = RegistrationState.complete
            elif rejected:
                self.state = RegistrationState.rejected
        elif self.state == RegistrationState.unpaid:
            if paid:
                self.state = RegistrationState.complete
            elif approved is False:
                self.state = RegistrationState.pending
        elif self.state == RegistrationState.complete:
            if approved is False and payment_required is False and moderation_required:
                self.state = RegistrationState.pending
            elif paid is False and payment_required:
                self.state = RegistrationState.unpaid
        if self.state != initial_state:
            signals.event.registration_state_updated.send(self, previous_state=initial_state)
示例#12
0
class Session(DescriptionMixin, ColorMixin, ProtectionManagersMixin,
              LocationMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model):
    __tablename__ = 'sessions'
    __auto_table_args = (db.Index(None, 'friendly_id', 'event_id',
                                  unique=True), {
                                      'schema': 'events'
                                  })
    location_backref_name = 'sessions'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    default_colors = ColorTuple('#202020', '#e3f2d3')
    allow_relationship_preloading = True

    PRELOAD_EVENT_ATTACHED_ITEMS = True
    PRELOAD_EVENT_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'session_id'
    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    @declared_attr
    def __table_args__(cls):
        return auto_table_args(cls)

    id = db.Column(db.Integer, primary_key=True)
    #: The human-friendly ID for the session
    friendly_id = db.Column(db.Integer,
                            nullable=False,
                            default=_get_next_friendly_id)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    title = db.Column(db.String, nullable=False)
    code = db.Column(db.String, nullable=False, default='')
    default_contribution_duration = db.Column(db.Interval,
                                              nullable=False,
                                              default=timedelta(minutes=20))
    is_poster = db.Column(db.Boolean, nullable=False, default=False)
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)

    event = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'sessions',
            primaryjoin='(Session.event_id == Event.id) & ~Session.is_deleted',
            cascade='all, delete-orphan',
            lazy=True))
    acl_entries = db.relationship('SessionPrincipal',
                                  lazy=True,
                                  cascade='all, delete-orphan',
                                  collection_class=set,
                                  backref='session')
    blocks = db.relationship('SessionBlock',
                             lazy=True,
                             cascade='all, delete-orphan',
                             backref=db.backref('session', lazy=False))

    # relationship backrefs:
    # - attachment_folders (AttachmentFolder.session)
    # - contributions (Contribution.session)
    # - legacy_mapping (LegacySessionMapping.session)
    # - note (EventNote.session)

    def __init__(self, **kwargs):
        # explicitly initialize this relationship with None to avoid
        # an extra query to check whether there is an object associated
        # when assigning a new one (e.g. during cloning)
        kwargs.setdefault('note', None)
        super(Session, self).__init__(**kwargs)

    @classmethod
    def preload_acl_entries(cls, event):
        cls.preload_relationships(cls.query.with_parent(event), 'acl_entries')

    @property
    def location_parent(self):
        return self.event

    @property
    def protection_parent(self):
        return self.event

    @property
    def session(self):
        """Convenience property so all event entities have it"""
        return self

    @property
    @memoize_request
    def start_dt(self):
        from fossir.modules.events.sessions.models.blocks import SessionBlock
        start_dt = (self.event.timetable_entries.with_entities(
            TimetableEntry.start_dt).join('session_block').filter(
                TimetableEntry.type == TimetableEntryType.SESSION_BLOCK,
                SessionBlock.session == self).order_by(
                    TimetableEntry.start_dt).first())
        return start_dt[0] if start_dt else None

    @property
    @memoize_request
    def end_dt(self):
        sorted_blocks = sorted(self.blocks,
                               key=attrgetter('timetable_entry.end_dt'),
                               reverse=True)
        return sorted_blocks[
            0].timetable_entry.end_dt if sorted_blocks else None

    @property
    @memoize_request
    def conveners(self):
        from fossir.modules.events.sessions.models.blocks import SessionBlock
        from fossir.modules.events.sessions.models.persons import SessionBlockPersonLink

        return (SessionBlockPersonLink.query.join(SessionBlock).filter(
            SessionBlock.session_id == self.id).distinct(
                SessionBlockPersonLink.person_id).all())

    @locator_property
    def locator(self):
        return dict(self.event.locator, session_id=self.id)

    def get_non_inheriting_objects(self):
        """Get a set of child objects that do not inherit protection"""
        return get_non_inheriting_objects(self)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           is_poster=False,
                           is_deleted=False,
                           _text=self.title)

    def can_manage_contributions(self, user, allow_admin=True):
        """Check whether a user can manage contributions within the session."""
        from fossir.modules.events.sessions.util import session_coordinator_priv_enabled
        if user is None:
            return False
        elif self.session.can_manage(user, allow_admin=allow_admin):
            return True
        elif (self.session.can_manage(user, 'coordinate')
              and session_coordinator_priv_enabled(self.event,
                                                   'manage-contributions')):
            return True
        else:
            return False

    def can_manage_blocks(self, user, allow_admin=True):
        """Check whether a user can manage session blocks.

        This only applies to the blocks themselves, not to contributions inside them.
        """
        from fossir.modules.events.sessions.util import session_coordinator_priv_enabled
        if user is None:
            return False
        # full session manager can always manage blocks. this also includes event managers and higher.
        elif self.session.can_manage(user, allow_admin=allow_admin):
            return True
        # session coordiator if block management is allowed
        elif (self.session.can_manage(user, 'coordinate') and
              session_coordinator_priv_enabled(self.event, 'manage-blocks')):
            return True
        else:
            return False
示例#13
0
class APIKey(db.Model):
    """API keys for users"""
    __tablename__ = 'api_keys'
    __table_args__ = (db.Index(None,
                               'user_id',
                               unique=True,
                               postgresql_where=db.text('is_active')), {
                                   'schema': 'users'
                               })

    #: api key id
    id = db.Column(db.Integer, primary_key=True)
    #: unique api key for a user
    token = db.Column(UUID,
                      nullable=False,
                      unique=True,
                      default=lambda: unicode(uuid4()))
    #: secret key used for signed requests
    secret = db.Column(UUID, nullable=False, default=lambda: unicode(uuid4()))
    #: ID of the user associated with the key
    user_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        nullable=False,
        index=True,
    )
    #: if the key is the currently active key for the user
    is_active = db.Column(db.Boolean, nullable=False, default=True)
    #: if the key has been blocked by an admin
    is_blocked = db.Column(db.Boolean, nullable=False, default=False)
    #: if persistent signatures are allowed
    is_persistent_allowed = db.Column(db.Boolean,
                                      nullable=False,
                                      default=False)
    #: the time when the key has been created
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    #: the last time when the key has been used
    last_used_dt = db.Column(UTCDateTime, nullable=True)
    #: the last ip address from which the key has been used
    last_used_ip = db.Column(INET, nullable=True)
    #: the last URI this key was used with
    last_used_uri = db.Column(db.String, nullable=True)
    #: if the last use was from an authenticated request
    last_used_auth = db.Column(db.Boolean, nullable=True)
    #: the number of times the key has been used
    use_count = db.Column(db.Integer, nullable=False, default=0)

    #: the user associated with this API key
    user = db.relationship('User', lazy=False)

    @return_ascii
    def __repr__(self):
        return '<APIKey({}, {}, {})>'.format(self.token, self.user_id,
                                             self.last_used_dt or 'never')

    def register_used(self, ip, uri, authenticated):
        """Updates the last used information"""
        self.last_used_dt = now_utc()
        self.last_used_ip = ip
        self.last_used_uri = uri
        self.last_used_auth = authenticated
        self.use_count = APIKey.use_count + 1
示例#14
0
class MenuEntry(MenuEntryMixin, db.Model):
    __tablename__ = 'menu_entries'
    __table_args__ = (
        db.CheckConstraint(
            '(type IN ({type.internal_link.value}, {type.plugin_link.value}) AND name IS NOT NULL) OR '
            '(type NOT IN ({type.internal_link.value}, {type.plugin_link.value}) and name IS NULL)'
            .format(type=MenuEntryType), 'valid_name'),
        db.CheckConstraint(
            '(type = {type.user_link.value}) = (link_url IS NOT NULL)'.format(
                type=MenuEntryType), 'valid_link_url'),
        db.CheckConstraint(
            '(type = {type.page.value} AND page_id IS NOT NULL) OR'
            ' (type != {type.page.value} AND page_id IS NULL)'.format(
                type=MenuEntryType), 'valid_page_id'),
        db.CheckConstraint(
            '(type = {type.plugin_link.value} AND plugin IS NOT NULL) OR'
            ' (type != {type.plugin_link.value} AND plugin IS NULL)'.format(
                type=MenuEntryType), 'valid_plugin'),
        db.CheckConstraint(
            '(type = {type.separator.value} AND title IS NULL) OR'
            ' (type IN ({type.user_link.value}, {type.page.value}) AND title IS NOT NULL) OR'
            ' (type NOT IN ({type.separator.value}, {type.user_link.value}, {type.page.value}))'
            .format(type=MenuEntryType), 'valid_title'),
        db.CheckConstraint("title != ''", 'title_not_empty'),
        db.Index(
            None,
            'event_id',
            'name',
            unique=True,
            postgresql_where=db.text(
                '(type = {type.internal_link.value} OR type = {type.plugin_link.value})'
                .format(type=MenuEntryType))), {
                    'schema': 'events'
                })

    #: The ID of the menu entry
    id = db.Column(db.Integer, primary_key=True)
    #: The ID of the parent menu entry (NULL if root menu entry)
    parent_id = db.Column(
        db.Integer,
        db.ForeignKey('events.menu_entries.id'),
        index=True,
        nullable=True,
    )
    #: The ID of the event which contains the menu
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    #: Whether the entry is visible in the event's menu
    is_enabled = db.Column(db.Boolean, nullable=False, default=True)
    #: The title of the menu entry (to be displayed to the user)
    title = db.Column(
        db.String,
        nullable=True,
    )
    #: The name of the menu entry (to uniquely identify a default entry for a given event)
    name = db.Column(db.String, nullable=True)
    #: The relative position of the entry in the menu
    position = db.Column(db.Integer,
                         nullable=False,
                         default=_get_next_position)
    #: Whether the menu entry should be opened in a new tab or window
    new_tab = db.Column(db.Boolean, nullable=False, default=False)
    #: The target URL of a custom link
    link_url = db.Column(db.String, nullable=True, default=None)
    #: The name of the plugin from which the entry comes from (NULL if the entry does not come from a plugin)
    plugin = db.Column(db.String, nullable=True)
    #: The page ID if the entry is a page
    page_id = db.Column(db.Integer,
                        db.ForeignKey('events.pages.id'),
                        nullable=True,
                        index=True,
                        default=None)
    #: The type of the menu entry
    type = db.Column(PyIntEnum(MenuEntryType), nullable=False)

    #: The Event containing the menu entry
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('menu_entries', lazy='dynamic'))
    #: The page of the menu entry
    page = db.relationship(
        'EventPage',
        lazy=True,
        cascade='all, delete-orphan',
        single_parent=True,
        backref=db.backref('menu_entry', lazy=False, uselist=False),
    )
    #: The children menu entries and parent backref
    children = db.relationship(
        'MenuEntry',
        order_by='MenuEntry.position',
        backref=db.backref('parent', remote_side=[id]),
    )

    # relationship backrefs:
    # - parent (MenuEntry.children)

    @property
    def is_root(self):
        return self.parent_id is None

    @staticmethod
    def get_for_event(event):
        return (MenuEntry.query.with_parent(event).filter(
            MenuEntry.parent_id.is_(None)).options(
                joinedload('children')).order_by(MenuEntry.position).all())

    def move(self, to):
        from_ = self.position
        new_pos = to
        value = -1
        if to is None or to < 0:
            new_pos = to = -1

        if from_ > to:
            new_pos += 1
            from_, to = to, from_
            to -= 1
            value = 1

        entries = (MenuEntry.query.with_parent(self.event).filter(
            MenuEntry.parent == self.parent,
            MenuEntry.position.between(from_ + 1, to)))
        for e in entries:
            e.position += value
        self.position = new_pos

    def insert(self, parent, position):
        if position is None or position < 0:
            position = -1
        old_siblings = (MenuEntry.query.with_parent(self.event).filter(
            MenuEntry.position > self.position,
            MenuEntry.parent == self.parent))
        for sibling in old_siblings:
            sibling.position -= 1

        new_siblings = (MenuEntry.query.with_parent(self.event).filter(
            MenuEntry.position > position, MenuEntry.parent == parent))
        for sibling in new_siblings:
            sibling.position += 1

        self.parent = parent
        self.position = position + 1
class EventReminder(db.Model):
    """Email reminders for events"""
    __tablename__ = 'reminders'
    __table_args__ = (db.Index(None,
                               'scheduled_dt',
                               postgresql_where=db.text('not is_sent')), {
                                   'schema': 'events'
                               })

    #: The ID of the reminder
    id = db.Column(db.Integer, primary_key=True)
    #: The ID of the event
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    #: The ID of the user who created the reminder
    creator_id = db.Column(db.Integer,
                           db.ForeignKey('users.users.id'),
                           index=True,
                           nullable=False)
    #: The date/time when the reminder was created
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    #: The date/time when the reminder should be sent
    scheduled_dt = db.Column(UTCDateTime, nullable=False)
    #: If the reminder has been sent
    is_sent = db.Column(db.Boolean, nullable=False, default=False)
    #: How long before the event start the reminder should be sent
    #: This is needed to update the `scheduled_dt` when changing the
    #: start  time of the event.
    event_start_delta = db.Column(db.Interval, nullable=True)
    #: The recipients of the notification
    recipients = db.Column(ARRAY(db.String), nullable=False, default=[])
    #: If the notification should also be sent to all event participants
    send_to_participants = db.Column(db.Boolean, nullable=False, default=False)
    #: If the notification should include a summary of the event's schedule.
    include_summary = db.Column(db.Boolean, nullable=False, default=False)
    #: The address to use as Reply-To in the notification email.
    reply_to_address = db.Column(db.String, nullable=False)
    #: Custom message to include in the email
    message = db.Column(db.String, nullable=False, default='')

    #: The user who created the reminder
    creator = db.relationship('User',
                              lazy=True,
                              backref=db.backref('event_reminders',
                                                 lazy='dynamic'))
    #: The Event this reminder is associated with
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('reminders', lazy='dynamic'))

    @property
    def locator(self):
        return dict(self.event.locator, reminder_id=self.id)

    @property
    def all_recipients(self):
        """Returns all recipients of the notifications.

        This includes both explicit recipients and, if enabled,
        participants of the event.
        """
        recipients = set(self.recipients)
        if self.send_to_participants:
            recipients.update(
                reg.email
                for reg in Registration.get_all_for_event(self.event))
        recipients.discard(
            '')  # just in case there was an empty email address somewhere
        return recipients

    @hybrid_property
    def is_relative(self):
        """Returns if the reminder is relative to the event time"""
        return self.event_start_delta is not None

    @is_relative.expression
    def is_relative(self):
        return self.event_start_delta != None  # NOQA

    @property
    def is_overdue(self):
        return not self.is_sent and self.scheduled_dt <= now_utc()

    def send(self):
        """Sends the reminder to its recipients."""
        self.is_sent = True
        recipients = self.all_recipients
        if not recipients:
            logger.info(
                'Notification %s has no recipients; not sending anything',
                self)
            return
        email_tpl = make_reminder_email(self.event, self.include_summary,
                                        self.message)
        email = make_email(bcc_list=recipients,
                           from_address=self.reply_to_address,
                           template=email_tpl)
        send_email(email, self.event, 'Reminder', self.creator)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'event_id',
                           'scheduled_dt',
                           is_sent=False)
示例#16
0
class VCRoom(db.Model):
    __tablename__ = 'vc_rooms'
    __table_args__ = (db.Index(None, 'data', postgresql_using='gin'),
                      {'schema': 'events'})

    #: Videoconference room ID
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: Type of the videoconference room
    type = db.Column(
        db.String,
        nullable=False
    )
    #: Name of the videoconference room
    name = db.Column(
        db.String,
        nullable=False
    )
    #: Status of the videoconference room
    status = db.Column(
        PyIntEnum(VCRoomStatus),
        nullable=False
    )
    #: ID of the creator
    created_by_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        nullable=False,
        index=True
    )
    #: Creation timestamp of the videoconference room
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc
    )

    #: Modification timestamp of the videoconference room
    modified_dt = db.Column(
        UTCDateTime
    )
    #: videoconference plugin-specific data
    data = db.Column(
        JSONB,
        nullable=False
    )

    #: The user who created the videoconference room
    created_by_user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'vc_rooms',
            lazy='dynamic'
        )
    )

    # relationship backrefs:
    # - events (VCRoomEventAssociation.vc_room)

    @property
    def plugin(self):
        from fossir.modules.vc.util import get_vc_plugins
        return get_vc_plugins().get(self.type)

    @property
    def locator(self):
        return {'vc_room_id': self.id, 'service': self.type}

    @return_ascii
    def __repr__(self):
        return '<VCRoom({}, {}, {})>'.format(self.id, self.name, self.type)
示例#17
0
class Contribution(DescriptionMixin, ProtectionManagersMixin, LocationMixin,
                   AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin,
                   AuthorsSpeakersMixin, CustomFieldsMixin, db.Model):
    __tablename__ = 'contributions'
    __auto_table_args = (
        db.Index(None,
                 'friendly_id',
                 'event_id',
                 unique=True,
                 postgresql_where=db.text('NOT is_deleted')),
        db.Index(None, 'event_id',
                 'track_id'), db.Index(None, 'event_id', 'abstract_id'),
        db.Index(None,
                 'abstract_id',
                 unique=True,
                 postgresql_where=db.text('NOT is_deleted')),
        db.CheckConstraint(
            "session_block_id IS NULL OR session_id IS NOT NULL",
            'session_block_if_session'),
        db.ForeignKeyConstraint(
            ['session_block_id', 'session_id'],
            ['events.session_blocks.id', 'events.session_blocks.session_id']),
        {
            'schema': 'events'
        })
    location_backref_name = 'contributions'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    possible_render_modes = {RenderMode.html, RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    allow_relationship_preloading = True

    PRELOAD_EVENT_ATTACHED_ITEMS = True
    PRELOAD_EVENT_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id'

    @declared_attr
    def __table_args__(cls):
        return auto_table_args(cls)

    id = db.Column(db.Integer, primary_key=True)
    #: The human-friendly ID for the contribution
    friendly_id = db.Column(db.Integer,
                            nullable=False,
                            default=_get_next_friendly_id)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    session_id = db.Column(db.Integer,
                           db.ForeignKey('events.sessions.id'),
                           index=True,
                           nullable=True)
    session_block_id = db.Column(db.Integer,
                                 db.ForeignKey('events.session_blocks.id'),
                                 index=True,
                                 nullable=True)
    track_id = db.Column(db.Integer,
                         db.ForeignKey('events.tracks.id',
                                       ondelete='SET NULL'),
                         index=True,
                         nullable=True)
    abstract_id = db.Column(db.Integer,
                            db.ForeignKey('event_abstracts.abstracts.id'),
                            index=True,
                            nullable=True)
    type_id = db.Column(db.Integer,
                        db.ForeignKey('events.contribution_types.id'),
                        index=True,
                        nullable=True)
    title = db.Column(db.String, nullable=False)
    duration = db.Column(db.Interval, nullable=False)
    board_number = db.Column(db.String, nullable=False, default='')
    keywords = db.Column(ARRAY(db.String), nullable=False, default=[])
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: The last user-friendly sub-contribution ID
    _last_friendly_subcontribution_id = db.deferred(
        db.Column('last_friendly_subcontribution_id',
                  db.Integer,
                  nullable=False,
                  default=0))

    event = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'contributions',
            primaryjoin=
            '(Contribution.event_id == Event.id) & ~Contribution.is_deleted',
            cascade='all, delete-orphan',
            lazy=True))
    session = db.relationship(
        'Session',
        lazy=True,
        backref=db.backref(
            'contributions',
            primaryjoin=
            '(Contribution.session_id == Session.id) & ~Contribution.is_deleted',
            lazy=True))
    session_block = db.relationship(
        'SessionBlock',
        lazy=True,
        foreign_keys=[session_block_id],
        backref=db.backref(
            'contributions',
            primaryjoin=
            '(Contribution.session_block_id == SessionBlock.id) & ~Contribution.is_deleted',
            lazy=True))
    type = db.relationship('ContributionType',
                           lazy=True,
                           backref=db.backref('contributions', lazy=True))
    acl_entries = db.relationship('ContributionPrincipal',
                                  lazy=True,
                                  cascade='all, delete-orphan',
                                  collection_class=set,
                                  backref='contribution')
    subcontributions = db.relationship(
        'SubContribution',
        lazy=True,
        primaryjoin=
        '(SubContribution.contribution_id == Contribution.id) & ~SubContribution.is_deleted',
        order_by='SubContribution.position',
        cascade='all, delete-orphan',
        backref=db.backref(
            'contribution',
            primaryjoin='SubContribution.contribution_id == Contribution.id',
            lazy=True))
    abstract = db.relationship(
        'Abstract',
        lazy=True,
        backref=db.backref(
            'contribution',
            primaryjoin=
            '(Contribution.abstract_id == Abstract.id) & ~Contribution.is_deleted',
            lazy=True,
            uselist=False))
    track = db.relationship(
        'Track',
        lazy=True,
        backref=db.backref(
            'contributions',
            primaryjoin=
            '(Contribution.track_id == Track.id) & ~Contribution.is_deleted',
            lazy=True,
            passive_deletes=True))
    #: External references associated with this contribution
    references = db.relationship('ContributionReference',
                                 lazy=True,
                                 cascade='all, delete-orphan',
                                 backref=db.backref('contribution', lazy=True))
    #: Persons associated with this contribution
    person_links = db.relationship('ContributionPersonLink',
                                   lazy=True,
                                   cascade='all, delete-orphan',
                                   backref=db.backref('contribution',
                                                      lazy=True))
    #: Data stored in abstract/contribution fields
    field_values = db.relationship('ContributionFieldValue',
                                   lazy=True,
                                   cascade='all, delete-orphan',
                                   backref=db.backref('contribution',
                                                      lazy=True))
    #: The accepted paper revision
    _accepted_paper_revision = db.relationship(
        'PaperRevision',
        lazy=True,
        viewonly=True,
        uselist=False,
        primaryjoin=
        ('(PaperRevision._contribution_id == Contribution.id) & (PaperRevision.state == {})'
         .format(PaperRevisionState.accepted)),
    )
    #: Paper files not submitted for reviewing
    pending_paper_files = db.relationship(
        'PaperFile',
        lazy=True,
        viewonly=True,
        primaryjoin=
        '(PaperFile._contribution_id == Contribution.id) & (PaperFile.revision_id.is_(None))',
    )
    #: Paper reviewing judges
    paper_judges = db.relationship('User',
                                   secondary='event_paper_reviewing.judges',
                                   collection_class=set,
                                   lazy=True,
                                   backref=db.backref(
                                       'judge_for_contributions',
                                       collection_class=set,
                                       lazy=True))
    #: Paper content reviewers
    paper_content_reviewers = db.relationship(
        'User',
        secondary='event_paper_reviewing.content_reviewers',
        collection_class=set,
        lazy=True,
        backref=db.backref('content_reviewer_for_contributions',
                           collection_class=set,
                           lazy=True))
    #: Paper layout reviewers
    paper_layout_reviewers = db.relationship(
        'User',
        secondary='event_paper_reviewing.layout_reviewers',
        collection_class=set,
        lazy=True,
        backref=db.backref('layout_reviewer_for_contributions',
                           collection_class=set,
                           lazy=True))

    @declared_attr
    def _paper_last_revision(cls):
        # Incompatible with joinedload
        subquery = (db.select([
            db.func.max(PaperRevision.submitted_dt)
        ]).where(PaperRevision._contribution_id == cls.id).correlate_except(
            PaperRevision).as_scalar())
        return db.relationship('PaperRevision',
                               uselist=False,
                               lazy=True,
                               viewonly=True,
                               primaryjoin=db.and_(
                                   PaperRevision._contribution_id == cls.id,
                                   PaperRevision.submitted_dt == subquery))

    # relationship backrefs:
    # - _paper_files (PaperFile._contribution)
    # - _paper_revisions (PaperRevision._contribution)
    # - attachment_folders (AttachmentFolder.contribution)
    # - legacy_mapping (LegacyContributionMapping.contribution)
    # - note (EventNote.contribution)
    # - timetable_entry (TimetableEntry.contribution)
    # - vc_room_associations (VCRoomEventAssociation.linked_contrib)

    @declared_attr
    def is_scheduled(cls):
        from fossir.modules.events.timetable.models.entries import TimetableEntry
        query = (db.exists([1]).where(TimetableEntry.contribution_id ==
                                      cls.id).correlate_except(TimetableEntry))
        return db.column_property(query, deferred=True)

    @declared_attr
    def subcontribution_count(cls):
        from fossir.modules.events.contributions.models.subcontributions import SubContribution
        query = (db.select([db.func.count(SubContribution.id)]).where(
            (SubContribution.contribution_id == cls.id)
            & ~SubContribution.is_deleted).correlate_except(SubContribution))
        return db.column_property(query, deferred=True)

    @declared_attr
    def _paper_revision_count(cls):
        query = (db.select([db.func.count(PaperRevision.id)
                            ]).where(PaperRevision._contribution_id ==
                                     cls.id).correlate_except(PaperRevision))
        return db.column_property(query, deferred=True)

    def __init__(self, **kwargs):
        # explicitly initialize those relationships with None to avoid
        # an extra query to check whether there is an object associated
        # when assigning a new one (e.g. during cloning)
        kwargs.setdefault('note', None)
        kwargs.setdefault('timetable_entry', None)
        super(Contribution, self).__init__(**kwargs)

    @classmethod
    def preload_acl_entries(cls, event):
        cls.preload_relationships(cls.query.with_parent(event), 'acl_entries')

    @property
    def location_parent(self):
        if self.session_block_id is not None:
            return self.session_block
        elif self.session_id is not None:
            return self.session
        else:
            return self.event

    @property
    def protection_parent(self):
        return self.session if self.session_id is not None else self.event

    @property
    def start_dt(self):
        return self.timetable_entry.start_dt if self.timetable_entry else None

    @property
    def end_dt(self):
        return self.timetable_entry.start_dt + self.duration if self.timetable_entry else None

    @property
    def start_dt_poster(self):
        if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent:
            return self.timetable_entry.parent.start_dt

    @property
    def end_dt_poster(self):
        if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent:
            return self.timetable_entry.parent.end_dt

    @property
    def duration_poster(self):
        if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent:
            return self.timetable_entry.parent.duration

    @property
    def start_dt_display(self):
        """The displayed start time of the contribution.

        This is the start time of the poster session if applicable,
        otherwise the start time of the contribution itself.
        """
        return self.start_dt_poster or self.start_dt

    @property
    def end_dt_display(self):
        """The displayed end time of the contribution.

        This is the end time of the poster session if applicable,
        otherwise the end time of the contribution itself.
        """
        return self.end_dt_poster or self.end_dt

    @property
    def duration_display(self):
        """The displayed duration of the contribution.

        This is the duration of the poster session if applicable,
        otherwise the duration of the contribution itself.
        """
        return self.duration_poster or self.duration

    @property
    def submitters(self):
        return {
            person_link
            for person_link in self.person_links if person_link.is_submitter
        }

    @locator_property
    def locator(self):
        return dict(self.event.locator, contrib_id=self.id)

    @property
    def verbose_title(self):
        return '#{} ({})'.format(self.friendly_id, self.title)

    @property
    def paper(self):
        return Paper(self) if self._paper_last_revision else None

    def is_paper_reviewer(self, user):
        return user in self.paper_content_reviewers or user in self.paper_layout_reviewers

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', is_deleted=False, _text=self.title)

    def can_manage(self,
                   user,
                   role=None,
                   allow_admin=True,
                   check_parent=True,
                   explicit_role=False):
        if super(Contribution, self).can_manage(user,
                                                role,
                                                allow_admin=allow_admin,
                                                check_parent=check_parent,
                                                explicit_role=explicit_role):
            return True
        if (check_parent and self.session_id is not None
                and self.session.can_manage(user,
                                            'coordinate',
                                            allow_admin=allow_admin,
                                            explicit_role=explicit_role)
                and session_coordinator_priv_enabled(self.event,
                                                     'manage-contributions')):
            return True
        return False

    def get_non_inheriting_objects(self):
        """Get a set of child objects that do not inherit protection."""
        return get_non_inheriting_objects(self)

    def is_user_associated(self, user, check_abstract=False):
        if user is None:
            return False
        if check_abstract and self.abstract and self.abstract.submitter == user:
            return True
        return any(pl.person.user == user for pl in self.person_links
                   if pl.person.user)
示例#18
0
class RegistrationForm(db.Model):
    """A registration form for an event"""

    __tablename__ = 'forms'
    __table_args__ = (
        db.Index(
            'ix_uq_forms_participation',
            'event_id',
            unique=True,
            postgresql_where=db.text('is_participation AND NOT is_deleted')),
        db.UniqueConstraint(
            'id', 'event_id'),  # useless but needed for the registrations fkey
        {
            'schema': 'event_registration'
        })

    #: The ID of the object
    id = db.Column(db.Integer, primary_key=True)
    #: The ID of the event
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    #: The title of the registration form
    title = db.Column(db.String, nullable=False)
    #: Whether it's the 'Participants' form of a meeting/lecture
    is_participation = db.Column(db.Boolean, nullable=False, default=False)
    # An introduction text for users
    introduction = db.Column(db.Text, nullable=False, default='')
    #: Contact information for registrants
    contact_info = db.Column(db.String, nullable=False, default='')
    #: Datetime when the registration form is open
    start_dt = db.Column(UTCDateTime, nullable=True)
    #: Datetime when the registration form is closed
    end_dt = db.Column(UTCDateTime, nullable=True)
    #: Whether registration modifications are allowed
    modification_mode = db.Column(PyIntEnum(ModificationMode),
                                  nullable=False,
                                  default=ModificationMode.not_allowed)
    #: Datetime when the modification period is over
    modification_end_dt = db.Column(UTCDateTime, nullable=True)
    #: Whether the registration has been marked as deleted
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: Whether users must be logged in to register
    require_login = db.Column(db.Boolean, nullable=False, default=False)
    #: Whether registrations must be associated with an fossir account
    require_user = db.Column(db.Boolean, nullable=False, default=False)
    #: Maximum number of registrations allowed
    registration_limit = db.Column(db.Integer, nullable=True)
    #: Whether registrations should be displayed in the participant list
    publish_registrations_enabled = db.Column(db.Boolean,
                                              nullable=False,
                                              default=False)
    #: Whether to display the number of registrations
    publish_registration_count = db.Column(db.Boolean,
                                           nullable=False,
                                           default=False)
    #: Whether checked-in status should be displayed in the event pages and participant list
    publish_checkin_enabled = db.Column(db.Boolean,
                                        nullable=False,
                                        default=False)
    #: Whether registrations must be approved by a manager
    moderation_enabled = db.Column(db.Boolean, nullable=False, default=False)
    #: The base fee users have to pay when registering
    base_price = db.Column(
        db.Numeric(8, 2),  # max. 999999.99
        nullable=False,
        default=0)
    #: Currency for prices in the registration form
    currency = db.Column(db.String, nullable=False)
    #: Notifications sender address
    notification_sender_address = db.Column(db.String, nullable=True)
    #: Custom message to include in emails for pending registrations
    message_pending = db.Column(db.Text, nullable=False, default='')
    #: Custom message to include in emails for unpaid registrations
    message_unpaid = db.Column(db.Text, nullable=False, default='')
    #: Custom message to include in emails for complete registrations
    message_complete = db.Column(db.Text, nullable=False, default='')
    #: Whether the manager notifications for this event are enabled
    manager_notifications_enabled = db.Column(db.Boolean,
                                              nullable=False,
                                              default=False)
    #: List of emails that should receive management notifications
    manager_notification_recipients = db.Column(ARRAY(db.String),
                                                nullable=False,
                                                default=[])
    #: Whether tickets are enabled for this form
    tickets_enabled = db.Column(db.Boolean, nullable=False, default=False)
    #: Whether to send tickets by e-mail
    ticket_on_email = db.Column(db.Boolean, nullable=False, default=True)
    #: Whether to show a ticket download link on the event homepage
    ticket_on_event_page = db.Column(db.Boolean, nullable=False, default=True)
    #: Whether to show a ticket download link on the registration summary page
    ticket_on_summary_page = db.Column(db.Boolean,
                                       nullable=False,
                                       default=True)
    #: The ID of the template used to generate tickets
    ticket_template_id = db.Column(db.Integer,
                                   db.ForeignKey(DesignerTemplate.id),
                                   nullable=True,
                                   index=True)

    #: The Event containing this registration form
    event = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'registration_forms',
            primaryjoin=
            '(RegistrationForm.event_id == Event.id) & ~RegistrationForm.is_deleted',
            cascade='all, delete-orphan',
            lazy=True))
    #: The template used to generate tickets
    ticket_template = db.relationship('DesignerTemplate',
                                      lazy=True,
                                      foreign_keys=ticket_template_id,
                                      backref=db.backref('ticket_for_regforms',
                                                         lazy=True))
    # The items (sections, text, fields) in the form
    form_items = db.relationship('RegistrationFormItem',
                                 lazy=True,
                                 cascade='all, delete-orphan',
                                 order_by='RegistrationFormItem.position',
                                 backref=db.backref('registration_form',
                                                    lazy=True))
    #: The registrations associated with this form
    registrations = db.relationship(
        'Registration',
        lazy=True,
        cascade='all, delete-orphan',
        foreign_keys=[Registration.registration_form_id],
        backref=db.backref('registration_form', lazy=True))
    #: The registration invitations associated with this form
    invitations = db.relationship('RegistrationInvitation',
                                  lazy=True,
                                  cascade='all, delete-orphan',
                                  backref=db.backref('registration_form',
                                                     lazy=True))

    @hybrid_property
    def has_ended(self):
        return self.end_dt is not None and self.end_dt <= now_utc()

    @has_ended.expression
    def has_ended(cls):
        return cls.end_dt.isnot(None) & (cls.end_dt <= now_utc())

    @hybrid_property
    def has_started(self):
        return self.start_dt is not None and self.start_dt <= now_utc()

    @has_started.expression
    def has_started(cls):
        return cls.start_dt.isnot(None) & (cls.start_dt <= now_utc())

    @hybrid_property
    def is_modification_open(self):
        end_dt = self.modification_end_dt if self.modification_end_dt else self.end_dt
        return now_utc() <= end_dt if end_dt else True

    @is_modification_open.expression
    def is_modification_open(self):
        now = now_utc()
        return now <= db.func.coalesce(self.modification_end_dt, self.end_dt,
                                       now)

    @hybrid_property
    def is_open(self):
        return not self.is_deleted and self.has_started and not self.has_ended

    @is_open.expression
    def is_open(cls):
        return ~cls.is_deleted & cls.has_started & ~cls.has_ended

    @hybrid_property
    def is_scheduled(self):
        return not self.is_deleted and self.start_dt is not None

    @is_scheduled.expression
    def is_scheduled(cls):
        return ~cls.is_deleted & cls.start_dt.isnot(None)

    @property
    def locator(self):
        return dict(self.event.locator, reg_form_id=self.id)

    @property
    def active_fields(self):
        return [
            field for field in self.form_items
            if (field.is_field and field.is_enabled and not field.is_deleted
                and field.parent.is_enabled and not field.parent.is_deleted)
        ]

    @property
    def sections(self):
        return [x for x in self.form_items if x.is_section]

    @property
    def disabled_sections(self):
        return [
            x for x in self.sections if not x.is_visible and not x.is_deleted
        ]

    @property
    def limit_reached(self):
        return self.registration_limit and len(
            self.active_registrations) >= self.registration_limit

    @property
    def is_active(self):
        return self.is_open and not self.limit_reached

    @property
    @memoize_request
    def active_registrations(self):
        return (Registration.query.with_parent(self).filter(
            Registration.is_active).options(subqueryload('data')).all())

    @property
    def sender_address(self):
        contact_email = self.event.contact_emails[
            0] if self.event.contact_emails else None
        return self.notification_sender_address or contact_email

    @return_ascii
    def __repr__(self):
        return '<RegistrationForm({}, {}, {})>'.format(self.id, self.event_id,
                                                       self.title)

    def is_modification_allowed(self, registration):
        """Checks whether a registration may be modified"""
        if not registration.is_active:
            return False
        elif self.modification_mode == ModificationMode.allowed_always:
            return True
        elif self.modification_mode == ModificationMode.allowed_until_payment:
            return not registration.is_paid
        else:
            return False

    def can_submit(self, user):
        return self.is_active and (not self.require_login or user)

    @memoize_request
    def get_registration(self, user=None, uuid=None, email=None):
        """Retrieves registrations for this registration form by user or uuid"""
        if (bool(user) + bool(uuid) + bool(email)) != 1:
            raise ValueError(
                "Exactly one of `user`, `uuid` and `email` must be specified")
        if user:
            return user.registrations.filter_by(registration_form=self).filter(
                Registration.is_active).first()
        if uuid:
            try:
                UUID(hex=uuid)
            except ValueError:
                raise BadRequest('Malformed registration token')
            return Registration.query.with_parent(self).filter_by(
                uuid=uuid).filter(Registration.is_active).first()
        if email:
            return Registration.query.with_parent(self).filter_by(
                email=email).filter(Registration.is_active).first()

    def render_base_price(self):
        return format_currency(self.base_price,
                               self.currency,
                               locale=session.lang or 'en_GB')

    def get_personal_data_field_id(self, personal_data_type):
        """Returns the field id corresponding to the personal data field with the given name."""
        for field in self.active_fields:
            if (isinstance(field, RegistrationFormPersonalDataField)
                    and field.personal_data_type == personal_data_type):
                return field.id
示例#19
0
class User(PersonMixin, db.Model):
    """fossir users"""

    # Useful when dealing with both users and groups in the same code
    is_group = False
    is_single_person = True
    is_network = False
    principal_order = 0
    principal_type = PrincipalType.user

    __tablename__ = 'users'
    __table_args__ = (
        db.Index(None,
                 'is_system',
                 unique=True,
                 postgresql_where=db.text('is_system')),
        db.CheckConstraint(
            'NOT is_system OR (NOT is_blocked AND NOT is_pending AND NOT is_deleted)',
            'valid_system_user'),
        db.CheckConstraint('id != merged_into_id', 'not_merged_self'),
        db.CheckConstraint(
            "is_pending OR (first_name != '' AND last_name != '')",
            'not_pending_proper_names'), {
                'schema': 'users'
            })

    #: the unique id of the user
    id = db.Column(db.Integer, primary_key=True)
    #: the first name of the user
    first_name = db.Column(db.String, nullable=False, index=True)
    #: the last/family name of the user
    last_name = db.Column(db.String, nullable=False, index=True)
    # the title of the user - you usually want the `title` property!
    _title = db.Column('title',
                       PyIntEnum(UserTitle),
                       nullable=False,
                       default=UserTitle.none)
    #: the phone number of the user
    phone = db.Column(db.String, nullable=False, default='')
    #: the address of the user
    address = db.Column(db.Text, nullable=False, default='')
    #: the id of the user this user has been merged into
    merged_into_id = db.Column(db.Integer,
                               db.ForeignKey('users.users.id'),
                               nullable=True)
    #: if the user is the default system user
    is_system = db.Column(db.Boolean, nullable=False, default=False)
    #: if the user is an administrator with unrestricted access to everything
    is_admin = db.Column(db.Boolean, nullable=False, default=False, index=True)
    #: if the user has been blocked
    is_blocked = db.Column(db.Boolean, nullable=False, default=False)
    #: if the user is pending (e.g. never logged in, only added to some list)
    is_pending = db.Column(db.Boolean, nullable=False, default=False)
    #: if the user is deleted (e.g. due to a merge)
    is_deleted = db.Column('is_deleted',
                           db.Boolean,
                           nullable=False,
                           default=False)

    _affiliation = db.relationship('UserAffiliation',
                                   lazy=False,
                                   uselist=False,
                                   cascade='all, delete-orphan',
                                   backref=db.backref('user', lazy=True))

    _primary_email = db.relationship(
        'UserEmail',
        lazy=False,
        uselist=False,
        cascade='all, delete-orphan',
        primaryjoin='(User.id == UserEmail.user_id) & UserEmail.is_primary')
    _secondary_emails = db.relationship(
        'UserEmail',
        lazy=True,
        cascade='all, delete-orphan',
        collection_class=set,
        primaryjoin='(User.id == UserEmail.user_id) & ~UserEmail.is_primary')
    _all_emails = db.relationship('UserEmail',
                                  lazy=True,
                                  viewonly=True,
                                  primaryjoin='User.id == UserEmail.user_id',
                                  collection_class=set,
                                  backref=db.backref('user', lazy=False))
    #: the affiliation of the user
    affiliation = association_proxy('_affiliation',
                                    'name',
                                    creator=lambda v: UserAffiliation(name=v))
    #: the primary email address of the user
    email = association_proxy(
        '_primary_email',
        'email',
        creator=lambda v: UserEmail(email=v, is_primary=True))
    #: any additional emails the user might have
    secondary_emails = association_proxy('_secondary_emails',
                                         'email',
                                         creator=lambda v: UserEmail(email=v))
    #: all emails of the user. read-only; use it only for searching by email! also, do not use it between
    #: modifying `email` or `secondary_emails` and a session expire/commit!
    all_emails = association_proxy('_all_emails', 'email')  # read-only!

    #: the user this user has been merged into
    merged_into_user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref('merged_from_users', lazy=True),
        remote_side='User.id',
    )
    #: the users's favorite users
    favorite_users = db.relationship(
        'User',
        secondary=favorite_user_table,
        primaryjoin=id == favorite_user_table.c.user_id,
        secondaryjoin=(id == favorite_user_table.c.target_id) & ~is_deleted,
        lazy=True,
        collection_class=set,
        backref=db.backref('favorite_of', lazy=True, collection_class=set),
    )
    #: the users's favorite categories
    favorite_categories = db.relationship(
        'Category',
        secondary=favorite_category_table,
        lazy=True,
        collection_class=set,
        backref=db.backref('favorite_of', lazy=True, collection_class=set),
    )
    #: the user's category suggestions
    suggested_categories = db.relationship(
        'SuggestedCategory',
        lazy='dynamic',
        order_by='SuggestedCategory.score.desc()',
        cascade='all, delete-orphan',
        backref=db.backref('user', lazy=True))
    #: the active API key of the user
    api_key = db.relationship(
        'APIKey',
        lazy=True,
        uselist=False,
        cascade='all, delete-orphan',
        primaryjoin='(User.id == APIKey.user_id) & APIKey.is_active',
        back_populates='user')
    #: the previous API keys of the user
    old_api_keys = db.relationship(
        'APIKey',
        lazy=True,
        cascade='all, delete-orphan',
        order_by='APIKey.created_dt.desc()',
        primaryjoin='(User.id == APIKey.user_id) & ~APIKey.is_active',
        back_populates='user')
    #: the identities used by this user
    identities = db.relationship('Identity',
                                 lazy=True,
                                 cascade='all, delete-orphan',
                                 collection_class=set,
                                 backref=db.backref('user', lazy=False))

    # relationship backrefs:
    # - _all_settings (UserSetting.user)
    # - abstract_comments (AbstractComment.user)
    # - abstract_email_log_entries (AbstractEmailLogEntry.user)
    # - abstract_reviewer_for_tracks (Track.abstract_reviewers)
    # - abstract_reviews (AbstractReview.user)
    # - abstracts (Abstract.submitter)
    # - agreements (Agreement.user)
    # - attachment_files (AttachmentFile.user)
    # - attachments (Attachment.user)
    # - blockings (Blocking.created_by_user)
    # - content_reviewer_for_contributions (Contribution.paper_content_reviewers)
    # - convener_for_tracks (Track.conveners)
    # - created_events (Event.creator)
    # - event_log_entries (EventLogEntry.user)
    # - event_notes_revisions (EventNoteRevision.user)
    # - event_persons (EventPerson.user)
    # - event_reminders (EventReminder.creator)
    # - favorite_of (User.favorite_users)
    # - global_abstract_reviewer_for_events (Event.global_abstract_reviewers)
    # - global_convener_for_events (Event.global_conveners)
    # - in_attachment_acls (AttachmentPrincipal.user)
    # - in_attachment_folder_acls (AttachmentFolderPrincipal.user)
    # - in_blocking_acls (BlockingPrincipal.user)
    # - in_category_acls (CategoryPrincipal.user)
    # - in_contribution_acls (ContributionPrincipal.user)
    # - in_event_acls (EventPrincipal.user)
    # - in_event_settings_acls (EventSettingPrincipal.user)
    # - in_session_acls (SessionPrincipal.user)
    # - in_settings_acls (SettingPrincipal.user)
    # - judge_for_contributions (Contribution.paper_judges)
    # - judged_abstracts (Abstract.judge)
    # - judged_papers (PaperRevision.judge)
    # - layout_reviewer_for_contributions (Contribution.paper_layout_reviewers)
    # - local_groups (LocalGroup.members)
    # - merged_from_users (User.merged_into_user)
    # - modified_abstract_comments (AbstractComment.modified_by)
    # - modified_abstracts (Abstract.modified_by)
    # - modified_review_comments (PaperReviewComment.modified_by)
    # - oauth_tokens (OAuthToken.user)
    # - owned_rooms (Room.owner)
    # - paper_competences (PaperCompetence.user)
    # - paper_reviews (PaperReview.user)
    # - paper_revisions (PaperRevision.submitter)
    # - registrations (Registration.user)
    # - requests_created (Request.created_by_user)
    # - requests_processed (Request.processed_by_user)
    # - reservations (Reservation.created_by_user)
    # - reservations_booked_for (Reservation.booked_for_user)
    # - review_comments (PaperReviewComment.user)
    # - static_sites (StaticSite.creator)
    # - survey_submissions (SurveySubmission.user)
    # - vc_rooms (VCRoom.created_by_user)

    @staticmethod
    def get_system_user():
        return User.query.filter_by(is_system=True).one()

    @property
    def as_principal(self):
        """The serializable principal identifier of this user"""
        return 'User', self.id

    @property
    def as_avatar(self):
        # TODO: remove this after DB is free of Avatars
        from fossir.modules.users.legacy import AvatarUserWrapper
        avatar = AvatarUserWrapper(self.id)

        # avoid garbage collection
        avatar.user
        return avatar

    as_legacy = as_avatar

    @property
    def avatar_css(self):
        from fossir.modules.users.util import get_color_for_username
        return 'background-color: {};'.format(
            get_color_for_username(self.full_name))

    @property
    def external_identities(self):
        """The external identities of the user"""
        return {x for x in self.identities if x.provider != 'fossir'}

    @property
    def local_identities(self):
        """The local identities of the user"""
        return {x for x in self.identities if x.provider == 'fossir'}

    @property
    def local_identity(self):
        """The main (most recently used) local identity"""
        identities = sorted(self.local_identities,
                            key=attrgetter('safe_last_login_dt'),
                            reverse=True)
        return identities[0] if identities else None

    @property
    def secondary_local_identities(self):
        """The local identities of the user except the main one"""
        return self.local_identities - {self.local_identity}

    @locator_property
    def locator(self):
        return {'user_id': self.id}

    @cached_property
    def settings(self):
        """Returns the user settings proxy for this user"""
        from fossir.modules.users import user_settings
        return user_settings.bind(self)

    @property
    def synced_fields(self):
        """The fields of the user whose values are currently synced.

        This set is always a subset of the synced fields define in
        synced fields of the idp in 'fossir.conf'.
        """
        synced_fields = self.settings.get('synced_fields')
        # If synced_fields is missing or None, then all fields are synced
        if synced_fields is None:
            return multipass.synced_fields
        else:
            return set(synced_fields) & multipass.synced_fields

    @synced_fields.setter
    def synced_fields(self, value):
        value = set(value) & multipass.synced_fields
        if value == multipass.synced_fields:
            self.settings.delete('synced_fields')
        else:
            self.settings.set('synced_fields', list(value))

    @property
    def synced_values(self):
        """The values from the synced identity for the user.

        Those values are not the actual user's values and might differ
        if they are not set as synchronized.
        """
        identity = self._get_synced_identity(refresh=False)
        if identity is None:
            return {}
        return {
            field: (identity.data.get(field) or '')
            for field in multipass.synced_fields
        }

    def __contains__(self, user):
        """Convenience method for `user in user_or_group`."""
        return self == user

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'email',
                           is_deleted=False,
                           is_pending=False,
                           _text=self.full_name)

    def can_be_modified(self, user):
        """If this user can be modified by the given user"""
        return self == user or user.is_admin

    def iter_identifiers(self, check_providers=False, providers=None):
        """Yields ``(provider, identifier)`` tuples for the user.

        :param check_providers: If True, providers are searched for
                                additional identifiers once all existing
                                identifiers have been yielded.
        :param providers: May be a set containing provider names to
                          get only identifiers from the specified
                          providers.
        """
        done = set()
        for identity in self.identities:
            if providers is not None and identity.provider not in providers:
                continue
            item = (identity.provider, identity.identifier)
            done.add(item)
            yield item
        if not check_providers:
            return
        for identity_info in multipass.search_identities(
                providers=providers, exact=True, email=self.all_emails):
            item = (identity_info.provider.name, identity_info.identifier)
            if item not in done:
                yield item

    def get_full_name(self, *args, **kwargs):
        kwargs['_show_empty_names'] = True
        return super(User, self).get_full_name(*args, **kwargs)

    def make_email_primary(self, email):
        """Promotes a secondary email address to the primary email address

        :param email: an email address that is currently a secondary email
        """
        secondary = next(
            (x for x in self._secondary_emails if x.email == email), None)
        if secondary is None:
            raise ValueError('email is not a secondary email address')
        self._primary_email.is_primary = False
        db.session.flush()
        secondary.is_primary = True
        db.session.flush()

    def synchronize_data(self, refresh=False):
        """Synchronize the fields of the user from the sync identity.

        This will take only into account :attr:`synced_fields`.

        :param refresh: bool -- Whether to refresh the synced identity
                        with the sync provider before instead of using
                        the stored data. (Only if the sync provider
                        supports refresh.)
        """
        identity = self._get_synced_identity(refresh=refresh)
        if identity is None:
            return
        for field in self.synced_fields:
            old_value = getattr(self, field)
            new_value = identity.data.get(field) or ''
            if field in ('first_name', 'last_name') and not new_value:
                continue
            if old_value == new_value:
                continue
            flash(
                _("Your {field_name} has been synchronised from '{old_value}' to '{new_value}'."
                  ).format(field_name=syncable_fields[field],
                           old_value=old_value,
                           new_value=new_value))
            setattr(self, field, new_value)

    def _get_synced_identity(self, refresh=False):
        sync_provider = multipass.sync_provider
        if sync_provider is None:
            return None
        identities = sorted(
            [x for x in self.identities if x.provider == sync_provider.name],
            key=attrgetter('safe_last_login_dt'),
            reverse=True)
        if not identities:
            return None
        identity = identities[0]
        if refresh and identity.multipass_data is not None:
            try:
                identity_info = sync_provider.refresh_identity(
                    identity.identifier, identity.multipass_data)
            except IdentityRetrievalFailed:
                identity_info = None
            if identity_info:
                identity.data = identity_info.data
        return identity
示例#20
0
class RegistrationFormItem(db.Model):
    """Generic registration form item"""

    __tablename__ = 'form_items'
    __table_args__ = (
        db.CheckConstraint(
            "(input_type IS NULL) = (type NOT IN ({t.field}, {t.field_pd}))".
            format(t=RegistrationFormItemType),
            name='valid_input'),
        db.CheckConstraint("NOT is_manager_only OR type = {type}".format(
            type=RegistrationFormItemType.section),
                           name='valid_manager_only'),
        db.CheckConstraint(
            "(type IN ({t.section}, {t.section_pd})) = (parent_id IS NULL)".
            format(t=RegistrationFormItemType),
            name='top_level_sections'),
        db.CheckConstraint(
            "(type != {type}) = (personal_data_type IS NULL)".format(
                type=RegistrationFormItemType.field_pd),
            name='pd_field_type'),
        db.CheckConstraint(
            "NOT is_deleted OR (type NOT IN ({t.section_pd}, {t.field_pd}))".
            format(t=RegistrationFormItemType),
            name='pd_not_deleted'),
        db.CheckConstraint("is_enabled OR type != {type}".format(
            type=RegistrationFormItemType.section_pd),
                           name='pd_section_enabled'),
        db.CheckConstraint(
            "is_enabled OR type != {type} OR personal_data_type NOT IN "
            "({pt.email}, {pt.first_name}, {pt.last_name})".format(
                type=RegistrationFormItemType.field_pd, pt=PersonalDataType),
            name='pd_field_enabled'),
        db.CheckConstraint(
            "is_required OR type != {type} OR personal_data_type NOT IN "
            "({pt.email}, {pt.first_name}, {pt.last_name})".format(
                type=RegistrationFormItemType.field_pd, pt=PersonalDataType),
            name='pd_field_required'),
        db.CheckConstraint(
            "current_data_id IS NULL OR type IN ({t.field}, {t.field_pd})".
            format(t=RegistrationFormItemType),
            name='current_data_id_only_field'),
        db.Index('ix_uq_form_items_pd_section',
                 'registration_form_id',
                 unique=True,
                 postgresql_where=db.text('type = {type}'.format(
                     type=RegistrationFormItemType.section_pd))),
        db.Index('ix_uq_form_items_pd_field',
                 'registration_form_id',
                 'personal_data_type',
                 unique=True,
                 postgresql_where=db.text('type = {type}'.format(
                     type=RegistrationFormItemType.field_pd))), {
                         'schema': 'event_registration'
                     })
    __mapper_args__ = {'polymorphic_on': 'type', 'polymorphic_identity': None}

    #: The ID of the object
    id = db.Column(db.Integer, primary_key=True)
    #: The ID  of the registration form
    registration_form_id = db.Column(
        db.Integer,
        db.ForeignKey('event_registration.forms.id'),
        index=True,
        nullable=False)
    #: The type of the registration form item
    type = db.Column(PyIntEnum(RegistrationFormItemType), nullable=False)
    #: The type of a personal data field
    personal_data_type = db.Column(PyIntEnum(PersonalDataType), nullable=True)
    #: The ID of the parent form item
    parent_id = db.Column(db.Integer,
                          db.ForeignKey('event_registration.form_items.id'),
                          index=True,
                          nullable=True)
    position = db.Column(db.Integer,
                         nullable=False,
                         default=_get_next_position)
    #: The title of this field
    title = db.Column(db.String, nullable=False)
    #: Description of this field
    description = db.Column(db.String, nullable=False, default='')
    #: Whether the field is enabled
    is_enabled = db.Column(db.Boolean, nullable=False, default=True)
    #: Whether field has been "deleted"
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: determines if the field is mandatory
    is_required = db.Column(db.Boolean, nullable=False, default=False)
    #: if the section is only accessible to managers
    is_manager_only = db.Column(db.Boolean, nullable=False, default=False)
    #: input type of this field
    input_type = db.Column(db.String, nullable=True)
    #: unversioned field data
    data = db.Column(JSON, nullable=False, default=lambda: None)

    #: The ID of the latest data
    current_data_id = db.Column(db.Integer,
                                db.ForeignKey(
                                    'event_registration.form_field_data.id',
                                    use_alter=True),
                                index=True,
                                nullable=True)

    #: The latest value of the field
    current_data = db.relationship(
        'RegistrationFormFieldData',
        primaryjoin=
        'RegistrationFormItem.current_data_id == RegistrationFormFieldData.id',
        foreign_keys=current_data_id,
        lazy=True,
        post_update=True)

    #: The list of all versions of the field data
    data_versions = db.relationship(
        'RegistrationFormFieldData',
        primaryjoin=
        'RegistrationFormItem.id == RegistrationFormFieldData.field_id',
        foreign_keys='RegistrationFormFieldData.field_id',
        lazy=True,
        cascade='all, delete-orphan',
        backref=db.backref('field', lazy=False))

    # The children of the item and the parent backref
    children = db.relationship('RegistrationFormItem',
                               lazy=True,
                               order_by='RegistrationFormItem.position',
                               backref=db.backref('parent',
                                                  lazy=False,
                                                  remote_side=[id]))

    # relationship backrefs:
    # - parent (RegistrationFormItem.children)
    # - registration_form (RegistrationForm.form_items)

    @property
    def view_data(self):
        """Returns object with data that Angular can understand"""
        return dict(id=self.id,
                    description=self.description,
                    position=self.position)

    @hybrid_property
    def is_section(self):
        return self.type in {
            RegistrationFormItemType.section,
            RegistrationFormItemType.section_pd
        }

    @is_section.expression
    def is_section(cls):
        return cls.type.in_([
            RegistrationFormItemType.section,
            RegistrationFormItemType.section_pd
        ])

    @hybrid_property
    def is_field(self):
        return self.type in {
            RegistrationFormItemType.field, RegistrationFormItemType.field_pd
        }

    @is_field.expression
    def is_field(cls):
        return cls.type.in_([
            RegistrationFormItemType.field, RegistrationFormItemType.field_pd
        ])

    @hybrid_property
    def is_visible(self):
        return self.is_enabled and not self.is_deleted and (
            self.parent_id is None or self.parent.is_visible)

    @is_visible.expression
    def is_visible(cls):
        sections = aliased(RegistrationFormSection)
        query = (db.session.query(literal(True)).filter(
            sections.id == cls.parent_id).filter(~sections.is_deleted).filter(
                sections.is_enabled).exists())
        return cls.is_enabled & ~cls.is_deleted & (
            (cls.parent_id == None) | query)  # noqa

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'registration_form_id',
                           is_enabled=True,
                           is_deleted=False,
                           is_manager_only=False,
                           _text=self.title)
class PaperRevision(ProposalRevisionMixin, RenderModeMixin, db.Model):
    __tablename__ = 'revisions'
    __table_args__ = (
        db.Index(None,
                 'contribution_id',
                 unique=True,
                 postgresql_where=db.text('state = {}'.format(
                     PaperRevisionState.accepted))),
        db.UniqueConstraint('contribution_id', 'submitted_dt'),
        db.CheckConstraint(
            '(state IN ({}, {}, {})) = (judge_id IS NOT NULL)'.format(
                PaperRevisionState.accepted, PaperRevisionState.rejected,
                PaperRevisionState.to_be_corrected),
            name='judge_if_judged'),
        db.CheckConstraint(
            '(state IN ({}, {}, {})) = (judgment_dt IS NOT NULL)'.format(
                PaperRevisionState.accepted, PaperRevisionState.rejected,
                PaperRevisionState.to_be_corrected),
            name='judgment_dt_if_judged'), {
                'schema': 'event_paper_reviewing'
            })

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    proposal_attr = 'paper'

    id = db.Column(db.Integer, primary_key=True)
    state = db.Column(PyIntEnum(PaperRevisionState),
                      nullable=False,
                      default=PaperRevisionState.submitted)
    _contribution_id = db.Column('contribution_id',
                                 db.Integer,
                                 db.ForeignKey('events.contributions.id'),
                                 index=True,
                                 nullable=False)
    submitter_id = db.Column(db.Integer,
                             db.ForeignKey('users.users.id'),
                             index=True,
                             nullable=False)
    submitted_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    judge_id = db.Column(db.Integer,
                         db.ForeignKey('users.users.id'),
                         index=True,
                         nullable=True)
    judgment_dt = db.Column(UTCDateTime, nullable=True)
    _judgment_comment = db.Column('judgment_comment',
                                  db.Text,
                                  nullable=False,
                                  default='')

    _contribution = db.relationship('Contribution',
                                    lazy=True,
                                    backref=db.backref(
                                        '_paper_revisions',
                                        lazy=True,
                                        order_by=submitted_dt.asc()))
    submitter = db.relationship('User',
                                lazy=True,
                                foreign_keys=submitter_id,
                                backref=db.backref('paper_revisions',
                                                   lazy='dynamic'))
    judge = db.relationship('User',
                            lazy=True,
                            foreign_keys=judge_id,
                            backref=db.backref('judged_papers',
                                               lazy='dynamic'))

    judgment_comment = RenderModeMixin.create_hybrid_property(
        '_judgment_comment')

    # relationship backrefs:
    # - comments (PaperReviewComment.paper_revision)
    # - files (PaperFile.paper_revision)
    # - reviews (PaperReview.revision)

    def __init__(self, *args, **kwargs):
        paper = kwargs.pop('paper', None)
        if paper:
            kwargs.setdefault('_contribution', paper.contribution)
        super(PaperRevision, self).__init__(*args, **kwargs)

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', '_contribution_id', state=None)

    @locator_property
    def locator(self):
        return dict(self.paper.locator, revision_id=self.id)

    @property
    def paper(self):
        return self._contribution.paper

    @property
    def is_last_revision(self):
        return self == self.paper.last_revision

    @property
    def number(self):
        return self.paper.revisions.index(self) + 1

    @paper.setter
    def paper(self, paper):
        self._contribution = paper.contribution

    def get_timeline(self, user=None):
        comments = [x for x in self.comments
                    if x.can_view(user)] if user else self.comments
        reviews = [x for x in self.reviews
                   if x.can_view(user)] if user else self.reviews
        judgment = [
            PaperJudgmentProxy(self)
        ] if self.state == PaperRevisionState.to_be_corrected else []
        return sorted(chain(comments, reviews, judgment),
                      key=attrgetter('created_dt'))

    def get_reviews(self, group=None, user=None):
        reviews = []
        if user and group:
            reviews = [
                x for x in self.reviews
                if x.group.instance == group and x.user == user
            ]
        elif user:
            reviews = [x for x in self.reviews if x.user == user]
        elif group:
            reviews = [x for x in self.reviews if x.group.instance == group]
        return reviews

    def get_reviewed_for_groups(self, user, include_reviewed=False):
        from fossir.modules.events.papers.models.reviews import PaperTypeProxy
        reviewed_for = {x.type
                        for x in self.reviews
                        if x.user == user} if include_reviewed else set()
        if self.paper.cfp.content_reviewing_enabled and user in self.paper.cfp.content_reviewers:
            reviewed_for.add(PaperReviewType.content)
        if self.paper.cfp.layout_reviewing_enabled and user in self.paper.cfp.layout_reviewers:
            reviewed_for.add(PaperReviewType.layout)
        return set(map(PaperTypeProxy, reviewed_for))

    def has_user_reviewed(self, user, review_type=None):
        from fossir.modules.events.papers.models.reviews import PaperReviewType
        if review_type:
            if isinstance(review_type, basestring):
                review_type = PaperReviewType[review_type]
            return any(review.user == user and review.type == review_type
                       for review in self.reviews)
        else:
            layout_review = next(
                (review for review in self.reviews if review.user == user
                 and review.type == PaperReviewType.layout), None)
            content_review = next(
                (review for review in self.reviews if review.user == user
                 and review.type == PaperReviewType.content), None)
            if user in self._contribution.paper_layout_reviewers and user in self._contribution.paper_content_reviewers:
                return bool(layout_review and content_review)
            elif user in self._contribution.paper_layout_reviewers:
                return bool(layout_review)
            elif user in self._contribution.paper_content_reviewers:
                return bool(content_review)

    def get_spotlight_file(self):
        pdf_files = [
            paper_file for paper_file in self.files
            if paper_file.content_type == 'application/pdf'
        ]
        return pdf_files[0] if len(pdf_files) == 1 else None
 def __table_args__(cls):
     return (db.Index('ix_uq_reference_types_name_lower',
                      db.func.lower(cls.name),
                      unique=True), {
                          'schema': 'fossir'
                      })
class Abstract(ProposalMixin, ProposalRevisionMixin, DescriptionMixin,
               CustomFieldsMixin, AuthorsSpeakersMixin, db.Model):
    """Represents an abstract that can be associated to a Contribution."""

    __tablename__ = 'abstracts'
    __auto_table_args = (
        db.Index(None,
                 'friendly_id',
                 'event_id',
                 unique=True,
                 postgresql_where=db.text('NOT is_deleted')),
        db.CheckConstraint(
            '(state = {}) OR (accepted_track_id IS NULL)'.format(
                AbstractState.accepted),
            name='accepted_track_id_only_accepted'),
        db.CheckConstraint(
            '(state = {}) OR (accepted_contrib_type_id IS NULL)'.format(
                AbstractState.accepted),
            name='accepted_contrib_type_id_only_accepted'),
        db.CheckConstraint(
            '(state = {}) = (merged_into_id IS NOT NULL)'.format(
                AbstractState.merged),
            name='merged_into_id_only_merged'),
        db.CheckConstraint(
            '(state = {}) = (duplicate_of_id IS NOT NULL)'.format(
                AbstractState.duplicate),
            name='duplicate_of_id_only_duplicate'),
        db.CheckConstraint(
            '(state IN ({}, {}, {}, {})) = (judge_id IS NOT NULL)'.format(
                AbstractState.accepted, AbstractState.rejected,
                AbstractState.merged, AbstractState.duplicate),
            name='judge_if_judged'),
        db.CheckConstraint(
            '(state IN ({}, {}, {}, {})) = (judgment_dt IS NOT NULL)'.format(
                AbstractState.accepted, AbstractState.rejected,
                AbstractState.merged, AbstractState.duplicate),
            name='judgment_dt_if_judged'), {
                'schema': 'event_abstracts'
            })

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    marshmallow_aliases = {'_description': 'content'}

    # Proposal mixin properties
    proposal_type = 'abstract'
    call_for_proposals_attr = 'cfa'
    delete_comment_endpoint = 'abstracts.delete_abstract_comment'
    create_comment_endpoint = 'abstracts.comment_abstract'
    edit_comment_endpoint = 'abstracts.edit_abstract_comment'
    create_review_endpoint = 'abstracts.review_abstract'
    edit_review_endpoint = 'abstracts.edit_review'
    create_judgment_endpoint = 'abstracts.judge_abstract'
    revisions_enabled = False

    @declared_attr
    def __table_args__(cls):
        return auto_table_args(cls)

    id = db.Column(db.Integer, primary_key=True)
    friendly_id = db.Column(db.Integer,
                            nullable=False,
                            default=_get_next_friendly_id)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    title = db.Column(db.String, nullable=False)
    #: ID of the user who submitted the abstract
    submitter_id = db.Column(db.Integer,
                             db.ForeignKey('users.users.id'),
                             index=True,
                             nullable=False)
    submitted_contrib_type_id = db.Column(db.Integer,
                                          db.ForeignKey(
                                              'events.contribution_types.id',
                                              ondelete='SET NULL'),
                                          nullable=True,
                                          index=True)
    submitted_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    modified_by_id = db.Column(db.Integer,
                               db.ForeignKey('users.users.id'),
                               nullable=True,
                               index=True)
    modified_dt = db.Column(
        UTCDateTime,
        nullable=True,
    )
    state = db.Column(PyIntEnum(AbstractState),
                      nullable=False,
                      default=AbstractState.submitted)
    submission_comment = db.Column(db.Text, nullable=False, default='')
    #: ID of the user who judged the abstract
    judge_id = db.Column(db.Integer,
                         db.ForeignKey('users.users.id'),
                         index=True,
                         nullable=True)
    _judgment_comment = db.Column('judgment_comment',
                                  db.Text,
                                  nullable=False,
                                  default='')
    judgment_dt = db.Column(
        UTCDateTime,
        nullable=True,
    )
    accepted_track_id = db.Column(db.Integer,
                                  db.ForeignKey('events.tracks.id',
                                                ondelete='SET NULL'),
                                  nullable=True,
                                  index=True)
    accepted_contrib_type_id = db.Column(db.Integer,
                                         db.ForeignKey(
                                             'events.contribution_types.id',
                                             ondelete='SET NULL'),
                                         nullable=True,
                                         index=True)
    merged_into_id = db.Column(db.Integer,
                               db.ForeignKey('event_abstracts.abstracts.id'),
                               index=True,
                               nullable=True)
    duplicate_of_id = db.Column(db.Integer,
                                db.ForeignKey('event_abstracts.abstracts.id'),
                                index=True,
                                nullable=True)
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    event = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'abstracts',
            primaryjoin=
            '(Abstract.event_id == Event.id) & ~Abstract.is_deleted',
            cascade='all, delete-orphan',
            lazy=True))
    #: User who submitted the abstract
    submitter = db.relationship(
        'User',
        lazy=True,
        foreign_keys=submitter_id,
        backref=db.backref(
            'abstracts',
            primaryjoin=
            '(Abstract.submitter_id == User.id) & ~Abstract.is_deleted',
            lazy='dynamic'))
    modified_by = db.relationship(
        'User',
        lazy=True,
        foreign_keys=modified_by_id,
        backref=db.backref(
            'modified_abstracts',
            primaryjoin=
            '(Abstract.modified_by_id == User.id) & ~Abstract.is_deleted',
            lazy='dynamic'))
    submitted_contrib_type = db.relationship(
        'ContributionType',
        lazy=True,
        foreign_keys=submitted_contrib_type_id,
        backref=db.backref(
            'proposed_abstracts',
            primaryjoin=
            '(Abstract.submitted_contrib_type_id == ContributionType.id) & ~Abstract.is_deleted',
            lazy=True,
            passive_deletes=True))
    submitted_for_tracks = db.relationship(
        'Track',
        secondary='event_abstracts.submitted_for_tracks',
        collection_class=set,
        backref=db.backref(
            'abstracts_submitted',
            primaryjoin=
            'event_abstracts.submitted_for_tracks.c.track_id == Track.id',
            secondaryjoin=
            '(event_abstracts.submitted_for_tracks.c.abstract_id == Abstract.id) & ~Abstract.is_deleted',
            collection_class=set,
            lazy=True,
            passive_deletes=True))
    reviewed_for_tracks = db.relationship(
        'Track',
        secondary='event_abstracts.reviewed_for_tracks',
        collection_class=set,
        backref=db.backref(
            'abstracts_reviewed',
            primaryjoin=
            'event_abstracts.reviewed_for_tracks.c.track_id == Track.id',
            secondaryjoin=
            '(event_abstracts.reviewed_for_tracks.c.abstract_id == Abstract.id) & ~Abstract.is_deleted',
            collection_class=set,
            lazy=True,
            passive_deletes=True))
    #: User who judged the abstract
    judge = db.relationship(
        'User',
        lazy=True,
        foreign_keys=judge_id,
        backref=db.backref(
            'judged_abstracts',
            primaryjoin='(Abstract.judge_id == User.id) & ~Abstract.is_deleted',
            lazy='dynamic'))
    accepted_track = db.relationship(
        'Track',
        lazy=True,
        backref=db.backref(
            'abstracts_accepted',
            primaryjoin=
            '(Abstract.accepted_track_id == Track.id) & ~Abstract.is_deleted',
            lazy=True,
            passive_deletes=True))
    accepted_contrib_type = db.relationship(
        'ContributionType',
        lazy=True,
        foreign_keys=accepted_contrib_type_id,
        backref=db.backref(
            'abstracts_accepted',
            primaryjoin=
            '(Abstract.accepted_contrib_type_id == ContributionType.id) & ~Abstract.is_deleted',
            lazy=True,
            passive_deletes=True))
    merged_into = db.relationship(
        'Abstract',
        lazy=True,
        remote_side=id,
        foreign_keys=merged_into_id,
        backref=db.backref('merged_abstracts',
                           primaryjoin=(db.remote(merged_into_id) == id)
                           & ~db.remote(is_deleted),
                           lazy=True))
    duplicate_of = db.relationship(
        'Abstract',
        lazy=True,
        remote_side=id,
        foreign_keys=duplicate_of_id,
        backref=db.backref('duplicate_abstracts',
                           primaryjoin=(db.remote(duplicate_of_id) == id)
                           & ~db.remote(is_deleted),
                           lazy=True))
    #: Data stored in abstract/contribution fields
    field_values = db.relationship('AbstractFieldValue',
                                   lazy=True,
                                   cascade='all, delete-orphan',
                                   backref=db.backref('abstract', lazy=True))
    #: Persons associated with this abstract
    person_links = db.relationship('AbstractPersonLink',
                                   lazy=True,
                                   cascade='all, delete-orphan',
                                   order_by='AbstractPersonLink.display_order',
                                   backref=db.backref('abstract', lazy=True))

    # relationship backrefs:
    # - comments (AbstractComment.abstract)
    # - contribution (Contribution.abstract)
    # - duplicate_abstracts (Abstract.duplicate_of)
    # - email_logs (AbstractEmailLogEntry.abstract)
    # - files (AbstractFile.abstract)
    # - merged_abstracts (Abstract.merged_into)
    # - proposed_related_abstract_reviews (AbstractReview.proposed_related_abstract)
    # - reviews (AbstractReview.abstract)

    @property
    def candidate_contrib_types(self):
        contrib_types = set()
        for track in self.reviewed_for_tracks:
            if self.get_track_reviewing_state(
                    track) == AbstractReviewingState.positive:
                review = next((x for x in self.reviews if x.track == track),
                              None)
                contrib_types.add(review.proposed_contribution_type)
        return contrib_types

    @property
    def candidate_tracks(self):
        states = {
            AbstractReviewingState.positive, AbstractReviewingState.conflicting
        }
        return {
            t
            for t in self.reviewed_for_tracks
            if self.get_track_reviewing_state(t) in states
        }

    @property
    def edit_track_mode(self):
        if not inspect(self).persistent:
            return EditTrackMode.both
        elif self.state not in {
                AbstractState.submitted, AbstractState.withdrawn
        }:
            return EditTrackMode.none
        elif (self.public_state
              in (AbstractPublicState.awaiting, AbstractPublicState.withdrawn)
              and self.reviewed_for_tracks == self.submitted_for_tracks):
            return EditTrackMode.both
        else:
            return EditTrackMode.reviewed_for

    @property
    def public_state(self):
        if self.state != AbstractState.submitted:
            return getattr(AbstractPublicState, self.state.name)
        elif self.reviews:
            return AbstractPublicState.under_review
        else:
            return AbstractPublicState.awaiting

    @property
    def reviewing_state(self):
        if not self.reviews:
            return AbstractReviewingState.not_started
        track_states = {
            x: self.get_track_reviewing_state(x)
            for x in self.reviewed_for_tracks
        }
        positiveish_states = {
            AbstractReviewingState.positive, AbstractReviewingState.conflicting
        }
        if any(x == AbstractReviewingState.not_started
               for x in track_states.itervalues()):
            return AbstractReviewingState.in_progress
        elif all(x == AbstractReviewingState.negative
                 for x in track_states.itervalues()):
            return AbstractReviewingState.negative
        elif all(x in positiveish_states for x in track_states.itervalues()):
            if len(self.reviewed_for_tracks) > 1:
                # Accepted for more than one track
                return AbstractReviewingState.conflicting
            elif any(x == AbstractReviewingState.conflicting
                     for x in track_states.itervalues()):
                # The only accepted track is in conflicting state
                return AbstractReviewingState.conflicting
            else:
                return AbstractReviewingState.positive
        else:
            return AbstractReviewingState.mixed

    @property
    def score(self):
        scores = [x.score for x in self.reviews if x.score is not None]
        if not scores:
            return None
        return sum(scores) / len(scores)

    @property
    def data_by_field(self):
        return {
            value.contribution_field_id: value
            for value in self.field_values
        }

    @locator_property
    def locator(self):
        return dict(self.event.locator, abstract_id=self.id)

    @hybrid_property
    def judgment_comment(self):
        return MarkdownText(self._judgment_comment)

    @judgment_comment.setter
    def judgment_comment(self, value):
        self._judgment_comment = value

    @judgment_comment.expression
    def judgment_comment(cls):
        return cls._judgment_comment

    @property
    def verbose_title(self):
        return '#{} ({})'.format(self.friendly_id, self.title)

    @property
    def is_in_final_state(self):
        return self.state != AbstractState.submitted

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'event_id',
                           is_deleted=False,
                           _text=text_to_repr(self.title))

    def can_access(self, user):
        if not user:
            return False
        if self.submitter == user:
            return True
        if self.event.can_manage(user):
            return True
        if any(x.person.user == user for x in self.person_links):
            return True
        return self.can_judge(user) or self.can_convene(
            user) or self.can_review(user)

    def can_comment(self, user, check_state=False):
        if not user:
            return False
        if check_state and self.is_in_final_state:
            return False
        if not self.event.cfa.allow_comments:
            return False
        if self.user_owns(
                user) and self.event.cfa.allow_contributors_in_comments:
            return True
        return self.can_judge(user) or self.can_convene(
            user) or self.can_review(user)

    def can_convene(self, user):
        if not user:
            return False
        elif not self.event.can_manage(
                user, role='track_convener', explicit_role=True):
            return False
        elif self.event in user.global_convener_for_events:
            return True
        elif user.convener_for_tracks & self.reviewed_for_tracks:
            return True
        else:
            return False

    def can_review(self, user, check_state=False):
        # The total number of tracks/events a user is a reviewer for (fossir-wide)
        # is usually reasonably low so we just access the relationships instead of
        # sending a more specific query which would need to be cached to avoid
        # repeating it when performing this check on many abstracts.
        if not user:
            return False
        elif check_state and self.public_state not in (
                AbstractPublicState.under_review,
                AbstractPublicState.awaiting):
            return False
        elif not self.event.can_manage(
                user, role='abstract_reviewer', explicit_role=True):
            return False
        elif self.event in user.global_abstract_reviewer_for_events:
            return True
        elif user.abstract_reviewer_for_tracks & self.reviewed_for_tracks:
            return True
        else:
            return False

    def can_judge(self, user, check_state=False):
        if not user:
            return False
        elif check_state and self.state != AbstractState.submitted:
            return False
        elif self.event.can_manage(user):
            return True
        elif self.event.cfa.allow_convener_judgment and self.can_convene(user):
            return True
        else:
            return False

    def can_edit(self, user):
        if not user:
            return False
        is_manager = self.event.can_manage(user)
        if not self.user_owns(user) and not is_manager:
            return False
        elif is_manager and self.public_state in (
                AbstractPublicState.under_review,
                AbstractPublicState.withdrawn):
            return True
        elif (self.public_state == AbstractPublicState.awaiting
              and (is_manager or self.event.cfa.can_edit_abstracts(user))):
            return True
        else:
            return False

    def can_withdraw(self, user, check_state=False):
        if not user:
            return False
        elif self.event.can_manage(user) and (
                not check_state or self.state != AbstractState.withdrawn):
            return True
        elif user == self.submitter and (not check_state or self.state
                                         == AbstractState.submitted):
            return True
        else:
            return False

    def can_see_reviews(self, user):
        return self.can_judge(user) or self.can_convene(user)

    def get_timeline(self, user=None):
        comments = [x for x in self.comments
                    if x.can_view(user)] if user else self.comments
        reviews = [x for x in self.reviews
                   if x.can_view(user)] if user else self.reviews
        return sorted(chain(comments, reviews), key=attrgetter('created_dt'))

    def get_track_reviewing_state(self, track):
        if track not in self.reviewed_for_tracks:
            raise ValueError("Abstract not in review for given track")
        reviews = self.get_reviews(group=track)
        if not reviews:
            return AbstractReviewingState.not_started
        rejections = any(x.proposed_action == AbstractAction.reject
                         for x in reviews)
        acceptances = {
            x
            for x in reviews if x.proposed_action == AbstractAction.accept
        }
        if rejections and not acceptances:
            return AbstractReviewingState.negative
        elif acceptances and not rejections:
            proposed_contrib_types = {
                x.proposed_contribution_type
                for x in acceptances
                if x.proposed_contribution_type is not None
            }
            if len(proposed_contrib_types) <= 1:
                return AbstractReviewingState.positive
            else:
                return AbstractReviewingState.conflicting
        else:
            return AbstractReviewingState.mixed

    def get_track_question_scores(self):
        query = (db.session.query(
            AbstractReview.track_id, AbstractReviewQuestion,
            db.func.avg(AbstractReviewRating.value)).join(
                AbstractReviewRating.review).join(
                    AbstractReviewRating.question).filter(
                        AbstractReview.abstract == self,
                        ~AbstractReviewQuestion.is_deleted,
                        ~AbstractReviewQuestion.no_score).group_by(
                            AbstractReview.track_id,
                            AbstractReviewQuestion.id))
        scores = defaultdict(lambda: defaultdict(lambda: None))
        for track_id, question, score in query:
            scores[track_id][question] = score
        return scores

    def get_reviewed_for_groups(self, user, include_reviewed=False):
        already_reviewed = {
            each.track
            for each in self.get_reviews(user=user)
        } if include_reviewed else set()
        if self.event in user.global_abstract_reviewer_for_events:
            return self.reviewed_for_tracks | already_reviewed
        return (self.reviewed_for_tracks
                & user.abstract_reviewer_for_tracks) | already_reviewed

    def get_track_score(self, track):
        if track not in self.reviewed_for_tracks:
            raise ValueError("Abstract not in review for given track")
        reviews = [x for x in self.reviews if x.track == track]
        scores = [x.score for x in reviews if x.score is not None]
        if not scores:
            return None
        return sum(scores) / len(scores)

    def reset_state(self):
        self.state = AbstractState.submitted
        self.judgment_comment = ''
        self.judge = None
        self.judgment_dt = None
        self.accepted_track = None
        self.accepted_contrib_type = None
        self.merged_into = None
        self.duplicate_of = None

    def user_owns(self, user):
        if not user:
            return None
        return user == self.submitter or any(x.person.user == user
                                             for x in self.person_links)
示例#24
0
class VCRoomEventAssociation(db.Model):
    __tablename__ = 'vc_room_events'
    __table_args__ = tuple(_make_checks()) + (db.Index(None, 'data', postgresql_using='gin'),
                                              {'schema': 'events'})

    #: Association ID
    id = db.Column(
        db.Integer,
        primary_key=True
    )

    #: ID of the event
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        autoincrement=False,
        nullable=False
    )
    #: ID of the videoconference room
    vc_room_id = db.Column(
        db.Integer,
        db.ForeignKey('events.vc_rooms.id'),
        index=True,
        nullable=False
    )
    #: Type of the object the vc_room is linked to
    link_type = db.Column(
        PyIntEnum(VCRoomLinkType),
        nullable=False
    )
    linked_event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=True
    )
    session_block_id = db.Column(
        db.Integer,
        db.ForeignKey('events.session_blocks.id'),
        index=True,
        nullable=True
    )
    contribution_id = db.Column(
        db.Integer,
        db.ForeignKey('events.contributions.id'),
        index=True,
        nullable=True
    )
    #: If the vc room should be shown on the event page
    show = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: videoconference plugin-specific data
    data = db.Column(
        JSONB,
        nullable=False
    )

    #: The associated :class:VCRoom
    vc_room = db.relationship(
        'VCRoom',
        lazy=False,
        backref=db.backref('events', cascade='all, delete-orphan')
    )
    #: The associated Event
    event = db.relationship(
        'Event',
        foreign_keys=event_id,
        lazy=True,
        backref=db.backref(
            'all_vc_room_associations',
            lazy='dynamic'
        )
    )
    #: The linked event (if the VC room is attached to the event itself)
    linked_event = db.relationship(
        'Event',
        foreign_keys=linked_event_id,
        lazy=True,
        backref=db.backref(
            'vc_room_associations',
            lazy=True
        )
    )
    #: The linked contribution (if the VC room is attached to a contribution)
    linked_contrib = db.relationship(
        'Contribution',
        lazy=True,
        backref=db.backref(
            'vc_room_associations',
            lazy=True
        )
    )
    #: The linked session block (if the VC room is attached to a block)
    linked_block = db.relationship(
        'SessionBlock',
        lazy=True,
        backref=db.backref(
            'vc_room_associations',
            lazy=True
        )
    )

    @classmethod
    def register_link_events(cls):
        event_mapping = {cls.linked_block: lambda x: x.event,
                         cls.linked_contrib: lambda x: x.event,
                         cls.linked_event: lambda x: x}

        type_mapping = {cls.linked_event: VCRoomLinkType.event,
                        cls.linked_block: VCRoomLinkType.block,
                        cls.linked_contrib: VCRoomLinkType.contribution}

        def _set_link_type(link_type, target, value, *unused):
            if value is not None:
                target.link_type = link_type

        def _set_event_obj(fn, target, value, *unused):
            if value is not None:
                event = fn(value)
                assert event is not None
                target.event = event

        for rel, fn in event_mapping.iteritems():
            if rel is not None:
                listen(rel, 'set', partial(_set_event_obj, fn))

        for rel, link_type in type_mapping.iteritems():
            if rel is not None:
                listen(rel, 'set', partial(_set_link_type, link_type))

    @property
    def locator(self):
        return dict(self.event.locator, service=self.vc_room.type, event_vc_room_id=self.id)

    @hybrid_property
    def link_object(self):
        if self.link_type == VCRoomLinkType.event:
            return self.linked_event
        elif self.link_type == VCRoomLinkType.contribution:
            return self.linked_contrib
        else:
            return self.linked_block

    @link_object.setter
    def link_object(self, obj):
        self.linked_event = self.linked_contrib = self.linked_block = None
        if isinstance(obj, db.m.Event):
            self.linked_event = obj
        elif isinstance(obj, db.m.Contribution):
            self.linked_contrib = obj
        elif isinstance(obj, db.m.SessionBlock):
            self.linked_block = obj
        else:
            raise TypeError('Unexpected object: {}'.format(obj))

    @link_object.comparator
    def link_object(cls):
        return _LinkObjectComparator(cls)

    @return_ascii
    def __repr__(self):
        return '<VCRoomEventAssociation({}, {})>'.format(self.event_id, self.vc_room)

    @classmethod
    def find_for_event(cls, event, include_hidden=False, include_deleted=False, only_linked_to_event=False, **kwargs):
        """Returns a Query that retrieves the videoconference rooms for an event

        :param event: an fossir Event
        :param only_linked_to_event: only retrieve the vc rooms linked to the whole event
        :param kwargs: extra kwargs to pass to ``find()``
        """
        if only_linked_to_event:
            kwargs['link_type'] = int(VCRoomLinkType.event)
        query = event.all_vc_room_associations
        if kwargs:
            query = query.filter_by(**kwargs)
        if not include_hidden:
            query = query.filter(cls.show)
        if not include_deleted:
            query = query.filter(VCRoom.status != VCRoomStatus.deleted).join(VCRoom)
        return query

    @classmethod
    @memoize_request
    def get_linked_for_event(cls, event):
        """Get a dict mapping link objects to event vc rooms"""
        return {vcr.link_object: vcr for vcr in cls.find_for_event(event)}

    def delete(self, user, delete_all=False):
        """Deletes a VC room from an event

        If the room is not used anywhere else, the room itself is also deleted.

        :param user: the user performing the deletion
        :param delete_all: if True, the room is detached from all
                           events and deleted.
        """
        vc_room = self.vc_room
        if delete_all:
            for assoc in vc_room.events[:]:
                Logger.get('modules.vc').info("Detaching VC room {} from event {} ({})".format(
                    vc_room, assoc.event, assoc.link_object)
                )
                vc_room.events.remove(assoc)
        else:
            Logger.get('modules.vc').info("Detaching VC room {} from event {} ({})".format(
                vc_room, self.event, self.link_object)
            )
            vc_room.events.remove(self)
        db.session.flush()
        if not vc_room.events:
            Logger.get('modules.vc').info("Deleting VC room {}".format(vc_room))
            if vc_room.status != VCRoomStatus.deleted:
                vc_room.plugin.delete_room(vc_room, self.event)
                notify_deleted(vc_room.plugin, vc_room, self, self.event, user)
            db.session.delete(vc_room)
示例#25
0
 def __auto_table_args():
     return (db.Index(None, 'category_id', 'module',
                      'name'), db.Index(None, 'category_id', 'module'),
             db.UniqueConstraint('category_id', 'module', 'name'), {
                 'schema': 'categories'
             })
 def __table_args__(cls):
     return (db.Index('ix_reservations_start_dt_date', cast(cls.start_dt, Date)),
             db.Index('ix_reservations_end_dt_date', cast(cls.end_dt, Date)),
             db.Index('ix_reservations_start_dt_time', cast(cls.start_dt, Time)),
             db.Index('ix_reservations_end_dt_time', cast(cls.end_dt, Time)),
             {'schema': 'roombooking'})
示例#27
0
 def __table_args__(cls):
     return (db.Index('ix_uq_ip_network_groups_name_lower',
                      db.func.lower(cls.name),
                      unique=True), {
                          'schema': 'fossir'
                      })