Example #1
0
 def is_deleted(cls):
     return db.Column(db.Boolean, nullable=False, default=False)
Example #2
0
class ContributionField(db.Model):
    __tablename__ = 'contribution_fields'
    __table_args__ = (db.UniqueConstraint('event_id', 'legacy_id'), {
        'schema': 'events'
    })

    id = db.Column(db.Integer, primary_key=True)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    legacy_id = db.Column(db.String, nullable=True)
    position = db.Column(db.Integer,
                         nullable=False,
                         default=_get_next_position)
    title = db.Column(db.String, nullable=False)
    description = db.Column(db.Text, nullable=False, default='')
    is_required = db.Column(db.Boolean, nullable=False, default=False)
    is_active = db.Column(db.Boolean, nullable=False, default=True)
    is_user_editable = db.Column(db.Boolean, nullable=False, default=True)
    visibility = db.Column(PyIntEnum(ContributionFieldVisibility),
                           nullable=False,
                           default=ContributionFieldVisibility.public)
    field_type = db.Column(db.String, nullable=True)
    field_data = db.Column(JSONB, nullable=False, default={})

    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('contribution_fields',
                                               order_by=position,
                                               cascade='all, delete-orphan',
                                               lazy='dynamic'))

    # relationship backrefs:
    # - abstract_values (AbstractFieldValue.contribution_field)
    # - contribution_values (ContributionFieldValue.contribution_field)

    def _get_field(self, management=False):
        from indico.modules.events.contributions import get_contrib_field_types
        try:
            impl = get_contrib_field_types()[self.field_type]
        except KeyError:
            return None
        return impl(self, management=management)

    @property
    def field(self):
        return self._get_field()

    @property
    def mgmt_field(self):
        return self._get_field(management=True)

    @property
    def is_public(self):
        return self.visibility == ContributionFieldVisibility.public

    @property
    def filter_choices(self):
        return {
            x['id']: x['option']
            for x in self.field_data.get('options', {})
        }

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'field_type',
                           is_required=False,
                           is_active=True,
                           _text=self.title)

    @locator_property
    def locator(self):
        return dict(self.event.locator, contrib_field_id=self.id)
Example #3
0
 def content_type(cls):
     """The MIME type of the file"""
     return db.Column(
         db.String,
         nullable=not cls.file_required
     )
Example #4
0
 def subcontribution_id(cls):
     return db.Column(
         db.String,
         nullable=True
     )
