Example #1
0
 def event(cls):
     return db.relationship(
         'Event',
         lazy=True,
         backref=db.backref(
             cls.events_backref_name,
             lazy='dynamic'
         )
     )
Example #2
0
 def event_new(cls):
     return db.relationship(
         'Event',
         lazy=True,
         backref=db.backref(
             cls.settings_backref_name,
             lazy='dynamic'
         )
     )
Example #3
0
 def review(cls):
     return db.relationship(
         cls.review_class,
         lazy=True,
         backref=db.backref(
             'ratings',
             cascade='all, delete-orphan',
             lazy=True
         )
     )
Example #4
0
 def event_new(cls):
     return db.relationship(
         'Event',
         foreign_keys=cls.event_id,
         lazy=True,
         backref=db.backref(
             cls.events_backref_name,
             lazy='dynamic'
         )
     )
Example #5
0
 def own_venue(cls):
     return db.relationship(
         'Location',
         foreign_keys=[cls.own_venue_id],
         lazy=True,
         backref=db.backref(
             cls.location_backref_name,
             lazy='dynamic'
         )
     )
Example #6
0
 def own_room(cls):
     return db.relationship(
         'Room',
         foreign_keys=[cls.own_room_id],
         lazy=True,
         backref=db.backref(
             cls.location_backref_name,
             lazy='dynamic'
         )
     )
Example #7
0
 def contribution_field(cls):
     return db.relationship(
         'ContributionField',
         lazy=False,
         backref=db.backref(
             cls.contribution_field_backref_name,
             cascade='all, delete-orphan',
             lazy=True
         )
     )
Example #8
0
 def reference_type(cls):
     return db.relationship(
         'ReferenceType',
         lazy=False,
         backref=db.backref(
             cls.reference_backref_name,
             cascade='all, delete-orphan',
             lazy=True
         )
     )
Example #9
0
 def user(cls):
     return db.relationship(
         'User',
         lazy=True,
         foreign_keys=cls.user_id,
         backref=db.backref(
             cls.user_backref_name,
             primaryjoin='({0}.user_id == User.id) & ~{0}.is_deleted'.format(cls.__name__),
             lazy='dynamic'
         )
     )
Example #10
0
 def all_files(cls):
     return db.relationship(
         cls.stored_file_class,
         primaryjoin=
         lambda: cls.id == getattr(cls.stored_file_class, cls.stored_file_fkey),
         foreign_keys=
         lambda: getattr(cls.stored_file_class, cls.stored_file_fkey),
         lazy=True,
         cascade='all, delete, delete-orphan',
         order_by=lambda: cls.stored_file_class.created_dt.desc(),
         backref=db.backref(
             getattr(cls.stored_file_class, 'version_of'), lazy=False))
Example #11
0
 def subcontribution(cls):
     if LinkType.subcontribution in cls.allowed_link_types:
         return db.relationship(
             'SubContribution',
             lazy=True,
             backref=db.backref(
                 cls.link_backref_name,
                 cascade='all, delete-orphan',
                 uselist=(cls.unique_links != True),  # noqa
                 lazy=cls.link_backref_lazy
             )
         )
Example #12
0
 def paper_revision(cls):
     return db.relationship(
         'PaperRevision',
         lazy=True,
         backref=db.backref(
             'comments',
             primaryjoin='(PaperReviewComment.revision_id == PaperRevision.id) & ~PaperReviewComment.is_deleted',
             order_by=cls.created_dt,
             cascade='all, delete-orphan',
             lazy=True,
         )
     )
Example #13
0
 def abstract(cls):
     return db.relationship(
         'Abstract',
         lazy=True,
         backref=db.backref(
             'comments',
             primaryjoin='(AbstractComment.abstract_id == Abstract.id) & ~AbstractComment.is_deleted',
             order_by=cls.created_dt,
             cascade='all, delete-orphan',
             lazy=True,
         )
     )
Example #14
0
 def session_block(cls):
     if LinkType.session_block in cls.allowed_link_types:
         return db.relationship(
             'SessionBlock',
             lazy=True,
             backref=db.backref(
                 cls.link_backref_name,
                 cascade='all, delete-orphan',
                 uselist=(cls.unique_links != True),  # noqa
                 lazy=cls.link_backref_lazy
             )
         )
Example #15
0
 def category(cls):
     if LinkType.category in cls.allowed_link_types:
         return db.relationship(
             'Category',
             lazy=True,
             backref=db.backref(
                 cls.link_backref_name,
                 cascade='all, delete-orphan',
                 uselist=(cls.unique_links != True),  # noqa
                 lazy=cls.link_backref_lazy
             )
         )
Example #16
0
 def event(cls):
     return db.relationship(
         'Event',
         lazy=True,
         backref=db.backref(
             cls.event_backref_name,
             primaryjoin='({0}.event_id == Event.id) & ~{0}.is_deleted'.format(cls.__name__),
             order_by=cls.position,
             cascade='all, delete-orphan',
             lazy=True
         )
     )
Example #17
0
 def linked_event(cls):
     if LinkType.event in cls.allowed_link_types:
         return db.relationship(
             'Event',
             foreign_keys=cls.linked_event_id,
             lazy=True,
             backref=db.backref(
                 cls.link_backref_name,
                 cascade='all, delete-orphan',
                 uselist=(cls.unique_links != True),  # noqa
                 lazy=cls.link_backref_lazy
             )
         )
Example #18
0
class MenuEntry(MenuEntryMixin, db.Model):
    __tablename__ = 'menu_entries'
    __table_args__ = (
        db.CheckConstraint(
            '(type IN ({type.internal_link.value}, {type.plugin_link.value}) AND name IS NOT NULL) OR '
            '(type NOT IN ({type.internal_link.value}, {type.plugin_link.value}) and name IS NULL)'
            .format(type=MenuEntryType),
            'valid_name'),
        db.CheckConstraint(
            '(type = {type.user_link.value}) = (link_url IS NOT NULL)'
            .format(type=MenuEntryType),
            'valid_link_url'),
        db.CheckConstraint(
            '(type = {type.page.value} AND page_id IS NOT NULL) OR'
            ' (type != {type.page.value} AND page_id IS NULL)'.format(type=MenuEntryType),
            'valid_page_id'),
        db.CheckConstraint(
            '(type = {type.plugin_link.value} AND plugin IS NOT NULL) OR'
            ' (type != {type.plugin_link.value} AND plugin IS NULL)'.format(type=MenuEntryType),
            'valid_plugin'),
        db.CheckConstraint(
            '(type = {type.separator.value} AND title IS NULL) OR'
            ' (type IN ({type.user_link.value}, {type.page.value}) AND title IS NOT NULL) OR'
            ' (type NOT IN ({type.separator.value}, {type.user_link.value}, {type.page.value}))'
            .format(type=MenuEntryType),
            'valid_title'),
        db.CheckConstraint(
            "title != ''",
            'title_not_empty'),
        db.Index(None, 'event_id', 'name', unique=True,
                 postgresql_where=db.text('(type = {type.internal_link.value} OR type = {type.plugin_link.value})'
                                          .format(type=MenuEntryType))),
        {'schema': 'events'}
    )

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

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

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

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

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

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

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

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

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

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

        self.parent = parent
        self.position = position + 1