Example #5
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 indico.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)
Example #6
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,
                                  postgresql_where=db.text('NOT is_deleted')),
                         {
                             '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)
    type_id = db.Column(db.Integer,
                        db.ForeignKey('events.session_types.id'),
                        index=True,
                        nullable=True)
    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_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))
    type = db.relationship('SessionType',
                           lazy=True,
                           backref=db.backref('sessions', lazy=True))

    # relationship backrefs:
    # - attachment_folders (AttachmentFolder.session)
    # - contributions (Contribution.session)
    # - default_for_tracks (Track.default_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().__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 indico.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 indico.modules.events.sessions.models.blocks import SessionBlock
        from indico.modules.events.sessions.models.persons import SessionBlockPersonLink

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

    @property
    def is_poster(self):
        return self.type.is_poster if self.type else False

    @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)

    def __repr__(self):
        return format_repr(self, 'id', 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 indico.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 indico.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
Example #7
0
class Abstract(ProposalMixin, ProposalRevisionMixin, DescriptionMixin,
               CustomFieldsMixin, AuthorsSpeakersMixin, db.Model):
    """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'),
        db.CheckConstraint('(state != {}) OR (uuid IS NOT NULL)'.format(
            AbstractState.invited),
                           name='uuid_if_invited'), {
                               '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

    AUTHORS_SPEAKERS_DISPLAY_ORDER_ATTR = 'display_order_key_lastname'

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

    id = db.Column(db.Integer, primary_key=True)
    uuid = db.Column(UUID, index=True, unique=True, nullable=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)

    @locator.token
    def locator(self):
        return dict(self.event.locator, uuid=self.uuid)

    @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

    @property
    def modification_ended(self):
        return self.event.cfa.modification_ended

    @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, permission='track_convener', explicit_permission=True):
            return False
        elif self.event.can_manage(user,
                                   permission='convene_all_abstracts',
                                   explicit_permission=True):
            return True
        elif any(
                track.can_manage(
                    user, permission='convene', explicit_permission=True)
                for track in 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 (indico-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,
                                       permission='abstract_reviewer',
                                       explicit_permission=True):
            return False
        elif self.event.can_manage(user,
                                   permission='review_all_abstracts',
                                   explicit_permission=True):
            return True
        elif any(
                track.can_manage(
                    user, permission='review', explicit_permission=True)
                for track in 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

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

        editing_allowed = self.event.cfa.allow_editing
        author_type = next(
            (x.author_type
             for x in self.person_links if x.person.user == user), None)
        is_primary = author_type == AuthorType.primary
        is_secondary = author_type == AuthorType.secondary
        if user == self.submitter:
            return True
        elif editing_allowed == AllowEditingType.submitter_all:
            return True
        elif editing_allowed == AllowEditingType.submitter_primary and is_primary:
            return True
        elif editing_allowed == AllowEditingType.submitter_authors and (
                is_primary or is_secondary):
            return True
        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.op('#>>')('{}').cast(
                    db.Integer))).join(AbstractReviewRating.review).join(
                        AbstractReviewRating.question).filter(
                            AbstractReview.abstract == self,
                            AbstractReviewQuestion.field_type == 'rating',
                            db.func.jsonb_typeof(
                                AbstractReviewRating.value) == 'null',
                            ~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.can_manage(user,
                                 permission='review_all_abstracts',
                                 explicit_permission=True):
            return self.reviewed_for_tracks | already_reviewed
        reviewer_tracks = {
            track
            for track in self.reviewed_for_tracks if track.can_manage(
                user, permission='review', explicit_permission=True)
        }
        return reviewer_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)

    def log(self, *args, **kwargs):
        """Log with prefilled metadata for the abstract."""
        self.event.log(*args, meta={'abstract_id': self.id}, **kwargs)
Example #8
0
class Category(SearchableTitleMixin, DescriptionMixin, ProtectionManagersMixin,
               AttachedItemsMixin, db.Model):
    """An Indico category"""

    __tablename__ = 'categories'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    allow_no_access_contact = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'category_id'

    @strict_classproperty
    @classmethod
    def __auto_table_args(cls):
        return (db.CheckConstraint(
            "(icon IS NULL) = (icon_metadata::text = 'null')", 'valid_icon'),
                db.CheckConstraint(
                    "(logo IS NULL) = (logo_metadata::text = 'null')",
                    'valid_logo'),
                db.CheckConstraint("(parent_id IS NULL) = (id = 0)",
                                   'valid_parent'),
                db.CheckConstraint("(id != 0) OR NOT is_deleted",
                                   'root_not_deleted'),
                db.CheckConstraint(
                    "(id != 0) OR (protection_mode != {})".format(
                        ProtectionMode.inheriting), 'root_not_inheriting'),
                db.CheckConstraint('visibility IS NULL OR visibility > 0',
                                   'valid_visibility'), {
                                       'schema': 'categories'
                                   })

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

    id = db.Column(db.Integer, primary_key=True)
    parent_id = db.Column(db.Integer,
                          db.ForeignKey('categories.categories.id'),
                          index=True,
                          nullable=True)
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    position = db.Column(db.Integer,
                         nullable=False,
                         default=_get_next_position)
    visibility = db.Column(db.Integer, nullable=True, default=None)
    icon_metadata = db.Column(JSONB, nullable=False, default=lambda: None)
    icon = db.deferred(db.Column(db.LargeBinary, nullable=True))
    logo_metadata = db.Column(JSONB, nullable=False, default=lambda: None)
    logo = db.deferred(db.Column(db.LargeBinary, nullable=True))
    timezone = db.Column(db.String,
                         nullable=False,
                         default=lambda: config.DEFAULT_TIMEZONE)
    default_event_themes = db.Column(JSONB,
                                     nullable=False,
                                     default=_get_default_event_themes)
    event_creation_restricted = db.Column(db.Boolean,
                                          nullable=False,
                                          default=True)
    event_creation_notification_emails = db.Column(ARRAY(db.String),
                                                   nullable=False,
                                                   default=[])
    event_message_mode = db.Column(PyIntEnum(EventMessageMode),
                                   nullable=False,
                                   default=EventMessageMode.disabled)
    _event_message = db.Column('event_message',
                               db.Text,
                               nullable=False,
                               default='')
    suggestions_disabled = db.Column(db.Boolean, nullable=False, default=False)
    notify_managers = db.Column(db.Boolean, nullable=False, default=False)
    default_ticket_template_id = db.Column(
        db.ForeignKey('indico.designer_templates.id'),
        nullable=True,
        index=True)

    children = db.relationship(
        'Category',
        order_by='Category.position',
        primaryjoin=(id == db.remote(parent_id)) & ~db.remote(is_deleted),
        lazy=True,
        backref=db.backref('parent',
                           primaryjoin=(db.remote(id) == parent_id),
                           lazy=True))
    acl_entries = db.relationship('CategoryPrincipal',
                                  backref='category',
                                  cascade='all, delete-orphan',
                                  collection_class=set)
    default_ticket_template = db.relationship(
        'DesignerTemplate',
        lazy=True,
        foreign_keys=default_ticket_template_id,
        backref='default_ticket_template_of')

    # column properties:
    # - deep_events_count

    # relationship backrefs:
    # - attachment_folders (AttachmentFolder.category)
    # - designer_templates (DesignerTemplate.category)
    # - events (Event.category)
    # - favorite_of (User.favorite_categories)
    # - legacy_mapping (LegacyCategoryMapping.category)
    # - parent (Category.children)
    # - roles (CategoryRole.category)
    # - settings (CategorySetting.category)
    # - suggestions (SuggestedCategory.category)

    @hybrid_property
    def event_message(self):
        return MarkdownText(self._event_message)

    @event_message.setter
    def event_message(self, value):
        self._event_message = value

    @event_message.expression
    def event_message(cls):
        return cls._event_message

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

    @property
    def protection_parent(self):
        return self.parent if not self.is_root else None

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

    @classmethod
    def get_root(cls):
        """Get the root category"""
        return cls.query.filter(cls.is_root).one()

    @property
    def url(self):
        return url_for('categories.display', self)

    @property
    def has_only_events(self):
        return self.has_events and not self.children

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

    @is_root.expression
    def is_root(cls):
        return cls.parent_id.is_(None)

    @property
    def is_empty(self):
        return not self.deep_children_count and not self.deep_events_count

    @property
    def has_icon(self):
        return self.icon_metadata is not None

    @property
    def has_effective_icon(self):
        return self.effective_icon_data['metadata'] is not None

    @property
    def has_logo(self):
        return self.logo_metadata is not None

    @property
    def tzinfo(self):
        return pytz.timezone(self.timezone)

    @property
    def display_tzinfo(self):
        """The tzinfo of the category or the one specified by the user"""
        return get_display_tz(self, as_timezone=True)

    def can_create_events(self, user):
        """Check whether the user can create events in the category."""
        # if creation is not restricted anyone who can access the category
        # can also create events in it, otherwise only people with the
        # creation role can
        return user and (
            (not self.event_creation_restricted and self.can_access(user))
            or self.can_manage(user, permission='create'))

    def move(self, target):
        """Move the category into another category."""
        assert not self.is_root
        old_parent = self.parent
        self.position = (max(x.position for x in target.children) +
                         1) if target.children else 1
        self.parent = target
        db.session.flush()
        signals.category.moved.send(self, old_parent=old_parent)

    @classmethod
    def get_tree_cte(cls, col='id'):
        """Create a CTE for the category tree.

        The CTE contains the following columns:

        - ``id`` -- the category id
        - ``path`` -- an array containing the path from the root to
                      the category itself
        - ``is_deleted`` -- whether the category is deleted

        :param col: The name of the column to use in the path or a
                    callable receiving the category alias that must
                    return the expression used for the 'path'
                    retrieved by the CTE.
        """
        cat_alias = db.aliased(cls)
        if callable(col):
            path_column = col(cat_alias)
        else:
            path_column = getattr(cat_alias, col)
        cte_query = (select([
            cat_alias.id,
            array([path_column]).label('path'), cat_alias.is_deleted
        ]).where(cat_alias.parent_id.is_(None)).cte(recursive=True))
        rec_query = (select([
            cat_alias.id,
            cte_query.c.path.op('||')(path_column),
            cte_query.c.is_deleted | cat_alias.is_deleted
        ]).where(cat_alias.parent_id == cte_query.c.id))
        return cte_query.union_all(rec_query)

    @classmethod
    def get_protection_cte(cls):
        cat_alias = db.aliased(cls)
        cte_query = (select([cat_alias.id, cat_alias.protection_mode]).where(
            cat_alias.parent_id.is_(None)).cte(recursive=True))
        rec_query = (select([
            cat_alias.id,
            db.case(
                {ProtectionMode.inheriting.value: cte_query.c.protection_mode},
                else_=cat_alias.protection_mode,
                value=cat_alias.protection_mode)
        ]).where(cat_alias.parent_id == cte_query.c.id))
        return cte_query.union_all(rec_query)

    def get_protection_parent_cte(self):
        cte_query = (select([
            Category.id,
            db.cast(literal(None), db.Integer).label('protection_parent')
        ]).where(Category.id == self.id).cte(recursive=True))
        rec_query = (select([
            Category.id,
            db.case(
                {
                    ProtectionMode.inheriting.value:
                    func.coalesce(cte_query.c.protection_parent, self.id)
                },
                else_=Category.id,
                value=Category.protection_mode)
        ]).where(Category.parent_id == cte_query.c.id))
        return cte_query.union_all(rec_query)

    @classmethod
    def get_icon_data_cte(cls):
        cat_alias = db.aliased(cls)
        cte_query = (select([
            cat_alias.id,
            cat_alias.id.label('source_id'), cat_alias.icon_metadata
        ]).where(cat_alias.parent_id.is_(None)).cte(recursive=True))
        rec_query = (select([
            cat_alias.id,
            db.case({'null': cte_query.c.source_id},
                    else_=cat_alias.id,
                    value=db.func.jsonb_typeof(cat_alias.icon_metadata)),
            db.case({'null': cte_query.c.icon_metadata},
                    else_=cat_alias.icon_metadata,
                    value=db.func.jsonb_typeof(cat_alias.icon_metadata))
        ]).where(cat_alias.parent_id == cte_query.c.id))
        return cte_query.union_all(rec_query)

    @property
    def deep_children_query(self):
        """Get a query object for all subcategories.

        This includes subcategories at any level of nesting.
        """
        cte = Category.get_tree_cte()
        return (Category.query.join(cte, Category.id == cte.c.id).filter(
            cte.c.path.contains([self.id]), cte.c.id != self.id,
            ~cte.c.is_deleted))

    @staticmethod
    def _get_chain_query(start_criterion):
        cte_query = (select([
            Category.id, Category.parent_id,
            literal(0).label('level')
        ]).where(start_criterion).cte('category_chain', recursive=True))
        parent_query = (select([
            Category.id, Category.parent_id, cte_query.c.level + 1
        ]).where(Category.id == cte_query.c.parent_id))
        cte_query = cte_query.union_all(parent_query)
        return Category.query.join(cte_query,
                                   Category.id == cte_query.c.id).order_by(
                                       cte_query.c.level.desc())

    @property
    def chain_query(self):
        """Get a query object for the category chain.

        The query retrieves the root category first and then all the
        intermediate categories up to (and including) this category.
        """
        return self._get_chain_query(Category.id == self.id)

    @property
    def parent_chain_query(self):
        """Get a query object for the category's parent chain.

        The query retrieves the root category first and then all the
        intermediate categories up to (excluding) this category.
        """
        return self._get_chain_query(Category.id == self.parent_id)

    def nth_parent(self, n_categs, fail_on_overflow=True):
        """Return the nth parent of the category.

        :param n_categs: the number of categories to go up
        :param fail_on_overflow: whether to fail if we try to go above the root category
        :return: `Category` object or None (only if ``fail_on_overflow`` is not set)
        """
        if n_categs == 0:
            return self
        chain = self.parent_chain_query.all()

        assert n_categs >= 0
        if n_categs > len(chain):
            if fail_on_overflow:
                raise IndexError("Root category has no parent!")
            else:
                return None
        return chain[::-1][n_categs - 1]

    def is_descendant_of(self, categ):
        return categ != self and self.parent_chain_query.filter(
            Category.id == categ.id).has_rows()

    @property
    def visibility_horizon_query(self):
        """Get a query object that returns the highest category this one is visible from."""
        cte_query = (select([
            Category.id, Category.parent_id,
            db.case([(Category.visibility.is_(None), None)],
                    else_=(Category.visibility - 1)).label('n'),
            literal(0).label('level')
        ]).where(Category.id == self.id).cte('visibility_horizon',
                                             recursive=True))
        parent_query = (select([
            Category.id, Category.parent_id,
            db.case([
                (Category.visibility.is_(None) & cte_query.c.n.is_(None), None)
            ],
                    else_=db.func.least(Category.visibility, cte_query.c.n) -
                    1), cte_query.c.level + 1
        ]).where(
            db.and_(Category.id == cte_query.c.parent_id,
                    (cte_query.c.n > 0) | cte_query.c.n.is_(None))))
        cte_query = cte_query.union_all(parent_query)
        return db.session.query(cte_query.c.id, cte_query.c.n).order_by(
            cte_query.c.level.desc()).limit(1)

    @property
    def own_visibility_horizon(self):
        """Get the highest category this one would like to be visible from (configured visibility)."""
        if self.visibility is None:
            return Category.get_root()
        else:
            return self.nth_parent(self.visibility - 1)

    @property
    def real_visibility_horizon(self):
        """Get the highest category this one is actually visible from (as limited by categories above)."""
        horizon_id, final_visibility = self.visibility_horizon_query.one()
        if final_visibility is not None and final_visibility < 0:
            return None  # Category is invisible
        return Category.get(horizon_id)

    @staticmethod
    def get_visible_categories_cte(category_id):
        """
        Get a sqlalchemy select for the visible categories within
        the given category, including the category itself.
        """
        cte_query = (select([
            Category.id, literal(0).label('level')
        ]).where((Category.id == category_id)
                 & (Category.visibility.is_(None)
                    | (Category.visibility > 0))).cte(recursive=True))
        parent_query = (select([Category.id, cte_query.c.level + 1]).where(
            db.and_(
                Category.parent_id == cte_query.c.id,
                db.or_(Category.visibility.is_(None),
                       Category.visibility > cte_query.c.level + 1))))
        return cte_query.union_all(parent_query)

    @property
    def visible_categories_query(self):
        """
        Get a query object for the visible categories within
        this category, including the category itself.
        """
        cte_query = Category.get_visible_categories_cte(self.id)
        return Category.query.join(cte_query, Category.id == cte_query.c.id)

    @property
    def icon_url(self):
        """Get the HTTP URL of the icon."""
        return url_for('categories.display_icon',
                       self,
                       slug=self.icon_metadata['hash'])

    @property
    def effective_icon_url(self):
        """Get the HTTP URL of the icon (possibly inherited)."""
        data = self.effective_icon_data
        return url_for('categories.display_icon',
                       category_id=data['source_id'],
                       slug=data['metadata']['hash'])

    @property
    def logo_url(self):
        """Get the HTTP URL of the logo."""
        return url_for('categories.display_logo',
                       self,
                       slug=self.logo_metadata['hash'])
Example #9
0
class DesignerTemplate(db.Model):
    __tablename__ = 'designer_templates'
    __table_args__ = (db.CheckConstraint(
        '(event_id IS NULL) != (category_id IS NULL)',
        'event_xor_category_id_null'), {
            'schema': 'indico'
        })

    id = db.Column(db.Integer, primary_key=True)
    type = db.Column(PyIntEnum(TemplateType), nullable=False)
    title = db.Column(db.String, nullable=False)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=True)
    category_id = db.Column(db.Integer,
                            db.ForeignKey('categories.categories.id'),
                            index=True,
                            nullable=True)
    data = db.Column(JSONB, nullable=False)
    background_image_id = db.Column(
        db.Integer,
        db.ForeignKey('indico.designer_image_files.id'),
        index=False,
        nullable=True)
    backside_template_id = db.Column(
        db.ForeignKey('indico.designer_templates.id'),
        index=True,
        nullable=True)
    is_clonable = db.Column(db.Boolean, nullable=False, default=True)
    is_system_template = db.Column(db.Boolean, nullable=False, default=False)
    category = db.relationship('Category',
                               lazy=True,
                               foreign_keys=category_id,
                               backref=db.backref('designer_templates',
                                                  cascade='all, delete-orphan',
                                                  lazy=True))
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('designer_templates',
                                               cascade='all, delete-orphan',
                                               lazy=True))
    background_image = db.relationship('DesignerImageFile',
                                       lazy=True,
                                       foreign_keys=background_image_id,
                                       post_update=True)
    backside_template = db.relationship('DesignerTemplate',
                                        lazy=True,
                                        remote_side=id,
                                        backref='backside_template_of')

    # relationship backrefs:
    # - backside_template_of (DesignerTemplate.backside_template)
    # - default_badge_template_of (Category.default_badge_template)
    # - default_ticket_template_of (Category.default_ticket_template)
    # - images (DesignerImageFile.template)
    # - ticket_for_regforms (RegistrationForm.ticket_template)

    def __init__(self, **kwargs):
        data = kwargs.pop('data', None)
        tpl_type = kwargs.get('type')
        if data is None:
            data = {'items': [], 'background_position': 'stretch'}
            size = DEFAULT_CONFIG[tpl_type]['tpl_size']
            data.update({'width': size[0], 'height': size[1]})
        super().__init__(data=data, **kwargs)

    @hybrid_property
    def owner(self):
        return self.event if self.event else self.category

    @owner.comparator
    def owner(cls):
        return _OwnerComparator(cls)

    @locator_property
    def locator(self):
        return dict(self.owner.locator, template_id=self.id)

    @property
    def is_ticket(self):
        placeholders = get_placeholders('designer-fields')
        if any(placeholders[item['type']].is_ticket
               for item in self.data['items'] if item['type'] in placeholders):
            return True
        elif self.backside_template and self.backside_template.is_ticket:
            return True
        else:
            return False

    def __repr__(self):
        return format_repr(self,
                           'id',
                           'event_id',
                           'category_id',
                           _text=self.title)
Example #10
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_new = 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_new,
            cls.linked_contrib: lambda x: x.event_new,
            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_new = 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_new.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
    @unify_event_args
    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 indico 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_new, assoc.link_object))
                vc_room.events.remove(assoc)
        else:
            Logger.get('modules.vc').info(
                "Detaching VC room {} from event {} ({})".format(
                    vc_room, self.event_new, 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_new)
                notify_deleted(vc_room.plugin, vc_room, self, self.event_new,
                               user)
            db.session.delete(vc_room)
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)
    #: Whether the menu entry should be viewable only by registered users
    registered_only = 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
Example #12
0
        return '<LocalGroup({}, {})>'.format(self.id, self.name)

    @property
    def proxy(self):
        """Returns a GroupProxy wrapping this group"""
        from indico.modules.groups import GroupProxy
        return GroupProxy(self.id, _group=self)


group_members_table = db.Table(
    'group_members',
    db.metadata,
    db.Column(
        'group_id',
        db.Integer,
        db.ForeignKey('users.groups.id'),
        primary_key=True,
        nullable=False,
        index=True
    ),
    db.Column(
        'user_id',
        db.Integer,
        db.ForeignKey('users.users.id'),
        primary_key=True,
        nullable=False,
        index=True
    ),
    schema='users'
)
Example #13
0
class Agreement(db.Model):
    """Agreements between a person and Indico"""
    __tablename__ = 'agreements'
    __table_args__ = (db.UniqueConstraint('event_id', 'type', 'identifier'), {
        'schema': 'events'
    })

    #: Entry ID
    id = db.Column(db.Integer, primary_key=True)
    #: Entry universally unique ID
    uuid = db.Column(db.String, nullable=False)
    #: ID of the event
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         nullable=False,
                         index=True)
    #: Type of agreement
    type = db.Column(db.String, nullable=False)
    #: Unique identifier within the event and type
    identifier = db.Column(db.String, nullable=False)
    #: Email of the person agreeing
    person_email = db.Column(db.String, nullable=True)
    #: Full name of the person agreeing
    person_name = db.Column(db.String, nullable=False)
    #: A :class:`AgreementState`
    state = db.Column(PyIntEnum(AgreementState),
                      default=AgreementState.pending,
                      nullable=False)
    #: The date and time the agreement was created
    timestamp = db.Column(UTCDateTime, default=now_utc, nullable=False)
    #: ID of a linked user
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=True)
    #: The date and time the agreement was signed
    signed_dt = db.Column(UTCDateTime)
    #: The IP from which the agreement was signed
    signed_from_ip = db.Column(db.String)
    #: Explanation as to why the agreement was accepted/rejected
    reason = db.Column(db.String)
    #: Attachment
    attachment = db.deferred(db.Column(db.LargeBinary))
    #: Filename and extension of the attachment
    attachment_filename = db.Column(db.String)
    #: Definition-specific data of the agreement
    data = db.Column(JSON)

    #: The user this agreement is linked to
    user = db.relationship('User',
                           lazy=False,
                           backref=db.backref('agreements', lazy='dynamic'))
    #: The Event this agreement is associated with
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('agreements', lazy='dynamic'))

    @hybrid_property
    def accepted(self):
        return self.state in {
            AgreementState.accepted, AgreementState.accepted_on_behalf
        }

    @accepted.expression
    def accepted(self):
        return self.state.in_(
            (AgreementState.accepted, AgreementState.accepted_on_behalf))

    @hybrid_property
    def pending(self):
        return self.state == AgreementState.pending

    @hybrid_property
    def rejected(self):
        return self.state in {
            AgreementState.rejected, AgreementState.rejected_on_behalf
        }

    @rejected.expression
    def rejected(self):
        return self.state.in_(
            (AgreementState.rejected, AgreementState.rejected_on_behalf))

    @hybrid_property
    def signed_on_behalf(self):
        return self.state in {
            AgreementState.accepted_on_behalf,
            AgreementState.rejected_on_behalf
        }

    @signed_on_behalf.expression
    def signed_on_behalf(self):
        return self.state.in_((AgreementState.accepted_on_behalf,
                               AgreementState.rejected_on_behalf))

    @property
    def definition(self):
        from indico.modules.events.agreements.util import get_agreement_definitions
        return get_agreement_definitions().get(self.type)

    @property
    def locator(self):
        return {'confId': self.event_id, 'id': self.id}

    @return_ascii
    def __repr__(self):
        state = self.state.name if self.state is not None else None
        return '<Agreement({}, {}, {}, {}, {}, {})>'.format(
            self.id, self.event_id, self.type, self.identifier,
            self.person_email, state)

    @staticmethod
    def create_from_data(event, type_, person):
        agreement = Agreement(event=event,
                              type=type_,
                              state=AgreementState.pending,
                              uuid=str(uuid4()))
        agreement.identifier = person.identifier
        agreement.person_email = person.email
        agreement.person_name = person.name
        if person.user:
            agreement.user = person.user
        agreement.data = person.data
        return agreement

    def accept(self, from_ip, reason=None, on_behalf=False):
        self.state = AgreementState.accepted if not on_behalf else AgreementState.accepted_on_behalf
        self.signed_from_ip = from_ip
        self.reason = reason
        self.signed_dt = now_utc()
        self.definition.handle_accepted(self)

    def reject(self, from_ip, reason=None, on_behalf=False):
        self.state = AgreementState.rejected if not on_behalf else AgreementState.rejected_on_behalf
        self.signed_from_ip = from_ip
        self.reason = reason
        self.signed_dt = now_utc()
        self.definition.handle_rejected(self)

    def reset(self):
        self.definition.handle_reset(self)
        self.state = AgreementState.pending
        self.attachment = None
        self.attachment_filename = None
        self.reason = None
        self.signed_dt = None
        self.signed_from_ip = None

    def render(self, form, **kwargs):
        definition = self.definition
        if definition is None:
            raise ServiceUnavailable(
                'This agreement type is currently not available.')
        return definition.render_form(self, form, **kwargs)

    def belongs_to(self, person):
        return self.identifier == person.identifier

    def is_orphan(self):
        definition = self.definition
        if definition is None:
            raise ServiceUnavailable(
                'This agreement type is currently not available.')
        return definition.is_agreement_orphan(self.event, self)
Example #14
0
    # - revisions (EditingRevision.tags)

    @property
    def verbose_title(self):
        """Properly formatted title, including tag code."""
        return f'{self.code}: {self.title}'

    def __repr__(self):
        return format_repr(self, 'id', 'event_id', system=False, _text=self.title)


db.Table(
    'revision_tags',
    db.metadata,
    db.Column(
        'revision_id',
        db.ForeignKey('event_editing.revisions.id'),
        primary_key=True,
        autoincrement=False,
        index=True
    ),
    db.Column(
        'tag_id',
        db.ForeignKey('event_editing.tags.id'),
        primary_key=True,
        autoincrement=False,
        index=True
    ),
    schema='event_editing'
)
Example #15
0
class EventNoteRevision(db.Model):
    __tablename__ = 'note_revisions'
    __table_args__ = {'schema': 'events'}

    #: The ID of the revision
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: The ID of the associated note
    note_id = db.Column(
        db.Integer,
        db.ForeignKey('events.notes.id'),
        nullable=False,
        index=True
    )
    #: The user who created the revision
    user_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        nullable=False,
        index=True
    )
    #: The date/time when the revision was created
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc
    )
    #: How the note is rendered
    render_mode = db.Column(
        PyIntEnum(RenderMode),
        nullable=False
    )
    #: The raw source of the note as provided by the user
    source = db.Column(
        db.Text,
        nullable=False
    )
    #: The rendered HTML of the note
    html = db.Column(
        db.Text,
        nullable=False
    )

    #: The user who created the revision
    user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'event_notes_revisions',
            lazy='dynamic'
        )
    )

    # relationship backrefs:
    # - note (EventNote.revisions)

    def __repr__(self):
        render_mode = self.render_mode.name if self.render_mode is not None else None
        source = text_to_repr(self.source, html=True)
        return '<EventNoteRevision({}, {}, {}, {}): "{}">'.format(self.id, self.note_id, render_mode, self.created_dt,
                                                                  source)
Example #16
0
class SurveyItem(db.Model):
    __tablename__ = 'items'
    __table_args__ = (db.CheckConstraint("type != {type} OR ("
                                         "title IS NOT NULL AND "
                                         "is_required IS NOT NULL AND "
                                         "field_type IS NOT NULL AND "
                                         "parent_id IS NOT NULL AND "
                                         "display_as_section IS NULL)"
                                         .format(type=SurveyItemType.question), 'valid_question'),
                      db.CheckConstraint("type != {type} OR ("
                                         "title IS NOT NULL AND "
                                         "is_required IS NULL AND "
                                         "field_type IS NULL AND "
                                         "field_data::text = '{{}}' AND "
                                         "parent_id IS NULL AND "
                                         "display_as_section IS NOT NULL)"
                                         .format(type=SurveyItemType.section), 'valid_section'),
                      db.CheckConstraint("type != {type} OR ("
                                         "title IS NULL AND "
                                         "is_required IS NULL AND "
                                         "field_type IS NULL AND "
                                         "field_data::text = '{{}}' AND "
                                         "parent_id IS NOT NULL AND "
                                         "display_as_section IS NULL)"
                                         .format(type=SurveyItemType.text), 'valid_text'),
                      {'schema': 'event_surveys'})
    __mapper_args__ = {
        'polymorphic_on': 'type',
        'polymorphic_identity': None
    }

    #: The ID of the item
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: The ID of the survey
    survey_id = db.Column(
        db.Integer,
        db.ForeignKey('event_surveys.surveys.id'),
        index=True,
        nullable=False,
    )
    #: The ID of the parent section item (NULL for top-level items, i.e. sections)
    parent_id = db.Column(
        db.Integer,
        db.ForeignKey('event_surveys.items.id'),
        index=True,
        nullable=True,
    )
    #: The position of the item in the survey form
    position = db.Column(
        db.Integer,
        nullable=False,
        default=_get_next_position
    )
    #: The type of the survey item
    type = db.Column(
        PyIntEnum(SurveyItemType),
        nullable=False
    )
    #: The title of the item
    title = db.Column(
        db.String,
        nullable=True,
        default=_get_item_default_title
    )
    #: The description of the item
    description = db.Column(
        db.Text,
        nullable=False,
        default=''
    )
    #: If a section should be rendered as a section
    display_as_section = db.Column(
        db.Boolean,
        nullable=True
    )

    # The following columns are only used for SurveyQuestion objects, but by
    # specifying them here we can access them withouy an extra query when we
    # query SurveyItem objects directly instead of going through a subclass.
    # This is done e.g. when using the Survey.top_level_items relationship.

    #: If the question must be answered (wtforms DataRequired)
    is_required = db.Column(
        db.Boolean,
        nullable=True
    )
    #: The type of the field used for the question
    field_type = db.Column(
        db.String,
        nullable=True
    )
    #: Field-specific data (such as choices for multi-select fields)
    field_data = db.Column(
        JSON,
        nullable=False,
        default={}
    )

    # relationship backrefs:
    # - parent (SurveySection.children)
    # - survey (Survey.items)

    def to_dict(self):
        """Return a json-serializable representation of this object.

        Subclasses must add their own data to the dict.
        """
        return {'type': self.type.name, 'title': self.title, 'description': self.description}
Example #17
0
class User(PersonMixin, db.Model):
    """Indico 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.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 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)
    # - legacy_paper_reviewing_roles (LegacyPaperReviewingRole.user)
    # - 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)

    @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 indico.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 indico.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 != 'indico'}

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

    @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}

    @property
    def is_janitor(self):
        return self.id == Config.getInstance().getJanitorUserId()

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

    @cached_property
    def settings(self):
        """Returns the user settings proxy for this user"""
        from indico.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 'indico.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
Example #18
0
 def event_id(cls):
     return db.Column(db.Integer,
                      db.ForeignKey('events.events.id'),
                      nullable=False,
                      index=True)
Example #19
0
class EventNote(LinkMixin, db.Model):
    __tablename__ = 'notes'
    allowed_link_types = LinkMixin.allowed_link_types - {
        LinkType.category, LinkType.session_block
    }
    unique_links = True
    events_backref_name = 'all_notes'
    link_backref_name = 'note'

    @strict_classproperty
    @classmethod
    def __auto_table_args(cls):
        return (make_fts_index(cls, 'html'), {'schema': 'events'})

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

    #: The ID of the note
    id = db.Column(db.Integer, primary_key=True)
    #: If the note has been deleted
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: The rendered HTML of the note
    html = db.Column(db.Text, nullable=False)
    #: The ID of the current revision
    current_revision_id = db.Column(
        db.Integer,
        db.ForeignKey('events.note_revisions.id', use_alter=True),
        nullable=True  # needed for post_update :(
    )

    #: The list of all revisions for the note
    revisions = db.relationship(
        'EventNoteRevision',
        primaryjoin=lambda: EventNote.id == EventNoteRevision.note_id,
        foreign_keys=lambda: EventNoteRevision.note_id,
        lazy=True,
        cascade='all, delete-orphan',
        order_by=lambda: EventNoteRevision.created_dt.desc(),
        backref=db.backref('note', lazy=False))
    #: The currently active revision of the note
    current_revision = db.relationship(
        'EventNoteRevision',
        primaryjoin=lambda: EventNote.current_revision_id == EventNoteRevision.
        id,
        foreign_keys=current_revision_id,
        lazy=True,
        post_update=True)

    @locator_property
    def locator(self):
        return self.object.locator

    @classmethod
    def get_for_linked_object(cls, linked_object, preload_event=True):
        """Get the note for the given object.

        This only returns a note that hasn't been deleted.

        :param linked_object: An event, session, contribution or
                              subcontribution.
        :param preload_event: If all notes for the same event should
                              be pre-loaded and cached in the app
                              context.
        """
        event = linked_object.event
        try:
            return g.event_notes[event].get(linked_object)
        except (AttributeError, KeyError):
            if not preload_event:
                return linked_object.note if linked_object.note and not linked_object.note.is_deleted else None
            if 'event_notes' not in g:
                g.event_notes = {}
            query = (event.all_notes.filter_by(is_deleted=False).options(
                joinedload(EventNote.linked_event),
                joinedload(EventNote.session),
                joinedload(EventNote.contribution),
                joinedload(EventNote.subcontribution)))
            g.event_notes[event] = {n.object: n for n in query}
            return g.event_notes[event].get(linked_object)

    @classmethod
    def get_or_create(cls, linked_object):
        """Get the note for the given object or creates a new one.

        If there is an existing note for the object, it will be returned
        even.  Otherwise a new note is created.
        """
        note = cls.query.filter_by(object=linked_object).first()
        if note is None:
            note = cls(object=linked_object)
        return note

    def delete(self, user):
        """Mark the note as deleted and adds a new empty revision."""
        self.create_revision(self.current_revision.render_mode, '', user)
        self.is_deleted = True

    def create_revision(self, render_mode, source, user):
        """Create a new revision if needed and marks it as undeleted if it was.

        Any change to the render mode or the source causes a new
        revision to be created.  The user is not taken into account
        since a user "modifying" a note without changing things is
        not really a change.
        """
        self.is_deleted = False
        with db.session.no_autoflush:
            current = self.current_revision
        if current is not None and current.render_mode == render_mode and current.source == source:
            return current
        self.current_revision = EventNoteRevision(render_mode=render_mode,
                                                  source=source,
                                                  user=user)
        return self.current_revision

    @classmethod
    def html_matches(cls, search_string, exact=False):
        """Check whether the html content matches a search string.

        To be used in a SQLAlchemy `filter` call.

        :param search_string: A string to search for
        :param exact: Whether to search for the exact string
        """
        return fts_matches(cls.html, search_string, exact=exact)

    def __repr__(self):
        return '<EventNote({}, current_revision={}{}, {})>'.format(
            self.id, self.current_revision_id,
            ', is_deleted=True' if self.is_deleted else '', self.link_repr)