Example #19
0
class Survey(db.Model):
    __tablename__ = 'surveys'
    __table_args__ = (db.CheckConstraint("anonymous OR require_user", 'valid_anonymous_user'),
                      {'schema': 'event_surveys'})

    #: The ID of the survey
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: The ID of the event
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=False
    )
    #: The title of the survey
    title = db.Column(
        db.String,
        nullable=False
    )
    uuid = db.Column(
        UUID,
        unique=True,
        nullable=False,
        default=lambda: unicode(uuid4())
    )
    # An introduction text for users of the survey
    introduction = db.Column(
        db.Text,
        nullable=False,
        default=''
    )
    #: Whether submissions will not be linked to a user
    anonymous = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Whether submissions must be done by logged users
    require_user = db.Column(
        db.Boolean,
        nullable=False,
        default=True
    )
    # #: Whether the survey is only for selected users
    private = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Maximum number of submissions allowed
    submission_limit = db.Column(
        db.Integer,
        nullable=True
    )
    #: Datetime when the survey is open
    start_dt = db.Column(
        UTCDateTime,
        nullable=True
    )
    #: Datetime when the survey is closed
    end_dt = db.Column(
        UTCDateTime,
        nullable=True
    )
    #: Whether the survey has been marked as deleted
    is_deleted = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Whether start notification has been already sent
    start_notification_sent = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Whether to send survey related notifications to users
    notifications_enabled = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Whether include Participants / Registrants when sending start notifications
    notify_participants = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Email addresses to notify about the start of a survey
    start_notification_emails = db.Column(
        ARRAY(db.String),
        nullable=False,
        default=[]
    )
    #: Email addresses to notify about new submissions
    new_submission_emails = db.Column(
        ARRAY(db.String),
        nullable=False,
        default=[]
    )
    #: Whether answers can be saved without submitting the survey
    partial_completion = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: The last user-friendly submission ID
    _last_friendly_submission_id = db.deferred(db.Column(
        'last_friendly_submission_id',
        db.Integer,
        nullable=False,
        default=0
    ))

    #: The list of submissions
    submissions = db.relationship(
        'SurveySubmission',
        cascade='all, delete-orphan',
        lazy=True,
        backref=db.backref(
            'survey',
            lazy=True
        )
    )

    #: The list of items
    items = db.relationship(
        'SurveyItem',
        cascade='all, delete-orphan',
        lazy=True,
        backref=db.backref(
            'survey',
            lazy=True
        )
    )
    #: The list of sections
    sections = db.relationship(
        'SurveySection',
        lazy=True,
        viewonly=True,
        order_by='SurveySection.position'
    )
    #: The list of questions
    questions = db.relationship(
        'SurveyQuestion',
        lazy=True,
        viewonly=True
    )
    #: The Event containing this survey
    event_new = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'surveys',
            lazy=True
        )
    )

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

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

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

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

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

    @locator.token
    def locator(self):
        """A locator that adds the UUID if the survey is private"""
        token = self.uuid if self.private else None
        return dict(self.locator, token=token)

    @property
    def state(self):
        if not self.questions:
            return SurveyState.not_ready
        if not self.has_started:
            return SurveyState.ready_to_open
        if not self.has_ended:
            if not self.submissions:
                return SurveyState.active_and_clean
            return SurveyState.active_and_answered
        return SurveyState.finished

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

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

    @hybrid_property
    def is_active(self):
        return not self.is_deleted and self.state in {SurveyState.active_and_answered, SurveyState.active_and_clean}

    @is_active.expression
    def is_active(cls):
        return ~cls.is_deleted & cls.questions.any() & cls.has_started & ~cls.has_ended

    @hybrid_property
    def is_visible(self):
        return (not self.is_deleted and
                self.state in {SurveyState.active_and_answered, SurveyState.active_and_clean, SurveyState.finished})

    @is_visible.expression
    def is_visible(cls):
        return ~cls.is_deleted & cls.questions.any() & cls.has_started

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

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

    def open(self):
        if self.state != SurveyState.ready_to_open:
            raise IndicoError("Survey can't be opened")
        self.start_dt = now_utc()

    def close(self):
        if self.state not in (SurveyState.active_and_clean, SurveyState.active_and_answered):
            raise IndicoError("Survey can't be closed")
        self.end_dt = now_utc()

    def send_start_notification(self):
        if not self.notifications_enabled or self.start_notification_sent or not self.event_new.has_feature('surveys'):
            return
        template_module = get_template_module('events/surveys/emails/start_notification_email.txt', survey=self)
        email = make_email(bcc_list=self.start_notification_recipients, template=template_module)
        send_email(email, event=self.event_new, module='Surveys')
        logger.info('Sending start notification for survey %s', self)
        self.start_notification_sent = True

    def send_submission_notification(self, submission):
        if not self.notifications_enabled:
            return
        template_module = get_template_module('events/surveys/emails/new_submission_email.txt', submission=submission)
        email = make_email(bcc_list=self.new_submission_emails, template=template_module)
        send_email(email, event=self.event_new, module='Surveys')
        logger.info('Sending submission notification for survey %s', self)
class VCRoomEventAssociation(db.Model):
    __tablename__ = 'vc_room_events'
    __table_args__ = tuple(_make_checks()) + (db.Index(
        None, 'data', postgresql_using='gin'), {
            'schema': 'events'
        })

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @classmethod
    def find_for_event(cls,
                       event,
                       include_hidden=False,
                       include_deleted=False,
                       only_linked_to_event=False,
                       **kwargs):
        """Return 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 ``filter_by()``
        """
        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):
        """Delete a VC room from an event.

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

        :param user: the user performing the deletion
        :param delete_all: if True, the room is detached from all
                           events and deleted.
        """
        vc_room = self.vc_room
        if delete_all:
            for assoc in vc_room.events[:]:
                Logger.get('modules.vc').info(
                    "Detaching VC room {} from event {} ({})".format(
                        vc_room, assoc.event, assoc.link_object))
                vc_room.events.remove(assoc)
        else:
            Logger.get('modules.vc').info(
                "Detaching VC room {} from event {} ({})".format(
                    vc_room, self.event, self.link_object))
            vc_room.events.remove(self)
        db.session.flush()
        if vc_room.plugin and not vc_room.events:
            Logger.get('modules.vc').info(f"Deleting VC room {vc_room}")
            if vc_room.status != VCRoomStatus.deleted:
                vc_room.plugin.delete_room(vc_room, self.event)
                notify_deleted(vc_room.plugin, vc_room, self, self.event, user)
            db.session.delete(vc_room)
Example #21
0
 def event_new(cls):
     return db.relationship('Event',
                            lazy=True,
                            backref=db.backref(cls.events_backref_name,
                                               lazy='dynamic'))
Example #22
0
class IPNetworkGroup(db.Model):
    __tablename__ = 'ip_network_groups'
    principal_type = PrincipalType.network
    principal_order = 1

    @declared_attr
    def __table_args__(cls):
        return (db.Index('ix_uq_ip_network_groups_name_lower',
                         db.func.lower(cls.name),
                         unique=True), {
                             'schema': 'indico'
                         })

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False)
    description = db.Column(db.Text, nullable=False, default='')
    #: Whether the network group is hidden in ACL forms
    hidden = db.Column(db.Boolean, nullable=False, default=False)
    #: Grants all IPs in the network group read access to all attachments
    attachment_access_override = db.Column(db.Boolean,
                                           nullable=False,
                                           default=False)

    _networks = db.relationship('IPNetwork',
                                lazy=False,
                                cascade='all, delete-orphan',
                                collection_class=set,
                                backref=db.backref('group', lazy=True))
    networks = association_proxy('_networks',
                                 'network',
                                 creator=lambda v: IPNetwork(network=v))

    # relationship backrefs:
    # - in_category_acls (CategoryPrincipal.ip_network_group)
    # - in_event_acls (EventPrincipal.ip_network_group)

    def __repr__(self):
        return format_repr(self,
                           'id',
                           'name',
                           hidden=False,
                           attachment_access_override=False)

    def __contains__(self, user):
        # This method is called via ``user in principal`` during ACL checks.
        # We have to take the IP from the request so if there's no request
        # (e.g. in the shell) we never grant IP-based access; same if we check
        # for a different user than the one from the current session.
        if not has_request_context() or not request.remote_addr:
            return False
        if session.user != user:
            return False
        return self.contains_ip(str(request.remote_addr))

    def contains_ip(self, ip):
        ip = ip_address(ip)
        return any(ip in network for network in self.networks)

    @property
    def locator(self):
        return {'network_group_id': self.id}
Example #23
0
class RegistrationFormItem(db.Model):
    """Generic registration form item"""

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'registration_form_id',
                           is_enabled=True,
                           is_deleted=False,
                           is_manager_only=False,
                           _text=self.title)