Example #20
0
class Location(db.Model):
    __tablename__ = 'locations'
    __table_args__ = {'schema': 'roombooking'}

    # TODO: Turn this into a proper admin setting
    working_time_periods = ((time(8, 30), time(12,
                                               30)), (time(13,
                                                           30), time(17, 30)))

    @classproperty
    @classmethod
    def working_time_start(cls):
        return cls.working_time_periods[0][0]

    @classproperty
    @classmethod
    def working_time_end(cls):
        return cls.working_time_periods[-1][1]

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False, unique=True, index=True)
    is_default = db.Column(db.Boolean, nullable=False, default=False)
    map_url_template = db.Column(db.String, nullable=False, default='')
    _room_name_format = db.Column('room_name_format',
                                  db.String,
                                  nullable=False,
                                  default=u'%1$s/%2$s-%3$s')

    #: The format used to display room names (with placeholders)
    @hybrid_property
    def room_name_format(self):
        """Translate Postgres' format syntax (e.g. `%1$s/%2$s-%3$s`) to Python's."""
        placeholders = ['building', 'floor', 'number']
        return re.sub(r'%(\d)\$s',
                      lambda m: '{%s}' % placeholders[int(m.group(1)) - 1],
                      self._room_name_format)

    @room_name_format.expression
    def room_name_format(cls):
        return cls._room_name_format

    @room_name_format.setter
    def room_name_format(self, value):
        self._room_name_format = value.format(building='%1$s',
                                              floor='%2$s',
                                              number='%3$s')

    rooms = db.relationship('Room',
                            backref='location',
                            cascade='all, delete-orphan',
                            lazy=True)

    # relationship backrefs:
    # - breaks (Break.own_venue)
    # - contributions (Contribution.own_venue)
    # - events (Event.own_venue)
    # - session_blocks (SessionBlock.own_venue)
    # - sessions (Session.own_venue)

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'name')

    @locator_property
    def locator(self):
        return {'locationId': self.name}

    @property
    @memoize_request
    def is_map_available(self):
        # XXX: Broken due to Aspect refactoring. Will be removed anyway.
        return False

    @classproperty
    @classmethod
    @memoize_request
    def default_location(cls):
        return cls.query.filter_by(is_default=True).first()

    def set_default(self):
        if self.is_default:
            return
        (Location.query.filter(Location.is_default
                               | (Location.id == self.id)).update(
                                   {
                                       'is_default': func.not_(
                                           Location.is_default)
                                   },
                                   synchronize_session='fetch'))

    def get_buildings(self):
        building_rooms = defaultdict(list)
        for room in self.rooms:
            building_rooms[room.building].append(room)

        buildings = []
        for building_name, rooms in building_rooms.iteritems():
            room_with_lat_lon = next(
                (r for r in rooms if r.longitude and r.latitude), None)
            if not room_with_lat_lon:
                continue
            buildings.append({
                'number':
                building_name,
                'title':
                _(u'Building {}'.format(building_name)),
                'longitude':
                room_with_lat_lon.longitude,
                'latitude':
                room_with_lat_lon.latitude,
                'rooms':
                [r.to_serializable('__public_exhaustive__') for r in rooms]
            })
        return buildings