Example #24
0
 def event_new(cls):
     return db.relationship("Event", lazy=True, backref=db.backref(cls.settings_backref_name, lazy="dynamic"))
Example #25
0
class AttachmentFile(StoredFileMixin, db.Model):
    __tablename__ = 'files'
    __table_args__ = {'schema': 'attachments'}

    version_of = 'attachment'

    #: The ID of the file
    id = db.Column(db.Integer, primary_key=True)
    #: The ID of the associated attachment
    attachment_id = db.Column(db.Integer,
                              db.ForeignKey('attachments.attachments.id'),
                              nullable=False,
                              index=True)
    #: The user who uploaded the file
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        nullable=False,
                        index=True)

    #: The user who uploaded the file
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('attachment_files',
                                              lazy='dynamic'))

    # relationship backrefs:
    # - attachment (Attachment.all_files)

    @property
    def is_previewable(self):
        return get_file_previewer(self) is not None

    @no_autoflush
    def _build_storage_path(self):
        folder = self.attachment.folder
        assert folder.object is not None
        if folder.link_type == LinkType.category:
            # category/<id>/...
            path_segments = ['category', strict_str(folder.category.id)]
        else:
            # event/<id>/event/...
            path_segments = [
                'event',
                strict_str(folder.event.id), folder.link_type.name
            ]
            if folder.link_type == LinkType.session:
                # event/<id>/session/<session_id>/...
                path_segments.append(strict_str(folder.session.id))
            elif folder.link_type == LinkType.contribution:
                # event/<id>/contribution/<contribution_id>/...
                path_segments.append(strict_str(folder.contribution.id))
            elif folder.link_type == LinkType.subcontribution:
                # event/<id>/subcontribution/<subcontribution_id>/...
                path_segments.append(strict_str(folder.subcontribution.id))
        self.attachment.assign_id()
        self.assign_id()
        filename = '{}-{}-{}'.format(self.attachment.id, self.id,
                                     secure_filename(self.filename, 'file'))
        path = posixpath.join(*(path_segments + [filename]))
        return config.ATTACHMENT_STORAGE, path

    def __repr__(self):
        return '<AttachmentFile({}, {}, {}, {})>'.format(
            self.id, self.attachment_id, self.filename, self.content_type)
Example #26
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(
        JSONB
    )

    #: 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}

    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 #27
0
 def event(cls):
     return db.relationship('Event',
                            lazy=True,
                            backref=db.backref(cls.settings_backref_name,
                                               lazy='dynamic'))
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(
                f"(id != 0) OR (protection_mode != {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)
    default_badge_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')
    default_badge_template = db.relationship(
        'DesignerTemplate',
        lazy=True,
        foreign_keys=default_badge_template_id,
        backref='default_badge_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

    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)

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

    def get_hidden_events(self, user=None):
        """Get all hidden events within the given category and user."""
        from indico.modules.events import Event
        hidden_events = Event.query.with_parent(self).filter_by(
            visibility=0).all()
        return [
            event for event in hidden_events if not event.can_display(user)
        ]

    @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 #29
0
class RegistrationForm(db.Model):
    """A registration form for an event"""

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def get_personal_data_field_id(self, personal_data_type):
        """Returns the field id corresponding to the personal data field with the given name."""
        for field in self.active_fields:
            if (isinstance(field, RegistrationFormPersonalDataField)
                    and field.personal_data_type == personal_data_type):
                return field.id
Example #30
0
class Editable(db.Model):
    __tablename__ = 'editables'
    __table_args__ = (db.UniqueConstraint('contribution_id', 'type'), {
        'schema': 'event_editing'
    })

    id = db.Column(db.Integer, primary_key=True)
    contribution_id = db.Column(db.ForeignKey('events.contributions.id'),
                                index=True,
                                nullable=False)
    type = db.Column(PyIntEnum(EditableType), nullable=False)
    editor_id = db.Column(db.ForeignKey('users.users.id'),
                          index=True,
                          nullable=True)
    published_revision_id = db.Column(
        db.ForeignKey('event_editing.revisions.id'), index=True, nullable=True)

    contribution = db.relationship('Contribution',
                                   lazy=True,
                                   backref=db.backref(
                                       'editables',
                                       lazy=True,
                                   ))
    editor = db.relationship('User',
                             lazy=True,
                             backref=db.backref('editor_for_editables',
                                                lazy='dynamic'))
    published_revision = db.relationship(
        'EditingRevision',
        foreign_keys=published_revision_id,
        lazy=True,
    )

    # relationship backrefs:
    # - revisions (EditingRevision.editable)

    def __repr__(self):
        return format_repr(self, 'id', 'contribution_id', 'type')

    @locator_property
    def locator(self):
        return dict(self.contribution.locator, type=self.type.name)

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

    def _has_general_editor_permissions(self, user):
        """Whether the user has general editor permissions on the Editable.

        This means that the user has editor permissions for the editable's type,
        but does not need to be the assigned editor.
        """
        # Editing (and event) managers always have editor-like access
        return (self.event.can_manage(user, permission='editing_manager')
                or self.event.can_manage(
                    user, permission=self.type.editor_permission))

    def can_see_timeline(self, user):
        """Whether the user can see the editable's timeline.

        This is pure read access, without any ability to make changes
        or leave comments.
        """
        # Anyone with editor access to the editable's type can see the timeline.
        # Users associated with the editable's contribution can do so as well.
        return (self._has_general_editor_permissions(user)
                or self.contribution.can_submit_proceedings(user)
                or self.contribution.is_user_associated(user,
                                                        check_abstract=True))

    def can_perform_submitter_actions(self, user):
        """Whether the user can perform any submitter actions.

        These are actions such as uploading a new revision after having
        been asked to make changes or approving/rejecting changes made
        by an editor.
        """
        # If the user can't even see the timeline, we never allow any modifications
        if not self.can_see_timeline(user):
            return False
        # Anyone who can submit new proceedings can also perform submitter actions,
        # i.e. the abstract submitter and anyone with submission access to the contribution.
        return self.contribution.can_submit_proceedings(user)

    def can_perform_editor_actions(self, user):
        """Whether the user can perform any Editing actions.

        These are actions usually made by the assigned Editor of the
        editable, such as making changes, asking the user to make changes,
        or approving/rejecting the editable.
        """
        from indico.modules.events.editing.settings import editable_type_settings

        # If the user can't even see the timeline, we never allow any modifications
        if not self.can_see_timeline(user):
            return False
        # Editing/event managers can perform actions when they are the assigned editor
        # even when editing is disabled in the settings
        if self.editor == user and self.event.can_manage(
                user, permission='editing_manager'):
            return True
        # Editing needs to be enabled in the settings otherwise
        if not editable_type_settings[self.type].get(self.event,
                                                     'editing_enabled'):
            return False
        # Editors need the permission on the editable type and also be the assigned editor
        if self.editor == user and self.event.can_manage(
                user, permission=self.type.editor_permission):
            return True
        return False

    def can_use_internal_comments(self, user):
        """Whether the user can create/see internal comments."""
        return self._has_general_editor_permissions(user)

    def can_comment(self, user):
        """Whether the user can comment on the editable."""
        # We allow any user associated with the contribution to comment, even if they are
        # not authorized to actually perform submitter actions.
        return (
            self.event.can_manage(user, permission=self.type.editor_permission)
            or self.event.can_manage(user, permission='editing_manager')
            or self.contribution.is_user_associated(user, check_abstract=True))

    def can_assign_self(self, user):
        """Whether the user can assign themselves on the editable."""
        from indico.modules.events.editing.settings import editable_type_settings
        type_settings = editable_type_settings[self.type]
        if self.editor and (self.editor == user
                            or not self.can_unassign(user)):
            return False
        return ((self.event.can_manage(user,
                                       permission=self.type.editor_permission)
                 and type_settings.get(self.event, 'editing_enabled')
                 and type_settings.get(self.event, 'self_assign_allowed'))
                or self.event.can_manage(user, permission='editing_manager'))

    def can_unassign(self, user):
        """Whether the user can unassign the editor of the editable."""
        from indico.modules.events.editing.settings import editable_type_settings
        type_settings = editable_type_settings[self.type]
        return (self.event.can_manage(user, permission='editing_manager')
                or (self.editor == user and self.event.can_manage(
                    user, permission=self.type.editor_permission)
                    and type_settings.get(self.event, 'editing_enabled')
                    and type_settings.get(self.event, 'self_assign_allowed')))

    @property
    def review_conditions_valid(self):
        from indico.modules.events.editing.models.review_conditions import EditingReviewCondition
        query = EditingReviewCondition.query.with_parent(
            self.event).filter_by(type=self.type)
        review_conditions = [{ft.id
                              for ft in cond.file_types} for cond in query]
        file_types = {file.file_type_id for file in self.revisions[-1].files}
        if not review_conditions:
            return True
        return any(file_types >= cond for cond in review_conditions)

    @property
    def editing_enabled(self):
        from indico.modules.events.editing.settings import editable_type_settings
        return editable_type_settings[self.type].get(self.event,
                                                     'editing_enabled')

    @property
    def external_timeline_url(self):
        return url_for('event_editing.editable', self, _external=True)

    @property
    def timeline_url(self):
        return url_for('event_editing.editable', self)

    def log(self, *args, **kwargs):
        """Log with prefilled metadata for the editable."""
        self.event.log(*args, meta={'editable_id': self.id}, **kwargs)
Example #31
0
class Reservation(Serializer, db.Model):
    __tablename__ = 'reservations'
    __public__ = []
    __calendar_public__ = [
        'id', ('booked_for_name', 'bookedForName'),
        ('booking_reason', 'reason'), ('details_url', 'bookingUrl')
    ]
    __api_public__ = [
        'id', ('start_dt', 'startDT'), ('end_dt', 'endDT'),
        'repeat_frequency', 'repeat_interval',
        ('booked_for_name', 'bookedForName'), ('details_url', 'bookingUrl'),
        ('booking_reason', 'reason'), ('uses_vc', 'usesAVC'),
        ('needs_vc_assistance', 'needsAVCSupport'), 'needs_assistance',
        ('is_accepted', 'isConfirmed'), ('is_valid', '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)), {
                             '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)
    is_accepted = db.Column(db.Boolean, nullable=False)
    is_cancelled = db.Column(db.Boolean, nullable=False, default=False)
    is_rejected = db.Column(db.Boolean, nullable=False, default=False)
    booking_reason = db.Column(db.Text, nullable=False)
    rejection_reason = db.Column(db.String)
    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)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         nullable=True,
                         index=True)

    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'))
    #: The Event this reservation was made for
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('reservations', lazy='dynamic'))

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

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

    @hybrid_property
    def is_pending(self):
        return not (self.is_accepted or self.is_rejected or self.is_cancelled)

    @is_pending.expression
    def is_pending(self):
        return ~(Reservation.is_accepted | Reservation.is_rejected
                 | Reservation.is_cancelled)

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

    @hybrid_property
    def is_valid(self):
        return self.is_accepted and not (self.is_rejected or self.is_cancelled)

    @is_valid.expression
    def is_valid(self):
        return self.is_accepted & ~(self.is_rejected | self.is_cancelled)

    @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 details_url(self):
        return url_for('rooms.roomBooking-bookingDetails',
                       self,
                       _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_valid:
            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))

    @return_ascii
    def __repr__(self):
        return u'<Reservation({0}, {1}, {2}, {3}, {4})>'.format(
            self.id, self.room_id, self.booked_for_name, self.start_dt,
            self.end_dt)

    @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', 'event_id')
        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_be_booked(user, ignore_admin=ignore_admin)
            if prebook and not room.can_be_prebooked(
                    user, ignore_admin=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.is_accepted = not prebook
        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'))
        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)

    @unify_user_args
    def accept(self, user):
        self.is_accepted = True
        self.add_edit_log(
            ReservationEditLog(user_name=user.full_name,
                               info=['Reservation accepted']))
        notify_confirmation(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')

    @unify_user_args
    def cancel(self, user, reason=None, silent=False):
        self.is_cancelled = True
        self.rejection_reason = reason
        self.occurrences.filter_by(is_valid=True).update(
            {
                'is_cancelled': True,
                'rejection_reason': reason
            },
            synchronize_session='fetch')
        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]))

    @unify_user_args
    def reject(self, user, reason, silent=False):
        self.is_rejected = True
        self.rejection_reason = reason
        self.occurrences.filter_by(is_valid=True).update(
            {
                'is_rejected': True,
                'rejection_reason': reason
            },
            synchronize_session='fetch')
        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()

    @unify_user_args
    def can_be_accepted(self, user):
        if user is None:
            return False
        return rb_is_admin(user) or self.room.is_owned_by(user)

    @unify_user_args
    def can_be_cancelled(self, user):
        if user is None:
            return False
        return self.is_owned_by(user) or rb_is_admin(
            user) or self.is_booked_for(user)

    @unify_user_args
    def can_be_deleted(self, user):
        if user is None:
            return False
        return rb_is_admin(user)

    @unify_user_args
    def can_be_modified(self, user):
        if user is None:
            return False
        if self.is_rejected or self.is_cancelled:
            return False
        if rb_is_admin(user):
            return True
        return self.created_by_user == user or self.is_booked_for(
            user) or self.room.is_owned_by(user)

    @unify_user_args
    def can_be_rejected(self, user):
        if user is None:
            return False
        return rb_is_admin(user) or self.room.is_owned_by(user)

    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

    @unify_user_args
    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 #32