Example #21
0
 def session_id(cls):
     return db.Column(
         db.String,
         nullable=True
     )
Example #22
0
class Location(db.Model):
    __tablename__ = 'locations'
    __table_args__ = (db.Index(None, 'name', unique=True, postgresql_where=db.text('NOT is_deleted')),
                      {'schema': 'roombooking'})

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    name = db.Column(
        db.String,
        nullable=False,
    )
    map_url_template = db.Column(
        db.String,
        nullable=False,
        default=''
    )
    _room_name_format = db.Column(
        'room_name_format',
        db.String,
        nullable=False,
        default='%1$s/%2$s-%3$s'
    )
    is_deleted = db.Column(
        db.Boolean,
        nullable=False,
        default=False,
    )

    #: The format used to display room names (with placeholders)
    @hybrid_property
    def room_name_format(self):
        """Translate Postgres' format syntax (e.g. `%1$s/%2$s-%3$s`) to Python's."""
        placeholders = ['building', 'floor', 'number', 'site']
        return re.sub(
            r'%(\d)\$s',
            lambda m: '{%s}' % placeholders[int(m.group(1)) - 1],
            self._room_name_format
        )

    @room_name_format.expression
    def room_name_format(cls):
        return cls._room_name_format

    @room_name_format.setter
    def room_name_format(self, value):
        self._room_name_format = value.format(
            building='%1$s',
            floor='%2$s',
            number='%3$s',
            site='%4$s'
        )

    rooms = db.relationship(
        'Room',
        back_populates='location',
        cascade='all, delete-orphan',
        primaryjoin='(Room.location_id == Location.id) & ~Room.is_deleted',
        lazy=True
    )

    # relationship backrefs:
    # - breaks (Break.own_venue)
    # - contributions (Contribution.own_venue)
    # - events (Event.own_venue)
    # - session_blocks (SessionBlock.own_venue)
    # - sessions (Session.own_venue)

    def __repr__(self):
        return format_repr(self, 'id', 'name', is_deleted=False)
Example #23
0
class RegistrationData(StoredFileMixin, db.Model):
    """Data entry within a registration for a field in a registration form"""

    __tablename__ = 'registration_data'
    __table_args__ = {'schema': 'event_registration'}

    # StoredFileMixin settings
    add_file_date_column = False
    file_required = False

    #: The ID of the registration
    registration_id = db.Column(
        db.Integer,
        db.ForeignKey('event_registration.registrations.id'),
        primary_key=True,
        autoincrement=False
    )
    #: The ID of the field data
    field_data_id = db.Column(
        db.Integer,
        db.ForeignKey('event_registration.form_field_data.id'),
        primary_key=True,
        autoincrement=False
    )
    #: The submitted data for the field
    data = db.Column(
        JSONB,
        default=lambda: None,
        nullable=False
    )

    #: The associated field data object
    field_data = db.relationship(
        'RegistrationFormFieldData',
        lazy=True,
        backref=db.backref(
            'registration_data',
            lazy=True,
            cascade='all, delete-orphan'
        )
    )

    # relationship backrefs:
    # - registration (Registration.data)

    @locator_property
    def locator(self):
        # a normal locator doesn't make much sense
        raise NotImplementedError

    @locator.file
    def locator(self):
        """A locator that points to the associated file."""
        if not self.filename:
            raise Exception('The file locator is only available if there is a file.')
        return dict(self.registration.locator, field_data_id=self.field_data_id, filename=self.filename)

    @property
    def friendly_data(self):
        return self.get_friendly_data()

    @property
    def search_data(self):
        return self.get_friendly_data(for_search=True)

    def get_friendly_data(self, **kwargs):
        return self.field_data.field.get_friendly_data(self, **kwargs)

    @property
    def price(self):
        return self.field_data.field.calculate_price(self)

    @property
    def summary_data(self):
        return {'data': self.friendly_data, 'price': self.price}

    @property
    def user_data(self):
        return self.filename if self.field_data.field.input_type == 'file' else self.data

    def _set_file(self, file_info):
        # in case we are deleting/replacing a file
        self.storage_backend = None
        self.storage_file_id = None
        self.filename = None
        self.content_type = None
        self.size = None
        if file_info is not None:
            self.filename = file_info['name']
            self.content_type = file_info['content_type']
            self.save(file_info['data'])

    file = property(fset=_set_file)
    del _set_file

    @return_ascii
    def __repr__(self):
        return '<RegistrationData({}, {}): {}>'.format(self.registration_id, self.field_data_id, self.data)

    def _build_storage_path(self):
        self.registration.registration_form.assign_id()
        self.registration.assign_id()
        path_segments = ['event', strict_unicode(self.registration.event_id), 'registrations',
                         strict_unicode(self.registration.registration_form.id), strict_unicode(self.registration.id)]
        assert None not in path_segments
        # add timestamp in case someone uploads the same file again
        filename = '{}-{}-{}'.format(self.field_data.field_id, int(time.time()), self.filename)
        path = posixpath.join(*(path_segments + [filename]))
        return config.ATTACHMENT_STORAGE, path

    def render_price(self):
        return format_currency(self.price, self.registration.currency, locale=session.lang or 'en_GB')