0
 def event(cls):
     return db.relationship('Event',
                            foreign_keys=cls.event_id,
                            lazy=True,
                            backref=db.backref(cls.events_backref_name,
                                               lazy='dynamic'))
Example #33
0
class Request(db.Model):
    """Event-related requests, e.g. for a webcast."""
    __tablename__ = 'requests'
    __table_args__ = {'schema': 'events'}

    #: request 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,
                         nullable=False)
    #: the request type name
    type = db.Column(db.String, nullable=False)
    #: the requests's date, a :class:`RequestState` value
    state = db.Column(PyIntEnum(RequestState),
                      nullable=False,
                      default=RequestState.pending)
    #: plugin-specific data of the request
    data = db.Column(JSONB, nullable=False)
    #: ID of the user creating the request
    created_by_id = db.Column(db.Integer,
                              db.ForeignKey('users.users.id'),
                              index=True,
                              nullable=False)
    #: the date/time the request was created
    created_dt = db.Column(UTCDateTime,
                           default=now_utc,
                           index=True,
                           nullable=False)
    #: ID of the user processing the request
    processed_by_id = db.Column(db.Integer,
                                db.ForeignKey('users.users.id'),
                                index=True,
                                nullable=True)
    #: the date/time the request was accepted/rejected
    processed_dt = db.Column(UTCDateTime, nullable=True)
    #: an optional comment for an accepted/rejected request
    comment = db.Column(db.Text, nullable=True)

    #: The user who created the request
    created_by_user = db.relationship('User',
                                      lazy=True,
                                      foreign_keys=[created_by_id],
                                      backref=db.backref('requests_created',
                                                         lazy='dynamic'))
    #: The user who processed the request
    processed_by_user = db.relationship('User',
                                        lazy=True,
                                        foreign_keys=[processed_by_id],
                                        backref=db.backref(
                                            'requests_processed',
                                            lazy='dynamic'))
    #: The Event this agreement is associated with
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('requests', lazy='dynamic'))

    @property
    def definition(self):
        return get_request_definitions().get(self.type)

    @definition.setter
    def definition(self, definition):
        assert self.type is None
        self.type = definition.name

    @property
    def can_be_modified(self):
        """Determine if the request can be modified or if a new one must be sent."""
        return self.state in {RequestState.pending, RequestState.accepted}

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

    def __repr__(self):
        state = self.state.name if self.state is not None else None
        return f'<Request({self.id}, {self.event_id}, {self.type}, {state})>'

    @classmethod
    def find_latest_for_event(cls, event, type_=None):
        """Return the latest requests for a given event.

        :param event: the event to find the requests for
        :param type_: the request type to retrieve, or `None` to get all
        :return: a dict mapping request types to a :class:`Request`
                 or if `type_` was specified, a single :class:`Request` or `None`
        """
        query = Request.query.with_parent(event)
        if type_ is not None:
            return (query.filter_by(type=type_).order_by(
                cls.created_dt.desc()).first())
        else:
            query = limit_groups(query, cls, cls.type, cls.created_dt.desc(),
                                 1)
            return {req.type: req for req in query}