Example #24
0
class Reservation(Serializer, db.Model):
    __tablename__ = 'reservations'
    __public__ = []
    __calendar_public__ = [
        'id', ('booked_for_name', 'bookedForName'),
        ('booking_reason', 'reason'), ('external_details_url', 'bookingUrl')
    ]
    __api_public__ = [
        'id', ('start_dt', 'startDT'), ('end_dt', 'endDT'), 'repeat_frequency',
        'repeat_interval', ('booked_for_name', 'bookedForName'),
        ('external_details_url', 'bookingUrl'), ('booking_reason', 'reason'),
        ('uses_vc', 'usesAVC'), ('needs_vc_assistance', 'needsAVCSupport'),
        'needs_assistance', ('is_accepted', 'isConfirmed'),
        ('is_accepted', 'isValid'), 'is_cancelled', 'is_rejected',
        ('location_name', 'location'),
        ('contact_email', 'booked_for_user_email')
    ]

    @declared_attr
    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)),
                db.CheckConstraint("rejection_reason != ''",
                                   'rejection_reason_not_empty'), {
                                       'schema': 'roombooking'
                                   })

    id = db.Column(db.Integer, primary_key=True)
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    start_dt = db.Column(db.DateTime, nullable=False, index=True)
    end_dt = db.Column(db.DateTime, nullable=False, index=True)
    repeat_frequency = db.Column(
        PyIntEnum(RepeatFrequency),
        nullable=False,
        default=RepeatFrequency.NEVER)  # week, month, year, etc.
    repeat_interval = db.Column(db.SmallInteger, nullable=False,
                                default=0)  # 1, 2, 3, etc.
    booked_for_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=True,
        # Must be nullable for legacy data :(
    )
    booked_for_name = db.Column(db.String, nullable=False)
    created_by_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=True,
        # Must be nullable for legacy data :(
    )
    room_id = db.Column(db.Integer,
                        db.ForeignKey('roombooking.rooms.id'),
                        nullable=False,
                        index=True)
    state = db.Column(PyIntEnum(ReservationState),
                      nullable=False,
                      default=ReservationState.accepted)
    booking_reason = db.Column(db.Text, nullable=False)
    rejection_reason = db.Column(db.String, nullable=True)
    uses_vc = db.Column(db.Boolean, nullable=False, default=False)
    needs_vc_assistance = db.Column(db.Boolean, nullable=False, default=False)
    needs_assistance = db.Column(db.Boolean, nullable=False, default=False)
    link_id = db.Column(db.Integer,
                        db.ForeignKey('roombooking.reservation_links.id'),
                        nullable=True,
                        index=True)
    end_notification_sent = db.Column(db.Boolean,
                                      nullable=False,
                                      default=False)

    edit_logs = db.relationship('ReservationEditLog',
                                backref='reservation',
                                cascade='all, delete-orphan',
                                lazy='dynamic')
    occurrences = db.relationship('ReservationOccurrence',
                                  backref='reservation',
                                  cascade='all, delete-orphan',
                                  lazy='dynamic')
    #: The user this booking was made for.
    #: Assigning a user here also updates `booked_for_name`.
    booked_for_user = db.relationship('User',
                                      lazy=False,
                                      foreign_keys=[booked_for_id],
                                      backref=db.backref(
                                          'reservations_booked_for',
                                          lazy='dynamic'))
    #: The user who created this booking.
    created_by_user = db.relationship('User',
                                      lazy=False,
                                      foreign_keys=[created_by_id],
                                      backref=db.backref('reservations',
                                                         lazy='dynamic'))

    link = db.relationship('ReservationLink',
                           lazy=True,
                           backref=db.backref('reservation', uselist=False))

    # relationship backrefs:
    # - room (Room.reservations)

    @hybrid_property
    def is_pending(self):
        return self.state == ReservationState.pending

    @hybrid_property
    def is_accepted(self):
        return self.state == ReservationState.accepted

    @hybrid_property
    def is_cancelled(self):
        return self.state == ReservationState.cancelled

    @hybrid_property
    def is_rejected(self):
        return self.state == ReservationState.rejected

    @hybrid_property
    def is_archived(self):
        return self.end_dt < datetime.now()

    @hybrid_property
    def is_repeating(self):
        return self.repeat_frequency != RepeatFrequency.NEVER

    @property
    def contact_email(self):
        return self.booked_for_user.email if self.booked_for_user else None

    @property
    def contact_phone(self):
        return self.booked_for_user.phone if self.booked_for_user else None

    @property
    def external_details_url(self):
        return url_for('rooms_new.booking_link',
                       booking_id=self.id,
                       _external=True)

    @property
    def location_name(self):
        return self.room.location_name

    @property
    def repetition(self):
        return self.repeat_frequency, self.repeat_interval

    @property
    def status_string(self):
        parts = []
        if self.is_accepted:
            parts.append(_(u"Valid"))
        else:
            if self.is_cancelled:
                parts.append(_(u"Cancelled"))
            if self.is_rejected:
                parts.append(_(u"Rejected"))
            if not self.is_accepted:
                parts.append(_(u"Not confirmed"))
        if self.is_archived:
            parts.append(_(u"Archived"))
        else:
            parts.append(_(u"Live"))
        return u', '.join(map(unicode, parts))

    @property
    def linked_object(self):
        return self.link.object if self.link else None

    @linked_object.setter
    def linked_object(self, obj):
        assert self.link is None
        self.link = ReservationLink(object=obj)

    @property
    def event(self):
        return self.link.event if self.link else None

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'room_id',
                           'start_dt',
                           'end_dt',
                           'state',
                           _text=self.booking_reason)

    @classmethod
    def create_from_data(cls,
                         room,
                         data,
                         user,
                         prebook=None,
                         ignore_admin=False):
        """Creates a new reservation.

        :param room: The Room that's being booked.
        :param data: A dict containing the booking data, usually from a :class:`NewBookingConfirmForm` instance
        :param user: The :class:`.User` who creates the booking.
        :param prebook: Instead of determining the booking type from the user's
                        permissions, always use the given mode.
        """

        populate_fields = ('start_dt', 'end_dt', 'repeat_frequency',
                           'repeat_interval', 'room_id', 'contact_email',
                           'contact_phone', 'booking_reason',
                           'needs_assistance', 'uses_vc',
                           'needs_vc_assistance')
        if data['repeat_frequency'] == RepeatFrequency.NEVER and data[
                'start_dt'].date() != data['end_dt'].date():
            raise ValueError('end_dt != start_dt for non-repeating booking')

        if prebook is None:
            prebook = not room.can_book(user, allow_admin=(not ignore_admin))
            if prebook and not room.can_prebook(
                    user, allow_admin=(not ignore_admin)):
                raise NoReportError(u'You cannot book this room')

        room.check_advance_days(data['end_dt'].date(), user)
        room.check_bookable_hours(data['start_dt'].time(),
                                  data['end_dt'].time(), user)

        reservation = cls()
        for field in populate_fields:
            if field in data:
                setattr(reservation, field, data[field])
        reservation.room = room
        # if 'room_usage' is not specified, we'll take whatever is passed in 'booked_for_user'
        reservation.booked_for_user = data['booked_for_user'] if data.get(
            'room_usage') != 'current_user' else user
        reservation.booked_for_name = reservation.booked_for_user.full_name
        reservation.state = ReservationState.pending if prebook else ReservationState.accepted
        reservation.created_by_user = user
        reservation.create_occurrences(True)
        if not any(occ.is_valid for occ in reservation.occurrences):
            raise NoReportError(_(u'Reservation has no valid occurrences'))
        db.session.flush()
        signals.rb.booking_created.send(reservation)
        notify_creation(reservation)
        return reservation

    @staticmethod
    def get_with_data(*args, **kwargs):
        filters = kwargs.pop('filters', None)
        limit = kwargs.pop('limit', None)
        offset = kwargs.pop('offset', 0)
        order = kwargs.pop('order', Reservation.start_dt)
        limit_per_room = kwargs.pop('limit_per_room', False)
        occurs_on = kwargs.pop('occurs_on')
        if kwargs:
            raise ValueError('Unexpected kwargs: {}'.format(kwargs))

        query = Reservation.query.options(joinedload(Reservation.room))
        if filters:
            query = query.filter(*filters)
        if occurs_on:
            query = query.filter(
                Reservation.id.in_(
                    db.session.query(
                        ReservationOccurrence.reservation_id).filter(
                            ReservationOccurrence.date.in_(occurs_on),
                            ReservationOccurrence.is_valid)))
        if limit_per_room and (limit or offset):
            query = limit_groups(query, Reservation, Reservation.room_id,
                                 order, limit, offset)

        query = query.order_by(order, Reservation.created_dt)

        if not limit_per_room:
            if limit:
                query = query.limit(limit)
            if offset:
                query = query.offset(offset)

        result = OrderedDict((r.id, {'reservation': r}) for r in query)

        if 'occurrences' in args:
            occurrence_data = OrderedMultiDict(
                db.session.query(ReservationOccurrence.reservation_id,
                                 ReservationOccurrence).filter(
                                     ReservationOccurrence.reservation_id.in_(
                                         result.iterkeys())).order_by(
                                             ReservationOccurrence.start_dt))
            for id_, data in result.iteritems():
                data['occurrences'] = occurrence_data.getlist(id_)

        return result.values()

    @staticmethod
    def find_overlapping_with(room, occurrences, skip_reservation_id=None):
        return Reservation.find(
            Reservation.room == room,
            Reservation.id != skip_reservation_id,
            ReservationOccurrence.is_valid,
            ReservationOccurrence.filter_overlap(occurrences),
            _join=ReservationOccurrence)

    def accept(self, user):
        self.state = ReservationState.accepted
        self.add_edit_log(
            ReservationEditLog(user_name=user.full_name,
                               info=['Reservation accepted']))
        notify_confirmation(self)
        signals.rb.booking_state_changed.send(self)
        valid_occurrences = self.occurrences.filter(
            ReservationOccurrence.is_valid).all()
        pre_occurrences = ReservationOccurrence.find_overlapping_with(
            self.room, valid_occurrences, self.id).all()
        for occurrence in pre_occurrences:
            if not occurrence.is_valid:
                continue
            occurrence.reject(
                user,
                u'Rejected due to collision with a confirmed reservation')

    def reset_approval(self, user):
        self.state = ReservationState.pending
        notify_reset_approval(self)
        self.add_edit_log(
            ReservationEditLog(user_name=user.full_name,
                               info=['Requiring new approval due to change']))

    def cancel(self, user, reason=None, silent=False):
        self.state = ReservationState.cancelled
        self.rejection_reason = reason or None
        self.occurrences.filter_by(is_valid=True).update(
            {
                ReservationOccurrence.state:
                ReservationOccurrenceState.cancelled,
                ReservationOccurrence.rejection_reason: reason
            },
            synchronize_session='fetch')
        signals.rb.booking_state_changed.send(self)
        if not silent:
            notify_cancellation(self)
            log_msg = u'Reservation cancelled: {}'.format(
                reason) if reason else 'Reservation cancelled'
            self.add_edit_log(
                ReservationEditLog(user_name=user.full_name, info=[log_msg]))

    def reject(self, user, reason, silent=False):
        self.state = ReservationState.rejected
        self.rejection_reason = reason or None
        self.occurrences.filter_by(is_valid=True).update(
            {
                ReservationOccurrence.state:
                ReservationOccurrenceState.rejected,
                ReservationOccurrence.rejection_reason: reason
            },
            synchronize_session='fetch')
        signals.rb.booking_state_changed.send(self)
        if not silent:
            notify_rejection(self)
            log_msg = u'Reservation rejected: {}'.format(reason)
            self.add_edit_log(
                ReservationEditLog(user_name=user.full_name, info=[log_msg]))

    def add_edit_log(self, edit_log):
        self.edit_logs.append(edit_log)
        db.session.flush()

    def can_accept(self, user, allow_admin=True):
        if user is None:
            return False
        return self.is_pending and self.room.can_moderate(
            user, allow_admin=allow_admin)

    def can_reject(self, user, allow_admin=True):
        if user is None:
            return False
        if self.is_rejected or self.is_cancelled:
            return False
        return self.room.can_moderate(user, allow_admin=allow_admin)

    def can_cancel(self, user, allow_admin=True):
        if user is None:
            return False
        if self.is_rejected or self.is_cancelled or self.is_archived:
            return False
        return self.is_owned_by(user) or self.is_booked_for(user) or (
            allow_admin and rb_is_admin(user))

    def can_edit(self, user, allow_admin=True):
        if user is None:
            return False
        if self.is_rejected or self.is_cancelled:
            return False
        if self.is_archived and not (allow_admin and rb_is_admin(user)):
            return False
        return self.is_owned_by(user) or self.is_booked_for(
            user) or self.room.can_manage(user, allow_admin=allow_admin)

    def can_delete(self, user, allow_admin=True):
        if user is None:
            return False
        return allow_admin and rb_is_admin(user) and (self.is_cancelled
                                                      or self.is_rejected)

    def create_occurrences(self, skip_conflicts, user=None):
        ReservationOccurrence.create_series_for_reservation(self)
        db.session.flush()

        if user is None:
            user = self.created_by_user

        # Check for conflicts with nonbookable periods
        if not rb_is_admin(user) and not self.room.is_owned_by(user):
            nonbookable_periods = self.room.nonbookable_periods.filter(
                NonBookablePeriod.end_dt > self.start_dt)
            for occurrence in self.occurrences:
                if not occurrence.is_valid:
                    continue
                for nbd in nonbookable_periods:
                    if nbd.overlaps(occurrence.start_dt, occurrence.end_dt):
                        if not skip_conflicts:
                            raise ConflictingOccurrences()
                        occurrence.cancel(user,
                                          u'Skipped due to nonbookable date',
                                          silent=True,
                                          propagate=False)
                        break

        # Check for conflicts with blockings
        blocked_rooms = self.room.get_blocked_rooms(
            *(occurrence.start_dt for occurrence in self.occurrences))
        for br in blocked_rooms:
            blocking = br.blocking
            if blocking.can_be_overridden(user, self.room):
                continue
            for occurrence in self.occurrences:
                if occurrence.is_valid and blocking.is_active_at(
                        occurrence.start_dt.date()):
                    # Cancel OUR occurrence
                    msg = u'Skipped due to collision with a blocking ({})'
                    occurrence.cancel(user,
                                      msg.format(blocking.reason),
                                      silent=True,
                                      propagate=False)

        # Check for conflicts with other occurrences
        conflicting_occurrences = self.get_conflicting_occurrences()
        for occurrence, conflicts in conflicting_occurrences.iteritems():
            if not occurrence.is_valid:
                continue
            if conflicts['confirmed']:
                if not skip_conflicts:
                    raise ConflictingOccurrences()
                # Cancel OUR occurrence
                msg = u'Skipped due to collision with {} reservation(s)'
                occurrence.cancel(user,
                                  msg.format(len(conflicts['confirmed'])),
                                  silent=True,
                                  propagate=False)
            elif conflicts['pending'] and self.is_accepted:
                # Reject OTHER occurrences
                for conflict in conflicts['pending']:
                    conflict.reject(
                        user,
                        u'Rejected due to collision with a confirmed reservation'
                    )

    def find_excluded_days(self):
        return self.occurrences.filter(~ReservationOccurrence.is_valid)

    def find_overlapping(self):
        occurrences = self.occurrences.filter(
            ReservationOccurrence.is_valid).all()
        return Reservation.find_overlapping_with(self.room, occurrences,
                                                 self.id)

    @locator_property
    def locator(self):
        return {'roomLocation': self.location_name, 'resvID': self.id}

    def get_conflicting_occurrences(self):
        valid_occurrences = self.occurrences.filter(
            ReservationOccurrence.is_valid).all()
        colliding_occurrences = ReservationOccurrence.find_overlapping_with(
            self.room, valid_occurrences, self.id).all()
        conflicts = defaultdict(lambda: dict(confirmed=[], pending=[]))
        for occurrence in valid_occurrences:
            for colliding in colliding_occurrences:
                if occurrence.overlaps(colliding):
                    key = 'confirmed' if colliding.reservation.is_accepted else 'pending'
                    conflicts[occurrence][key].append(colliding)
        return conflicts

    def is_booked_for(self, user):
        return user is not None and self.booked_for_user == user

    def is_owned_by(self, user):
        return self.created_by_user == user

    def modify(self, data, user):
        """Modifies an existing reservation.

        :param data: A dict containing the booking data, usually from a :class:`ModifyBookingForm` instance
        :param user: The :class:`.User` who modifies the booking.
        """

        populate_fields = ('start_dt', 'end_dt', 'repeat_frequency',
                           'repeat_interval', 'booked_for_user',
                           'contact_email', 'contact_phone', 'booking_reason',
                           'needs_assistance', 'uses_vc',
                           'needs_vc_assistance')
        # fields affecting occurrences
        occurrence_fields = {
            'start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval'
        }
        # fields where date and time are compared separately
        date_time_fields = {'start_dt', 'end_dt'}
        # fields for the repetition
        repetition_fields = {'repeat_frequency', 'repeat_interval'}
        # pretty names for logging
        field_names = {
            'start_dt/date': u"start date",
            'end_dt/date': u"end date",
            'start_dt/time': u"start time",
            'end_dt/time': u"end time",
            'repetition': u"booking type",
            'booked_for_user': u"'Booked for' user",
            'contact_email': u"contact email",
            'contact_phone': u"contact phone number",
            'booking_reason': u"booking reason",
            'needs_assistance': u"option 'General Assistance'",
            'uses_vc': u"option 'Uses Videoconference'",
            'needs_vc_assistance': u"option 'Videoconference Setup Assistance'"
        }

        self.room.check_advance_days(data['end_dt'].date(), user)
        self.room.check_bookable_hours(data['start_dt'].time(),
                                       data['end_dt'].time(), user)
        if data['room_usage'] == 'current_user':
            data['booked_for_user'] = session.user

        changes = {}
        update_occurrences = False
        old_repetition = self.repetition

        for field in populate_fields:
            if field not in data:
                continue
            old = getattr(self, field)
            new = data[field]
            converter = unicode
            if old != new:
                # Booked for user updates the (redundant) name
                if field == 'booked_for_user':
                    old = self.booked_for_name
                    new = self.booked_for_name = data[field].full_name
                # Apply the change
                setattr(self, field, data[field])
                # If any occurrence-related field changed we need to recreate the occurrences
                if field in occurrence_fields:
                    update_occurrences = True
                # Record change for history entry
                if field in date_time_fields:
                    # The date/time fields create separate entries for the date and time parts
                    if old.date() != new.date():
                        changes[field + '/date'] = {
                            'old': old.date(),
                            'new': new.date(),
                            'converter': format_date
                        }
                    if old.time() != new.time():
                        changes[field + '/time'] = {
                            'old': old.time(),
                            'new': new.time(),
                            'converter': format_time
                        }
                elif field in repetition_fields:
                    # Repetition needs special handling since it consists of two fields but they are tied together
                    # We simply update it whenever we encounter such a change; after the last change we end up with
                    # the correct change data
                    changes['repetition'] = {
                        'old': old_repetition,
                        'new': self.repetition,
                        'converter': lambda x: RepeatMapping.get_message(*x)
                    }
                else:
                    changes[field] = {
                        'old': old,
                        'new': new,
                        'converter': converter
                    }

        if not changes:
            return False

        # Create a verbose log entry for the modification
        log = [u'Booking modified']
        for field, change in changes.iteritems():
            field_title = field_names.get(field, field)
            converter = change['converter']
            old = to_unicode(converter(change['old']))
            new = to_unicode(converter(change['new']))
            if not old:
                log.append(u"The {} was set to '{}'".format(field_title, new))
            elif not new:
                log.append(u"The {} was cleared".format(field_title))
            else:
                log.append(u"The {} was changed from '{}' to '{}'".format(
                    field_title, old, new))

        self.edit_logs.append(
            ReservationEditLog(user_name=user.full_name, info=log))

        # Recreate all occurrences if necessary
        if update_occurrences:
            cols = [
                col.name for col in ReservationOccurrence.__table__.columns if
                not col.primary_key and col.name not in {'start_dt', 'end_dt'}
            ]

            old_occurrences = {occ.date: occ for occ in self.occurrences}
            self.occurrences.delete(synchronize_session='fetch')
            self.create_occurrences(True, user)
            db.session.flush()
            # Restore rejection data etc. for recreated occurrences
            for occurrence in self.occurrences:
                old_occurrence = old_occurrences.get(occurrence.date)
                # Copy data from old occurrence UNLESS the new one is invalid (e.g. because of collisions)
                # Otherwise we'd end up with valid occurrences ignoring collisions!
                if old_occurrence and occurrence.is_valid:
                    for col in cols:
                        setattr(occurrence, col, getattr(old_occurrence, col))
            # Don't cause new notifications for the entire booking in case of daily repetition
            if self.repeat_frequency == RepeatFrequency.DAY and all(
                    occ.notification_sent
                    for occ in old_occurrences.itervalues()):
                for occurrence in self.occurrences:
                    occurrence.notification_sent = True

        # Sanity check so we don't end up with an "empty" booking
        if not any(occ.is_valid for occ in self.occurrences):
            raise NoReportError(_(u'Reservation has no valid occurrences'))

        notify_modification(self, changes)
        return True