Example #34
0
class User(db.Model):
    """Indico users"""
    __tablename__ = 'users'
    __table_args__ = {'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),
    )
    _favorite_categories = db.relationship('FavoriteCategory',
                                           lazy=True,
                                           cascade='all, delete-orphan',
                                           collection_class=set)
    #: the users's favorite categories
    favorite_categories = association_proxy(
        '_favorite_categories',
        'target',
        creator=lambda x: FavoriteCategory(target=x))
    #: the legacy objects the user is connected to
    linked_objects = db.relationship('UserLink',
                                     lazy='dynamic',
                                     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:
    # - local_groups (User.local_groups)

    @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

    @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 locator(self):
        return {'user_id': self.id}

    @hybrid_property
    def title(self):
        """the title of the user"""
        return self._title.title

    @title.expression
    def title(cls):
        return cls._title

    @title.setter
    def title(self, value):
        self._title = value

    @hybrid_property
    def is_deleted(self):
        return self._is_deleted

    @is_deleted.setter
    def is_deleted(self, value):
        self._is_deleted = value
        # not using _all_emails here since it only contains newly added emails after an expire/commit
        if self._primary_email:
            self._primary_email.is_user_deleted = value
        for email in self._secondary_emails:
            email.is_user_deleted = value

    @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 full_name(self):
        """Returns the user's name in 'Firstname Lastname' notation."""
        return self.get_full_name(last_name_first=False,
                                  last_name_upper=False,
                                  abbrev_first_name=False)

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

    @return_ascii
    def __repr__(self):
        return '<User({}, {}, {}, {})>'.format(self.id, self.first_name,
                                               self.last_name, self.email)

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

    def get_full_name(self,
                      last_name_first=True,
                      last_name_upper=True,
                      abbrev_first_name=True,
                      show_title=False):
        """Returns the user's name in the specified 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
        :param show_title: if the title of the user should be included
        """
        last_name = self.last_name.upper(
        ) if last_name_upper else self.last_name
        first_name = '{}.'.format(self.first_name[0].upper()
                                  ) if abbrev_first_name else self.first_name
        full_name = '{}, {}'.format(
            last_name, first_name) if last_name_first else '{} {}'.format(
                first_name, last_name)
        return full_name if not show_title or not self.title else '{} {}'.format(
            self.title, full_name)

    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 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 is_in_group(self, group):
        """Checks if the user is in a group

        :param group: A :class:`GroupProxy`
        """
        return group.has_member(self)

    def get_linked_roles(self, type_):
        """Retrieves the roles the user is linked to for a given type"""
        return UserLink.get_linked_roles(self, type_)

    def get_linked_objects(self, type_, role):
        """Retrieves linked objects for the user"""
        return UserLink.get_links(self, type_, role)

    def link_to(self, obj, role):
        """Adds a link between the user and an object

        :param obj: a legacy object
        :param role: the role to use in the link
        """
        return UserLink.create_link(self, obj, role)

    def unlink_to(self, obj, role):
        """Removes a link between the user and an object

        :param obj: a legacy object
        :param role: the role to use in the link
        """
        return UserLink.remove_link(self, obj, role)

    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('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 #35
0
class Attachment(ProtectionMixin, VersionedResourceMixin, db.Model):
    __tablename__ = 'attachments'
    __table_args__ = (
        # links: url but no file
        db.CheckConstraint(
            f'type != {AttachmentType.link.value} OR (link_url IS NOT NULL AND file_id IS NULL)',
            'valid_link'),
        # we can't require the file_id to be NOT NULL for files because of the circular relationship...
        # but we can ensure that we never have both a file_id AND a link_url...for
        db.CheckConstraint('link_url IS NULL OR file_id IS NULL',
                           'link_or_file'),
        {
            'schema': 'attachments'
        })

    stored_file_table = 'attachments.files'
    stored_file_class = AttachmentFile
    stored_file_fkey = 'attachment_id'

    #: The ID of the attachment
    id = db.Column(db.Integer, primary_key=True)
    #: The ID of the folder the attachment belongs to
    folder_id = db.Column(db.Integer,
                          db.ForeignKey('attachments.folders.id'),
                          nullable=False,
                          index=True)
    #: The ID of the user who created the attachment
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=False)
    #: If the attachment has been deleted
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: The name of the attachment
    title = db.Column(db.String, nullable=False)
    #: The description of the attachment
    description = db.Column(db.Text, nullable=False, default='')
    #: The date/time when the attachment was created/modified
    modified_dt = db.Column(UTCDateTime,
                            nullable=False,
                            default=now_utc,
                            onupdate=now_utc)
    #: The type of the attachment (file or link)
    type = db.Column(PyIntEnum(AttachmentType), nullable=False)
    #: The target URL for a link attachment
    link_url = db.Column(db.String, nullable=True)

    #: The user who created the attachment
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('attachments', lazy='dynamic'))
    #: The folder containing the attachment
    folder = db.relationship('AttachmentFolder',
                             lazy=True,
                             backref=db.backref('all_attachments', lazy=True))
    acl_entries = db.relationship('AttachmentPrincipal',
                                  backref='attachment',
                                  cascade='all, delete-orphan',
                                  collection_class=set)
    #: The ACL of the folder (used for ProtectionMode.protected)
    acl = association_proxy('acl_entries',
                            'principal',
                            creator=lambda v: AttachmentPrincipal(principal=v))

    # relationship backrefs:
    # - legacy_mapping (LegacyAttachmentMapping.attachment)

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

    @property
    def locator(self):
        return dict(self.folder.locator, attachment_id=self.id)

    def get_download_url(self, absolute=False):
        """Return the download url for the attachment.

        During static site generation this returns a local URL for the
        file or the target URL for the link.

        :param absolute: If the returned URL should be absolute.
        """
        if g.get('static_site'):
            return _offline_download_url(self)
        else:
            filename = self.file.filename if self.type == AttachmentType.file else 'go'
            return url_for('attachments.download',
                           self,
                           filename=filename,
                           _external=absolute)

    @property
    def download_url(self):
        """The download url for the attachment."""
        return self.get_download_url()

    @property
    def absolute_download_url(self):
        """The absolute download url for the attachment."""
        return self.get_download_url(absolute=True)

    def can_access(self, user, *args, **kwargs):
        """Check if the user is allowed to access the attachment.

        This is the case if the user has access to see the attachment
        or if the user can manage attachments for the linked object.
        """
        return (super().can_access(user, *args, **kwargs)
                or can_manage_attachments(self.folder.object, user))

    def __repr__(self):
        return '<Attachment({}, {}, {}{}, {}, {})>'.format(
            self.id, self.title,
            self.file if self.type == AttachmentType.file else self.link_url,
            ', is_deleted=True' if self.is_deleted else '',
            self.protection_repr, self.folder_id)
Example #36
0
class SubContribution(DescriptionMixin, AttachedItemsMixin, AttachedNotesMixin,
                      db.Model):
    __tablename__ = 'subcontributions'
    __table_args__ = (db.Index(None,
                               'friendly_id',
                               'contribution_id',
                               unique=True), {
                                   'schema': 'events'
                               })

    PRELOAD_EVENT_ATTACHED_ITEMS = True
    PRELOAD_EVENT_ATTACHED_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'subcontribution_id'
    description_wrapper = MarkdownText

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def can_manage(self, user, role=None, **kwargs):
        return self.contribution.can_manage(user, role, **kwargs)
Example #37
0
 def reference_type(cls):
     return db.relationship('ReferenceType',
                            lazy=False,
                            backref=db.backref(cls.reference_backref_name,
                                               cascade='all, delete-orphan',
                                               lazy=True))