Example #25
0
class VCRoomEventAssociation(db.Model):
    __tablename__ = 'vc_room_events'
    __table_args__ = {'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)
    #: Link type of the vc_room to a event/contribution/session
    link_type = db.Column(PyIntEnum(VCRoomLinkType), nullable=False)
    #: Id of the event/contribution/session block the vc_room is linked to
    link_id = db.Column(db.String, nullable=True)
    #: If the chatroom should be hidden on the event page
    show = db.Column(db.Boolean, nullable=False, default=False)
    #: videoconference plugin-specific data
    data = db.Column(JSON, 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_new = db.relationship('Event',
                                lazy=True,
                                backref=db.backref('vc_room_associations',
                                                   lazy='dynamic'))

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

    @property
    def event(self):
        return ConferenceHolder().getById(str(self.event_id), True)

    @event.setter
    def event(self, event):
        self.event_id = int(event.getId())

    @property
    def link_object(self):
        if self.link_type == VCRoomLinkType.event:
            return self.event
        elif self.link_type == VCRoomLinkType.contribution:
            return self.event.getContributionById(self.link_id)
        else:
            session_id, slot_id = self.link_id.split(':')
            sess = self.event.getSessionById(session_id)
            return sess.getSlotById(slot_id) if sess is not None else None

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

    @classmethod
    @unify_event_args
    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 indico 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.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):
        """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
        """
        Logger.get('modules.vc').info(
            "Detaching VC room {} from event {} ({})".format(
                self.vc_room, self.event, self.link_object))
        db.session.delete(self)
        db.session.flush()
        if not self.vc_room.events:
            Logger.get('modules.vc').info("Deleting VC room {}".format(
                self.vc_room))
            if self.vc_room.status != VCRoomStatus.deleted:
                self.vc_room.plugin.delete_room(self.vc_room, self.event)
                notify_deleted(self.vc_room.plugin, self.vc_room, self,
                               self.event, user)
            db.session.delete(self.vc_room)
Example #26
0
    @property
    def identifier(self):
        return f'EventRole:{self.id}'

    @property
    def css(self):
        return 'color: #{0} !important; border-color: #{0} !important'.format(
            self.color)

    @property
    def style(self):
        return {'color': '#' + self.color, 'borderColor': '#' + self.color}


role_members_table = db.Table('role_members',
                              db.metadata,
                              db.Column('role_id',
                                        db.Integer,
                                        db.ForeignKey('events.roles.id'),
                                        primary_key=True,
                                        nullable=False,
                                        index=True),
                              db.Column('user_id',
                                        db.Integer,
                                        db.ForeignKey('users.users.id'),
                                        primary_key=True,
                                        nullable=False,
                                        index=True),
                              schema='events')
Example #27
0
 def filename(cls):
     """The name of the file"""
     return db.Column(
         db.String,
         nullable=not cls.file_required
     )
Example #28
0
class EventRole(db.Model):
    __tablename__ = 'roles'
    __table_args__ = (db.CheckConstraint('code = upper(code)',
                                         'uppercase_code'),
                      db.Index(None, 'event_id', 'code', unique=True), {
                          'schema': 'events'
                      })

    is_group = False
    is_event_role = True
    is_category_role = False
    is_single_person = True
    is_network = False
    is_registration_form = False
    principal_order = 2
    principal_type = PrincipalType.event_role

    id = db.Column(db.Integer, primary_key=True)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         nullable=False,
                         index=True)
    name = db.Column(db.String, nullable=False)
    code = db.Column(db.String, nullable=False)
    color = db.Column(db.String, nullable=False)

    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('roles',
                                               cascade='all, delete-orphan',
                                               lazy=True))
    members = db.relationship(
        'User',
        secondary='events.role_members',
        lazy=True,
        collection_class=set,
        backref=db.backref('event_roles', lazy=True, collection_class=set),
    )

    # relationship backrefs:
    # - in_attachment_acls (AttachmentPrincipal.event_role)
    # - in_attachment_folder_acls (AttachmentFolderPrincipal.event_role)
    # - in_contribution_acls (ContributionPrincipal.event_role)
    # - in_event_acls (EventPrincipal.event_role)
    # - in_event_settings_acls (EventSettingPrincipal.event_role)
    # - in_session_acls (SessionPrincipal.event_role)
    # - in_track_acls (TrackPrincipal.event_role)

    def __contains__(self, user):
        return user is not None and self in user.event_roles

    def __repr__(self):
        return format_repr(self, 'id', 'code', _text=self.name)

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

    @property
    def identifier(self):
        return f'EventRole:{self.id}'

    @property
    def css(self):
        return 'color: #{0} !important; border-color: #{0} !important'.format(
            self.color)

    @property
    def style(self):
        return {'color': '#' + self.color, 'borderColor': '#' + self.color}
Example #29
0
 def storage_file_id(cls):
     return db.Column(
         db.String,
         nullable=not cls.file_required
     )
Example #30
0
 def modified_dt(cls):
     return db.Column(UTCDateTime, nullable=True)