Example #38
0
class PaperFile(StoredFileMixin, db.Model):
    __tablename__ = 'files'
    __table_args__ = {'schema': 'event_paper_reviewing'}

    # StoredFileMixin settings
    add_file_date_column = False

    id = db.Column(db.Integer, primary_key=True)
    _contribution_id = db.Column('contribution_id',
                                 db.Integer,
                                 db.ForeignKey('events.contributions.id'),
                                 index=True,
                                 nullable=False)
    revision_id = db.Column(
        db.Integer,
        db.ForeignKey('event_paper_reviewing.revisions.id'),
        index=True,
        nullable=True)

    _contribution = db.relationship('Contribution',
                                    lazy=True,
                                    backref=db.backref('_paper_files',
                                                       lazy=True))
    paper_revision = db.relationship('PaperRevision',
                                     lazy=True,
                                     backref=db.backref(
                                         'files',
                                         lazy=True,
                                         cascade='all, delete-orphan'))

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

    def __repr__(self):
        return format_repr(self,
                           'id',
                           '_contribution_id',
                           content_type=None,
                           _text=text_to_repr(self.filename))

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

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

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

    def _build_storage_path(self):
        self.assign_id()
        path_segments = [
            'event',
            strict_str(self._contribution.event.id), 'papers',
            '{}_{}'.format(self.id, strict_str(self._contribution.id))
        ]
        filename = secure_filename(self.filename, 'paper')
        path = posixpath.join(*(path_segments + [filename]))
        return config.ATTACHMENT_STORAGE, path
Example #39
0
class PaymentTransaction(db.Model):
    """Payment transactions"""
    __tablename__ = 'payment_transactions'
    __table_args__ = (db.CheckConstraint('amount > 0', 'positive_amount'), {
        'schema': 'events'
    })

    #: Entry ID
    id = db.Column(db.Integer, primary_key=True)
    #: ID of the associated registration
    registration_id = db.Column(
        db.Integer,
        db.ForeignKey('event_registration.registrations.id'),
        index=True,
        nullable=False)
    #: a :class:`TransactionStatus`
    status = db.Column(PyIntEnum(TransactionStatus), nullable=False)
    #: the base amount the user needs to pay (without payment-specific fees)
    amount = db.Column(
        db.Numeric(8, 2),  # max. 999999.99
        nullable=False)
    #: the currency of the payment (ISO string, e.g. EUR or USD)
    currency = db.Column(db.String, nullable=False)
    #: the provider of the payment (e.g. manual, PayPal etc.)
    provider = db.Column(db.String, nullable=False, default='_manual')
    #: the date and time the transaction was recorded
    timestamp = db.Column(UTCDateTime, default=now_utc, nullable=False)
    #: plugin-specific data of the payment
    data = db.Column(JSONB, nullable=False)

    #: The associated registration
    registration = db.relationship('Registration',
                                   lazy=True,
                                   foreign_keys=[registration_id],
                                   backref=db.backref(
                                       'transactions',
                                       cascade='all, delete-orphan',
                                       lazy=True))

    @property
    def plugin(self):
        from indico.modules.events.payment.util import get_payment_plugins
        return get_payment_plugins().get(self.provider)

    @property
    def is_manual(self):
        return self.provider == '_manual'

    @return_ascii
    def __repr__(self):
        # in case of a new object we might not have the default status set
        status = TransactionStatus(
            self.status).name if self.status is not None else None
        return format_repr(self,
                           'id',
                           'registration_id',
                           'provider',
                           'amount',
                           'currency',
                           'timestamp',
                           status=status)

    def render_details(self):
        """Renders the transaction details"""
        if self.is_manual:
            return render_template(
                'events/payment/transaction_details_manual.html',
                transaction=self)
        plugin = self.plugin
        if plugin is None:
            return '[plugin not loaded: {}]'.format(self.provider)
        with plugin.plugin_context():
            return plugin.render_transaction_details(self)

    @classmethod
    def create_next(cls,
                    registration,
                    amount,
                    currency,
                    action,
                    provider=None,
                    data=None):
        previous_transaction = registration.transaction
        new_transaction = PaymentTransaction(amount=amount,
                                             currency=currency,
                                             provider=provider,
                                             data=data)
        try:
            next_status = TransactionStatusTransition.next(
                previous_transaction, action, provider)
        except InvalidTransactionStatus as e:
            Logger.get('payment').exception("%s (data received: %r)", e, data)
            return None
        except InvalidManualTransactionAction as e:
            Logger.get('payment').exception(
                "Invalid manual action code '%s' on initial status (data received: %r)",
                e, data)
            return None
        except InvalidTransactionAction as e:
            Logger.get('payment').exception(
                "Invalid action code '%s' on initial status (data received: %r)",
                e, data)
            return None
        except IgnoredTransactionAction as e:
            Logger.get('payment').warning("%s (data received: %r)", e, data)
            return None
        except DoublePaymentTransaction:
            next_status = TransactionStatus.successful
            Logger.get('payment').info(
                "Received successful payment for an already paid registration")
        registration.transaction = new_transaction
        new_transaction.status = next_status
        return new_transaction
Example #40
0
class SessionBlock(LocationMixin, db.Model):
    __tablename__ = 'session_blocks'
    __auto_table_args = (
        db.UniqueConstraint(
            'id', 'session_id'),  # useless but needed for the compound fkey
        db.CheckConstraint("date_trunc('minute', duration) = duration",
                           'duration_no_seconds'),
        {
            'schema': 'events'
        })
    location_backref_name = 'session_blocks'

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

    id = db.Column(db.Integer, primary_key=True)
    session_id = db.Column(db.Integer,
                           db.ForeignKey('events.sessions.id'),
                           index=True,
                           nullable=False)
    title = db.Column(db.String, nullable=False, default='')
    code = db.Column(db.String, nullable=False, default='')
    duration = db.Column(db.Interval, nullable=False)

    #: Persons associated with this session block
    person_links = db.relationship('SessionBlockPersonLink',
                                   lazy=True,
                                   cascade='all, delete-orphan',
                                   backref=db.backref('session_block',
                                                      lazy=True))

    # relationship backrefs:
    # - contributions (Contribution.session_block)
    # - legacy_mapping (LegacySessionBlockMapping.session_block)
    # - room_reservation_links (ReservationLink.session_block)
    # - session (Session.blocks)
    # - timetable_entry (TimetableEntry.session_block)
    # - vc_room_associations (VCRoomEventAssociation.linked_block)

    @declared_attr
    def contribution_count(cls):
        from indico.modules.events.contributions.models.contributions import Contribution
        query = (db.select([
            db.func.count(Contribution.id)
        ]).where((Contribution.session_block_id == cls.id)
                 & ~Contribution.is_deleted).correlate_except(
                     Contribution).scalar_subquery())
        return db.column_property(query, deferred=True)

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

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

    @locator_property
    def locator(self):
        return dict(self.session.locator, block_id=self.id)

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

    def can_access(self, user, allow_admin=True):
        return self.session.can_access(user, allow_admin=allow_admin)

    @property
    def has_note(self):
        return self.session.has_note

    @property
    def note(self):
        return self.session.note

    @property
    def full_title(self):
        return f'{self.session.title}: {self.title}' if self.title else self.session.title

    def can_manage(self, user, allow_admin=True):
        return self.session.can_manage_blocks(user, allow_admin=allow_admin)

    def can_manage_attachments(self, user):
        return self.session.can_manage_attachments(user)

    def can_edit_note(self, user):
        return self.session.can_edit_note(user)

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

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

    @property
    def slug(self):
        return slugify('b', self.id, self.session.title, self.title, maxlen=30)

    def __repr__(self):
        return format_repr(self, 'id', _text=self.title or None)
Example #41
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(Session, self).__init__(**kwargs)

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

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

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

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

    @property
    @memoize_request
    def start_dt(self):
        from 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)

    @return_ascii
    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 #42
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_event_role = False
    is_network = False
    principal_order = 0
    principal_type = PrincipalType.user

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

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

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

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

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

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

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

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

    @property
    def as_avatar(self):
        # TODO: remove this after DB is free of Avatars
        from 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}

    @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 #43
0
class Abstract(ProposalMixin, ProposalRevisionMixin, DescriptionMixin,
               CustomFieldsMixin, AuthorsSpeakersMixin, db.Model):
    """Represents an abstract that can be associated to a Contribution."""

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def can_review(self, user, check_state=False):
        # The total number of tracks/events a user is a reviewer for (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, role='abstract_reviewer', explicit_role=True):
            return False
        elif self.event in user.global_abstract_reviewer_for_events:
            return True
        elif user.abstract_reviewer_for_tracks & self.reviewed_for_tracks:
            return True
        else:
            return False

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

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

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

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

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

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

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

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

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

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

    def user_owns(self, user):
        if not user:
            return None
        return user == self.submitter or any(x.person.user == user
                                             for x in self.person_links)
Example #44
0
class TimetableEntry(db.Model):
    __tablename__ = 'timetable_entries'

    @declared_attr
    def __table_args__(cls):
        return (
            db.Index('ix_timetable_entries_start_dt_desc',
                     cls.start_dt.desc()),
            _make_check(TimetableEntryType.SESSION_BLOCK, 'session_block_id'),
            _make_check(TimetableEntryType.CONTRIBUTION, 'contribution_id'),
            _make_check(TimetableEntryType.BREAK, 'break_id'),
            db.CheckConstraint(
                f'type != {TimetableEntryType.SESSION_BLOCK} OR parent_id IS NULL',
                'valid_parent'), {
                    '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)
    parent_id = db.Column(
        db.Integer,
        db.ForeignKey('events.timetable_entries.id'),
        index=True,
        nullable=True,
    )
    session_block_id = db.Column(db.Integer,
                                 db.ForeignKey('events.session_blocks.id'),
                                 index=True,
                                 unique=True,
                                 nullable=True)
    contribution_id = db.Column(db.Integer,
                                db.ForeignKey('events.contributions.id'),
                                index=True,
                                unique=True,
                                nullable=True)
    break_id = db.Column(db.Integer,
                         db.ForeignKey('events.breaks.id'),
                         index=True,
                         unique=True,
                         nullable=True)
    type = db.Column(PyIntEnum(TimetableEntryType), nullable=False)
    start_dt = db.Column(UTCDateTime, nullable=False)

    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref(
                                'timetable_entries',
                                order_by=lambda: TimetableEntry.start_dt,
                                cascade='all, delete-orphan',
                                lazy='dynamic'))
    session_block = db.relationship('SessionBlock',
                                    lazy=False,
                                    backref=db.backref(
                                        'timetable_entry',
                                        cascade='all, delete-orphan',
                                        uselist=False,
                                        lazy=True))
    contribution = db.relationship('Contribution',
                                   lazy=False,
                                   backref=db.backref(
                                       'timetable_entry',
                                       cascade='all, delete-orphan',
                                       uselist=False,
                                       lazy=True))
    break_ = db.relationship('Break',
                             cascade='all, delete-orphan',
                             single_parent=True,
                             lazy=False,
                             backref=db.backref('timetable_entry',
                                                cascade='all, delete-orphan',
                                                uselist=False,
                                                lazy=True))
    children = db.relationship('TimetableEntry',
                               order_by='TimetableEntry.start_dt',
                               lazy=True,
                               backref=db.backref('parent',
                                                  remote_side=[id],
                                                  lazy=True))

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

    @property
    def object(self):
        if self.type == TimetableEntryType.SESSION_BLOCK:
            return self.session_block
        elif self.type == TimetableEntryType.CONTRIBUTION:
            return self.contribution
        elif self.type == TimetableEntryType.BREAK:
            return self.break_

    @object.setter
    def object(self, value):
        from indico.modules.events.contributions import Contribution
        from indico.modules.events.sessions.models.blocks import SessionBlock
        from indico.modules.events.timetable.models.breaks import Break
        self.session_block = self.contribution = self.break_ = None
        if isinstance(value, SessionBlock):
            self.session_block = value
        elif isinstance(value, Contribution):
            self.contribution = value
        elif isinstance(value, Break):
            self.break_ = value
        elif value is not None:
            raise TypeError(f'Unexpected object: {value}')

    @hybrid_property
    def duration(self):
        return self.object.duration if self.object is not None else None

    @duration.setter
    def duration(self, value):
        self.object.duration = value

    @duration.expression
    def duration(cls):
        from indico.modules.events.contributions import Contribution
        from indico.modules.events.sessions.models.blocks import SessionBlock
        from indico.modules.events.timetable.models.breaks import Break
        return db.case(
            {
                TimetableEntryType.SESSION_BLOCK.value:
                db.select([SessionBlock.duration]).where(
                    SessionBlock.id == cls.session_block_id).correlate_except(
                        SessionBlock).scalar_subquery(),
                TimetableEntryType.CONTRIBUTION.value:
                db.select([Contribution.duration]).where(
                    Contribution.id == cls.contribution_id).correlate_except(
                        Contribution).scalar_subquery(),
                TimetableEntryType.BREAK.value:
                db.select([Break.duration]).where(Break.id == cls.break_id).
                correlate_except(Break).scalar_subquery(),
            },
            value=cls.type)

    @hybrid_property
    def end_dt(self):
        if self.start_dt is None or self.duration is None:
            return None
        return self.start_dt + self.duration

    @end_dt.expression
    def end_dt(cls):
        return cls.start_dt + cls.duration

    @property
    def session_siblings(self):
        if self.type == TimetableEntryType.SESSION_BLOCK:
            return [
                x for x in self.siblings if x.session_block
                and x.session_block.session == self.session_block.session
            ]
        elif self.parent:
            return self.siblings
        else:
            return []

    @property
    def siblings(self):
        from indico.modules.events.timetable.util import get_nested_entries, get_top_level_entries
        tzinfo = self.event.tzinfo
        day = self.start_dt.astimezone(tzinfo).date()
        siblings = (get_nested_entries(self.event)[self.parent_id]
                    if self.parent_id else get_top_level_entries(self.event))
        return [
            x for x in siblings
            if x.start_dt.astimezone(tzinfo).date() == day and x.id != self.id
        ]

    @property
    def siblings_query(self):
        tzinfo = self.event.tzinfo
        day = self.start_dt.astimezone(tzinfo).date()
        criteria = (TimetableEntry.id != self.id,
                    TimetableEntry.parent == self.parent,
                    db.cast(TimetableEntry.start_dt.astimezone(tzinfo),
                            db.Date) == day)
        return TimetableEntry.query.with_parent(self.event).filter(*criteria)

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

    def __repr__(self):
        return format_repr(self,
                           'id',
                           'type',
                           'start_dt',
                           'end_dt',
                           _repr=self.object)

    def can_view(self, user):
        """Check whether the user will see this entry in the timetable."""
        if self.type in (TimetableEntryType.CONTRIBUTION,
                         TimetableEntryType.BREAK):
            return self.object.can_access(user)
        elif self.type == TimetableEntryType.SESSION_BLOCK:
            if self.object.can_access(user):
                return True
            return any(x.can_access(user) for x in self.object.contributions)

    def extend_start_dt(self, start_dt):
        assert start_dt < self.start_dt
        extension = self.start_dt - start_dt
        self.start_dt = start_dt
        self.duration = self.duration + extension

    def extend_end_dt(self, end_dt):
        diff = end_dt - self.end_dt
        if diff < timedelta(0):
            raise ValueError('New end_dt is before current end_dt.')
        self.duration += diff

    def extend_parent(self, by_start=True, by_end=True):
        """Extend start/end of parent objects if needed.

        No extension if performed for entries crossing a day boundary in the
        event timezone.

        :param by_start: Extend parent by start datetime.
        :param by_end: Extend parent by end datetime.
        """
        tzinfo = self.event.tzinfo
        if self.start_dt.astimezone(tzinfo).date() != self.end_dt.astimezone(
                tzinfo).date():
            return
        if self.parent is None:
            if by_start and self.start_dt < self.event.start_dt:
                self.event.start_dt = self.start_dt
            if by_end and self.end_dt > self.event.end_dt:
                self.event.end_dt = self.end_dt
        else:
            extended = False
            if by_start and self.start_dt < self.parent.start_dt:
                self.parent.extend_start_dt(self.start_dt)
                extended = True
            if by_end and self.end_dt > self.parent.end_dt:
                self.parent.extend_end_dt(self.end_dt)
                extended = True
            if extended:
                self.parent.extend_parent(by_start=by_start, by_end=by_end)

    def is_parallel(self, in_session=False):
        siblings = self.siblings if not in_session else self.session_siblings
        for sibling in siblings:
            if overlaps((self.start_dt, self.end_dt),
                        (sibling.start_dt, sibling.end_dt)):
                return True
        return False

    def move(self, start_dt):
        """Move the entry to start at a different time.

        This method automatically moves children of the entry to
        preserve their start time relative to the parent's start time.
        """
        if self.type == TimetableEntryType.SESSION_BLOCK:
            diff = start_dt - self.start_dt
            for child in self.children:
                child.start_dt += diff
        self.start_dt = start_dt

    def move_next_to(self, sibling, position='before'):
        if sibling not in self.siblings:
            raise ValueError('Not a sibling')
        if position not in ('before', 'after'):
            raise ValueError('Invalid position')
        if position == 'before':
            start_dt = sibling.start_dt - self.duration
        else:
            start_dt = sibling.end_dt
        self.move(start_dt)
Example #45
0
class RegistrationInvitation(db.Model):
    """An invitation for someone to register."""
    __tablename__ = 'invitations'
    __table_args__ = (db.CheckConstraint("(state = {state}) OR (registration_id IS NULL)"
                                         .format(state=InvitationState.accepted), name='registration_state'),
                      db.UniqueConstraint('registration_form_id', 'email'),
                      {'schema': 'event_registration'})

    #: The ID of the invitation
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: The UUID of the invitation
    uuid = db.Column(
        UUID,
        index=True,
        unique=True,
        nullable=False,
        default=lambda: str(uuid4())
    )
    #: 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 registration (if accepted)
    registration_id = db.Column(
        db.Integer,
        db.ForeignKey('event_registration.registrations.id'),
        index=True,
        unique=True,
        nullable=True
    )
    #: The state of the invitation
    state = db.Column(
        PyIntEnum(InvitationState),
        nullable=False,
        default=InvitationState.pending
    )
    #: Whether registration moderation should be skipped
    skip_moderation = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: The email of the invited person
    email = db.Column(
        db.String,
        nullable=False
    )
    #: The first name of the invited person
    first_name = db.Column(
        db.String,
        nullable=False
    )
    #: The last name of the invited person
    last_name = db.Column(
        db.String,
        nullable=False
    )
    #: The affiliation of the invited person
    affiliation = db.Column(
        db.String,
        nullable=False
    )

    #: The associated registration
    registration = db.relationship(
        'Registration',
        lazy=True,
        backref=db.backref(
            'invitation',
            lazy=True,
            uselist=False
        )
    )

    # relationship backrefs:
    # - registration_form (RegistrationForm.invitations)

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

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

        Instead of the numeric ID it uses the UUID.
        """
        assert self.uuid is not None
        return dict(self.registration_form.locator, invitation=self.uuid)

    def __repr__(self):
        full_name = f'{self.first_name} {self.last_name}'
        return format_repr(self, 'id', 'registration_form_id', 'email', 'state', _text=full_name)
Example #46
0
class Blocking(db.Model):
    __tablename__ = 'blockings'
    __table_args__ = {'schema': 'roombooking'}

    id = db.Column(db.Integer, primary_key=True)
    created_by_id = db.Column(db.Integer,
                              db.ForeignKey('users.users.id'),
                              index=True,
                              nullable=False)
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    start_date = db.Column(db.Date, nullable=False, index=True)
    end_date = db.Column(db.Date, nullable=False, index=True)
    reason = db.Column(db.Text, nullable=False)

    _allowed = db.relationship('BlockingPrincipal',
                               backref='blocking',
                               cascade='all, delete-orphan',
                               collection_class=set)
    allowed = association_proxy(
        '_allowed',
        'principal',
        creator=lambda v: BlockingPrincipal(principal=v))
    blocked_rooms = db.relationship('BlockedRoom',
                                    backref='blocking',
                                    cascade='all, delete-orphan')
    #: The user who created this blocking.
    created_by_user = db.relationship('User',
                                      lazy=False,
                                      backref=db.backref('blockings',
                                                         lazy='dynamic'))

    @hybrid_method
    def is_active_at(self, d):
        return self.start_date <= d <= self.end_date

    @is_active_at.expression
    def is_active_at(self, d):
        return (self.start_date <= d) & (d <= self.end_date)

    def can_edit(self, user, allow_admin=True):
        if not user:
            return False
        return user == self.created_by_user or (allow_admin
                                                and rb_is_admin(user))

    def can_delete(self, user, allow_admin=True):
        if not user:
            return False
        return user == self.created_by_user or (allow_admin
                                                and rb_is_admin(user))

    def can_override(self,
                     user,
                     room=None,
                     explicit_only=False,
                     allow_admin=True):
        """Check if a user can override the blocking.

        The following persons are authorized to override a blocking:
        - the creator of the blocking
        - anyone on the blocking's ACL
        - unless explicit_only is set: rb admins and room managers (if a room is given)
        """
        if not user:
            return False
        if self.created_by_user == user:
            return True
        if not explicit_only:
            if allow_admin and rb_is_admin(user):
                return True
            if room and room.can_manage(user):
                return True
        return any(user in principal for principal in iter_acl(self.allowed))

    @property
    def external_details_url(self):
        return url_for('rb.blocking_link', blocking_id=self.id, _external=True)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'start_date',
                           'end_date',
                           _text=self.reason)
Example #47
0
class AbstractEmailTemplate(db.Model):
    """Represents an email template for abstracts notifications."""

    __tablename__ = 'email_templates'
    __table_args__ = {'schema': 'event_abstracts'}

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    title = db.Column(
        db.String,
        nullable=False
    )
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=False
    )
    #: The relative position of the template in the list of templates
    position = db.Column(
        db.Integer,
        nullable=False,
        default=_get_next_position
    )
    #: The address to use as Reply-To in the email
    reply_to_address = db.Column(
        db.String,
        nullable=False
    )
    #: The subject of the email
    subject = db.Column(
        db.String,
        nullable=False
    )
    #: The body of the template
    body = db.Column(
        db.Text,
        nullable=False,
        default=''
    )
    #: List of extra email addresses to be added as CC in the email
    extra_cc_emails = db.Column(
        ARRAY(db.String),
        nullable=False,
        default=[],
    )
    #: Whether to include the submitter's email address as To for emails
    include_submitter = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Whether to include authors' email addresses as To for emails
    include_authors = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Whether to include co-authors' email addresses as CC for emails
    include_coauthors = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: Whether to stop checking the rest of the conditions when a match is found
    stop_on_match = db.Column(
        db.Boolean,
        nullable=False,
        default=True
    )
    #: Conditions need to be met to send the email
    rules = db.Column(
        JSON,
        nullable=False
    )

    event_new = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'abstract_email_templates',
            lazy=True
        )
    )

    # relationship backrefs:
    # - logs (AbstractEmailLogEntry.email_template)

    @locator_property
    def locator(self):
        return dict(self.event_new.locator, email_tpl_id=self.id)

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

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

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

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

    judgment_comment = RenderModeMixin.create_hybrid_property(
        '_judgment_comment')

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

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

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

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

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

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

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

    @property
    def spotlight_file(self):
        return self.get_spotlight_file()

    @property
    def timeline(self):
        return self.get_timeline()

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

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

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

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

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

    def get_spotlight_file(self):
        pdf_files = [
            paper_file for paper_file in self.files
            if paper_file.content_type == 'application/pdf'
        ]
        return pdf_files[0] if len(pdf_files) == 1 else None