Ejemplo n.º 1
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(11, 2),  # max. 999999999.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
Ejemplo n.º 2
0
class PaperRevision(ProposalRevisionMixin, RenderModeMixin, db.Model):
    __tablename__ = 'revisions'
    __table_args__ = (db.Index(None, 'contribution_id', unique=True,
                               postgresql_where=db.text(f'state = {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().__init__(*args, **kwargs)

    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
        from indico.modules.events.papers.util import is_type_reviewing_possible

        cfp = self.paper.cfp
        reviewed_for = set()
        if include_reviewed:
            reviewed_for = {x.type for x in self.reviews if x.user == user and is_type_reviewing_possible(cfp, x.type)}
        if is_type_reviewing_possible(cfp, PaperReviewType.content) and user in self.paper.cfp.content_reviewers:
            reviewed_for.add(PaperReviewType.content)
        if is_type_reviewing_possible(cfp, PaperReviewType.layout) 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, str):
                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
Ejemplo n.º 3
0
class Session(DescriptionMixin, ColorMixin, ProtectionManagersMixin,
              LocationMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model):
    __tablename__ = 'sessions'
    __auto_table_args = (db.Index(None, 'friendly_id', 'event_id',
                                  unique=True), {
                                      'schema': 'events'
                                  })
    location_backref_name = 'sessions'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    default_colors = ColorTuple('#202020', '#e3f2d3')
    allow_relationship_preloading = True

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

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

    id = db.Column(db.Integer, primary_key=True)
    #: The human-friendly ID for the session
    friendly_id = db.Column(db.Integer,
                            nullable=False,
                            default=_get_next_friendly_id)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    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
Ejemplo n.º 4
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)

    def __repr__(self):
        return format_repr(self,
                           'id',
                           'start_date',
                           'end_date',
                           _text=self.reason)
Ejemplo n.º 5
0
class SurveyItem(DescriptionMixin, db.Model):
    __tablename__ = 'items'
    __table_args__ = (db.CheckConstraint(
        'type != {type} OR ('
        'title IS NOT NULL AND '
        'is_required IS NOT NULL AND '
        'field_type IS NOT NULL AND '
        'parent_id IS NOT NULL AND '
        'display_as_section IS NULL)'.format(type=SurveyItemType.question),
        'valid_question'),
                      db.CheckConstraint(
                          'type != {type} OR ('
                          'title IS NOT NULL AND '
                          'is_required IS NULL AND '
                          'field_type IS NULL AND '
                          "field_data::text = '{{}}' AND "
                          'parent_id IS NULL AND '
                          'display_as_section IS NOT NULL)'.format(
                              type=SurveyItemType.section), 'valid_section'),
                      db.CheckConstraint(
                          'type != {type} OR ('
                          'title IS NULL AND '
                          'is_required IS NULL AND '
                          'field_type IS NULL AND '
                          "field_data::text = '{{}}' AND "
                          'parent_id IS NOT NULL AND '
                          'display_as_section IS NULL)'.format(
                              type=SurveyItemType.text), 'valid_text'), {
                                  'schema': 'event_surveys'
                              })
    __mapper_args__ = {'polymorphic_on': 'type', 'polymorphic_identity': None}

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

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

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

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

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

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

        Subclasses must add their own data to the dict.
        """
        return {
            'type': self.type.name,
            'title': self.title,
            'description': self.description
        }
Ejemplo n.º 6
0
class Contribution(DescriptionMixin, ProtectionManagersMixin, LocationMixin,
                   AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin,
                   AuthorsSpeakersMixin, CustomFieldsMixin, db.Model):
    __tablename__ = 'contributions'
    __auto_table_args = (
        db.Index(None,
                 'friendly_id',
                 'event_id',
                 unique=True,
                 postgresql_where=db.text('NOT is_deleted')),
        db.Index(None, 'event_id',
                 'track_id'), db.Index(None, 'event_id', 'abstract_id'),
        db.Index(None,
                 'abstract_id',
                 unique=True,
                 postgresql_where=db.text('NOT is_deleted')),
        db.CheckConstraint(
            "session_block_id IS NULL OR session_id IS NOT NULL",
            'session_block_if_session'),
        db.CheckConstraint("date_trunc('minute', duration) = duration",
                           'duration_no_seconds'),
        db.ForeignKeyConstraint(
            ['session_block_id', 'session_id'],
            ['events.session_blocks.id', 'events.session_blocks.session_id']),
        {
            'schema': 'events'
        })
    location_backref_name = 'contributions'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    possible_render_modes = {RenderMode.html, RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    allow_relationship_preloading = True

    PRELOAD_EVENT_ATTACHED_ITEMS = True
    PRELOAD_EVENT_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id'

    @classmethod
    def allocate_friendly_ids(cls, event, n):
        """Allocate n Contribution friendly_ids.

        This is needed so that we can allocate all IDs in one go. Not doing
        so could result in DB deadlocks. All operations that create more than
        one contribution should use this method.

        :param event: the :class:`Event` in question
        :param n: the number of ids to pre-allocate
        """
        from indico.modules.events import Event
        fid = increment_and_get(Event._last_friendly_contribution_id,
                                Event.id == event.id, n)
        friendly_ids = g.setdefault('friendly_ids', {})
        friendly_ids.setdefault(cls, {})[event.id] = list(
            range(fid - n + 1, fid + 1))

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

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

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

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

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

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

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

    @declared_attr
    def _paper_revision_count(cls):
        query = (db.select([
            db.func.count(PaperRevision.id)
        ]).where(PaperRevision._contribution_id == cls.id).correlate_except(
            PaperRevision).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('note', None)
        kwargs.setdefault('timetable_entry', None)
        super().__init__(**kwargs)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @property
    def allowed_types_for_editable(self):
        from indico.modules.events.editing.settings import editable_type_settings
        if not self.event.has_feature('editing'):
            return []

        submitted_for = {editable.type.name for editable in self.editables}
        return [
            editable_type for editable_type in self.event.editable_types
            if editable_type not in submitted_for
            and editable_type_settings[EditableType[editable_type]].get(
                self.event, 'submission_enabled')
        ]

    @property
    def enabled_editables(self):
        """Return all submitted editables with enabled types."""
        from indico.modules.events.editing.settings import editing_settings
        if not self.event.has_feature('editing'):
            return []

        enabled_editable_types = editing_settings.get(self.event,
                                                      'editable_types')
        enabled_editables = [
            editable for editable in self.editables
            if editable.type.name in enabled_editable_types
        ]
        order = list(EditableType)
        return sorted(enabled_editables,
                      key=lambda editable: order.index(editable.type))

    @property
    def has_published_editables(self):
        return any(e.published_revision_id is not None
                   for e in self.enabled_editables)

    @property
    def slug(self):
        return slugify(self.friendly_id, self.title, maxlen=30)

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

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

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

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

    def is_user_associated(self, user, check_abstract=False):
        if user is None:
            return False
        if check_abstract and self.abstract and self.abstract.submitter == user:
            return True
        return any(pl.person.user == user for pl in self.person_links
                   if pl.person.user)

    def can_submit_proceedings(self, user):
        """Whether the user can submit editables/papers."""
        if user is None:
            return False
        # The submitter of the original abstract is always authorized
        if self.abstract and self.abstract.submitter == user:
            return True
        # Otherwise only users with submission rights are authorized
        return self.can_manage(user,
                               'submit',
                               allow_admin=False,
                               check_parent=False)

    def get_editable(self, editable_type):
        """Get the editable of the given type."""
        return next((e for e in self.editables if e.type == editable_type),
                    None)

    def log(self, *args, **kwargs):
        """Log with prefilled metadata for the contribution."""
        self.event.log(*args, meta={'contribution_id': self.id}, **kwargs)
Ejemplo n.º 7
0
class EventNoteRevision(db.Model):
    __tablename__ = 'note_revisions'
    __table_args__ = {'schema': 'events'}

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

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

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

    def __repr__(self):
        render_mode = self.render_mode.name if self.render_mode is not None else None
        source = text_to_repr(self.source, html=True)
        return '<EventNoteRevision({}, {}, {}, {}): "{}">'.format(self.id, self.note_id, render_mode, self.created_dt,
                                                                  source)
Ejemplo n.º 8
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)
Ejemplo n.º 9
0
# You should have received a copy of the GNU General Public License
# along with Indico; if not, see <http://www.gnu.org/licenses/>.

from __future__ import unicode_literals

from indico.core.db import db
from indico.util.string import format_repr, return_ascii


RoomEquipmentAssociation = db.Table(
    'room_equipment',
    db.metadata,
    db.Column(
        'equipment_id',
        db.Integer,
        db.ForeignKey('roombooking.equipment_types.id'),
        primary_key=True,
    ),
    db.Column(
        'room_id',
        db.Integer,
        db.ForeignKey('roombooking.rooms.id'),
        primary_key=True
    ),
    schema='roombooking'
)


equipment_features_table = db.Table(
    'equipment_features',
    db.metadata,
Ejemplo n.º 10
0
 def session_block_id(cls):
     if LinkType.session_block in cls.allowed_link_types:
         return db.Column(db.Integer,
                          db.ForeignKey('events.session_blocks.id'),
                          nullable=True,
                          index=True)
Ejemplo n.º 11
0
 def subcontribution_id(cls):
     if LinkType.subcontribution in cls.allowed_link_types:
         return db.Column(db.Integer,
                          db.ForeignKey('events.subcontributions.id'),
                          nullable=True,
                          index=True)
Ejemplo n.º 12
0
 def linked_event_id(cls):
     if LinkType.event in cls.allowed_link_types:
         return db.Column(db.Integer,
                          db.ForeignKey('events.events.id'),
                          nullable=True,
                          index=True)
Ejemplo n.º 13
0
 def event_id(cls):
     return db.Column(db.Integer,
                      db.ForeignKey('events.events.id'),
                      nullable=True,
                      index=True)
Ejemplo n.º 14
0
 def category_id(cls):
     if LinkType.category in cls.allowed_link_types:
         return db.Column(db.Integer,
                          db.ForeignKey('categories.categories.id'),
                          nullable=True,
                          index=True)
Ejemplo n.º 15
0
class ReservationOccurrence(db.Model, Serializer):
    __tablename__ = 'reservation_occurrences'
    __table_args__ = (db.CheckConstraint("rejection_reason != ''",
                                         'rejection_reason_not_empty'), {
                                             'schema': 'roombooking'
                                         })
    __api_public__ = (('start_dt', 'startDT'), ('end_dt', 'endDT'),
                      'is_cancelled', 'is_rejected')

    #: A relationship loading strategy that will avoid loading the
    #: users linked to a reservation.  You want to use this in pretty
    #: much all cases where you eager-load the `reservation` relationship.
    NO_RESERVATION_USER_STRATEGY = defaultload('reservation')
    NO_RESERVATION_USER_STRATEGY.lazyload('created_by_user')
    NO_RESERVATION_USER_STRATEGY.noload('booked_for_user')

    reservation_id = db.Column(db.Integer,
                               db.ForeignKey('roombooking.reservations.id'),
                               nullable=False,
                               primary_key=True)
    start_dt = db.Column(db.DateTime,
                         nullable=False,
                         primary_key=True,
                         index=True)
    end_dt = db.Column(db.DateTime, nullable=False, index=True)
    notification_sent = db.Column(db.Boolean, nullable=False, default=False)
    state = db.Column(PyIntEnum(ReservationOccurrenceState),
                      nullable=False,
                      default=ReservationOccurrenceState.valid)
    rejection_reason = db.Column(db.String, nullable=True)

    # relationship backrefs:
    # - reservation (Reservation.occurrences)

    @hybrid_property
    def date(self):
        return self.start_dt.date()

    @date.expression
    def date(self):
        return cast(self.start_dt, Date)

    @hybrid_property
    def is_valid(self):
        return self.state == ReservationOccurrenceState.valid

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

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

    @hybrid_property
    def is_within_cancel_grace_period(self):
        return self.start_dt >= datetime.now() - timedelta(minutes=10)

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'reservation_id', 'start_dt', 'end_dt',
                           'state')

    @classmethod
    def create_series_for_reservation(cls, reservation):
        for o in cls.iter_create_occurrences(reservation.start_dt,
                                             reservation.end_dt,
                                             reservation.repetition):
            o.reservation = reservation

    @classmethod
    def create_series(cls, start, end, repetition):
        return list(cls.iter_create_occurrences(start, end, repetition))

    @classmethod
    def iter_create_occurrences(cls, start, end, repetition):
        for start in cls.iter_start_time(start, end, repetition):
            end = datetime.combine(start.date(), end.time())
            yield ReservationOccurrence(start_dt=start, end_dt=end)

    @staticmethod
    def iter_start_time(start, end, repetition):
        from indico.modules.rb.models.reservations import RepeatFrequency

        repeat_frequency, repeat_interval = repetition

        if repeat_frequency == RepeatFrequency.NEVER:
            return [start]

        elif repeat_frequency == RepeatFrequency.DAY:
            if repeat_interval != 1:
                raise IndicoError(u'Unsupported interval')
            return rrule.rrule(rrule.DAILY, dtstart=start, until=end)

        elif repeat_frequency == RepeatFrequency.WEEK:
            if repeat_interval <= 0:
                raise IndicoError(u'Unsupported interval')
            return rrule.rrule(rrule.WEEKLY,
                               dtstart=start,
                               until=end,
                               interval=repeat_interval)

        elif repeat_frequency == RepeatFrequency.MONTH:
            if repeat_interval == 0:
                raise IndicoError(u'Unsupported interval')
            position = int(ceil(start.day / 7.0))
            if position == 5:
                # The fifth weekday of the month will always be the last one
                position = -1
            return rrule.rrule(rrule.MONTHLY,
                               dtstart=start,
                               until=end,
                               byweekday=start.weekday(),
                               bysetpos=position,
                               interval=repeat_interval)

        raise IndicoError(u'Unexpected frequency {}'.format(repeat_frequency))

    @staticmethod
    def filter_overlap(occurrences):
        return or_(
            db_dates_overlap(ReservationOccurrence, 'start_dt', occ.start_dt,
                             'end_dt', occ.end_dt) for occ in occurrences)

    @classmethod
    def find_overlapping_with(cls,
                              room,
                              occurrences,
                              skip_reservation_id=None):
        from indico.modules.rb.models.reservations import Reservation

        return (ReservationOccurrence.find(
            Reservation.room == room,
            Reservation.id != skip_reservation_id,
            ReservationOccurrence.is_valid,
            ReservationOccurrence.filter_overlap(occurrences),
            _eager=ReservationOccurrence.reservation,
            _join=ReservationOccurrence.reservation).options(
                cls.NO_RESERVATION_USER_STRATEGY))

    def can_reject(self, user, allow_admin=True):
        if not self.is_valid:
            return False
        return self.reservation.can_reject(user, allow_admin=allow_admin)

    def can_cancel(self, user, allow_admin=True):
        if user is None:
            return False
        if not self.is_valid or self.end_dt < datetime.now():
            return False
        booking = self.reservation
        booked_or_owned_by_user = booking.is_owned_by(
            user) or booking.is_booked_for(user)
        if booking.is_rejected or booking.is_cancelled or booking.is_archived:
            return False
        if booked_or_owned_by_user and self.is_within_cancel_grace_period:
            return True
        return allow_admin and rb_is_admin(user)

    @proxy_to_reservation_if_last_valid_occurrence
    def cancel(self, user, reason=None, silent=False):
        self.state = ReservationOccurrenceState.cancelled
        self.rejection_reason = reason or None
        signals.rb.booking_occurrence_state_changed.send(self)
        if not silent:
            log = [
                u'Day cancelled: {}'.format(
                    format_date(self.date).decode('utf-8'))
            ]
            if reason:
                log.append(u'Reason: {}'.format(reason))
            self.reservation.add_edit_log(
                ReservationEditLog(user_name=user.full_name, info=log))
            from indico.modules.rb.notifications.reservation_occurrences import notify_cancellation
            notify_cancellation(self)

    @proxy_to_reservation_if_last_valid_occurrence
    def reject(self, user, reason, silent=False):
        self.state = ReservationOccurrenceState.rejected
        self.rejection_reason = reason or None
        signals.rb.booking_occurrence_state_changed.send(self)
        if not silent:
            log = [
                u'Day rejected: {}'.format(
                    format_date(self.date).decode('utf-8')),
                u'Reason: {}'.format(reason)
            ]
            self.reservation.add_edit_log(
                ReservationEditLog(user_name=user.full_name, info=log))
            from indico.modules.rb.notifications.reservation_occurrences import notify_rejection
            notify_rejection(self)

    def get_overlap(self, occurrence, skip_self=False):
        if self.reservation and occurrence.reservation and self.reservation.room_id != occurrence.reservation.room_id:
            raise ValueError(
                'ReservationOccurrence objects of different rooms')
        if skip_self and self.reservation and occurrence.reservation and self.reservation == occurrence.reservation:
            return None, None
        return date_time.get_overlap((self.start_dt, self.end_dt),
                                     (occurrence.start_dt, occurrence.end_dt))

    def overlaps(self, occurrence, skip_self=False):
        if self.reservation and occurrence.reservation and self.reservation.room_id != occurrence.reservation.room_id:
            raise ValueError(
                'ReservationOccurrence objects of different rooms')
        if skip_self and self.reservation and occurrence.reservation and self.reservation == occurrence.reservation:
            return False
        return date_time.overlaps((self.start_dt, self.end_dt),
                                  (occurrence.start_dt, occurrence.end_dt))
Ejemplo n.º 16
0
class SessionBlock(LocationMixin, db.Model):
    __tablename__ = 'session_blocks'
    __auto_table_args = (db.UniqueConstraint('id', 'session_id'),  # useless but needed for the compound fkey
                         {'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

    def __repr__(self):
        return format_repr(self, 'id', _text=self.title or None)
Ejemplo n.º 17
0
class BlockedRoom(db.Model):
    __tablename__ = 'blocked_rooms'
    __table_args__ = {'schema': 'roombooking'}

    State = BlockedRoomState  # make it available here for convenience

    id = db.Column(db.Integer, primary_key=True)
    state = db.Column(PyIntEnum(BlockedRoomState),
                      nullable=False,
                      default=BlockedRoomState.pending)
    rejected_by = db.Column(db.String)
    rejection_reason = db.Column(db.String)
    blocking_id = db.Column(db.Integer,
                            db.ForeignKey('roombooking.blockings.id'),
                            nullable=False)
    room_id = db.Column(db.Integer,
                        db.ForeignKey('roombooking.rooms.id'),
                        nullable=False,
                        index=True)

    # relationship backrefs:
    # - blocking (Blocking.blocked_rooms)
    # - room (Room.blocked_rooms)

    @property
    def state_name(self):
        return BlockedRoomState(self.state).title

    @classmethod
    def find_with_filters(cls, filters):
        q = cls.find(_eager=BlockedRoom.blocking, _join=BlockedRoom.blocking)
        if filters.get('room_ids'):
            q = q.filter(BlockedRoom.room_id.in_(filters['room_ids']))
        if filters.get('start_date') and filters.get('end_date'):
            q = q.filter(Blocking.start_date <= filters['end_date'],
                         Blocking.end_date >= filters['start_date'])
        if 'state' in filters:
            q = q.filter(BlockedRoom.state == filters['state'])
        return q

    def reject(self, user=None, reason=None):
        """Reject the room blocking."""
        self.state = BlockedRoomState.rejected
        if reason:
            self.rejection_reason = reason
        if user:
            self.rejected_by = user.full_name
        notify_request_response(self)

    def approve(self, notify_blocker=True):
        """Approve the room blocking, rejecting all colliding reservations/occurrences."""
        self.state = BlockedRoomState.accepted

        # Get colliding reservations
        start_dt = datetime.combine(self.blocking.start_date, time())
        end_dt = datetime.combine(self.blocking.end_date, time(23, 59, 59))

        reservation_criteria = [
            Reservation.room_id == self.room_id, ~Reservation.is_rejected,
            ~Reservation.is_cancelled
        ]

        # Whole reservations to reject
        reservations = Reservation.find_all(Reservation.start_dt >= start_dt,
                                            Reservation.end_dt <= end_dt,
                                            *reservation_criteria)

        # Single occurrences to reject
        occurrences = ReservationOccurrence.find_all(
            ReservationOccurrence.start_dt >= start_dt,
            ReservationOccurrence.end_dt <= end_dt,
            ReservationOccurrence.is_valid,
            ~ReservationOccurrence.reservation_id.in_(
                map(attrgetter('id'), reservations)) if reservations else True,
            *reservation_criteria,
            _join=Reservation)

        reason = 'Conflict with blocking {}: {}'.format(
            self.blocking.id, self.blocking.reason)

        for reservation in reservations:
            if self.blocking.can_be_overridden(reservation.created_by_user,
                                               reservation.room):
                continue
            reservation.reject(self.blocking.created_by_user, reason)

        for occurrence in occurrences:
            reservation = occurrence.reservation
            if self.blocking.can_be_overridden(reservation.created_by_user,
                                               reservation.room):
                continue
            occurrence.reject(self.blocking.created_by_user, reason)

        if notify_blocker:
            # We only need to notify the blocking creator if the blocked room wasn't approved yet.
            # This is the case if it's a new blocking for a room managed by the creator
            notify_request_response(self)

    @return_ascii
    def __repr__(self):
        return '<BlockedRoom({0}, {1}, {2})>'.format(self.blocking_id,
                                                     self.room_id,
                                                     self.state_name)
Ejemplo n.º 18
0
class VCRoomEventAssociation(db.Model):
    __tablename__ = 'vc_room_events'
    __table_args__ = tuple(_make_checks()) + (db.Index(
        None, 'data', postgresql_using='gin'), {
            'schema': 'events'
        })

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        :param user: the user performing the deletion
        :param delete_all: if True, the room is detached from all
                           events and deleted.
        """
        vc_room = self.vc_room
        if delete_all:
            for assoc in vc_room.events[:]:
                Logger.get('modules.vc').info(
                    "Detaching VC room {} from event {} ({})".format(
                        vc_room, assoc.event, assoc.link_object))
                vc_room.events.remove(assoc)
        else:
            Logger.get('modules.vc').info(
                "Detaching VC room {} from event {} ({})".format(
                    vc_room, self.event, self.link_object))
            vc_room.events.remove(self)
        db.session.flush()
        if not vc_room.events:
            Logger.get('modules.vc').info(
                "Deleting VC room {}".format(vc_room))
            if vc_room.status != VCRoomStatus.deleted:
                vc_room.plugin.delete_room(vc_room, self.event)
                notify_deleted(vc_room.plugin, vc_room, self, self.event, user)
            db.session.delete(vc_room)
Ejemplo n.º 19
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(
                    "type != {} OR parent_id IS NULL".format(
                        TimetableEntryType.SESSION_BLOCK), '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_new = 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('Unexpected object: {}'.format(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).as_scalar(),
                TimetableEntryType.CONTRIBUTION.value:
                db.select([Contribution.duration]).where(
                    Contribution.id == cls.contribution_id).correlate_except(
                        Contribution).as_scalar(),
                TimetableEntryType.BREAK.value:
                db.select([Break.duration]).where(Break.id == cls.break_id).
                correlate_except(Break).as_scalar(),
            },
            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_top_level_entries, get_nested_entries
        tzinfo = self.event_new.tzinfo
        day = self.start_dt.astimezone(tzinfo).date()
        siblings = (get_nested_entries(self.event_new)[self.parent_id] if
                    self.parent_id else get_top_level_entries(self.event_new))
        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_new.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_new).filter(*criteria)

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

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

    def can_view(self, user):
        """Checks 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
                if not x.is_inheriting)

    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_new.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_new.start_dt:
                self.event_new.start_dt = self.start_dt
            if by_end and self.end_dt > self.event_new.end_dt:
                self.event_new.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)
Ejemplo n.º 20
0
class Agreement(db.Model):
    """Agreements between a person and Indico"""
    __tablename__ = 'agreements'
    __table_args__ = (db.UniqueConstraint('event_id', 'type', 'identifier'),
                      {'schema': 'events'})

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

    #: The user this agreement is linked to
    user = db.relationship(
        'User',
        lazy=False,
        backref=db.backref(
            'agreements',
            lazy='dynamic'
        )
    )
    #: The Event this agreement is associated with
    event_new = 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 event(self):
        from MaKaC.conference import ConferenceHolder
        return ConferenceHolder().getById(str(self.event_id))

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

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

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

    @staticmethod
    def create_from_data(event, type_, person):
        agreement = Agreement(event_new=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 IndicoError(_('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):
        return self.definition.is_agreement_orphan(self.event_new, self)
Ejemplo n.º 21
0
class EventNote(LinkMixin, db.Model):
    __tablename__ = 'notes'
    allowed_link_types = LinkMixin.allowed_link_types - {LinkType.category, LinkType.session_block}
    unique_links = True
    events_backref_name = 'all_notes'
    link_backref_name = 'note'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        To be used in a SQLAlchemy `filter` call.

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

    def __repr__(self):
        return '<EventNote({}, current_revision={}{}, {})>'.format(
            self.id,
            self.current_revision_id,
            ', is_deleted=True' if self.is_deleted else '',
            self.link_repr
        )
Ejemplo n.º 22
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_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'subcontribution_id'
    possible_render_modes = {RenderMode.html, RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: The human-friendly ID for the sub-contribution
    friendly_id = db.Column(
        db.Integer,
        nullable=False,
        default=_get_next_friendly_id
    )
    contribution_id = db.Column(
        db.Integer,
        db.ForeignKey('events.contributions.id'),
        index=True,
        nullable=False
    )
    position = db.Column(
        db.Integer,
        nullable=False,
        default=_get_next_position
    )
    title = db.Column(
        db.String,
        nullable=False
    )
    code = db.Column(
        db.String,
        nullable=False,
        default=''
    )
    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().__init__(**kwargs)

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

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

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

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

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

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

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

    @property
    def slug(self):
        return slugify('sc', self.contribution.friendly_id, self.friendly_id, self.title, maxlen=30)

    @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, include_groups=True):
        return self.contribution.get_manager_list(recursive=recursive, include_groups=include_groups)

    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, permission=None, **kwargs):
        return self.contribution.can_manage(user, permission=permission, **kwargs)
Ejemplo n.º 23
0
class StaticListLink(db.Model):
    """Display configuration data used in static links to listing pages.

    This allows users to share links to listing pages in events
    while preserving e.g. column/filter configurations.
    """

    __tablename__ = 'static_list_links'
    __table_args__ = {'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)
    type = db.Column(db.String, nullable=False)
    uuid = db.Column(pg_UUID,
                     index=True,
                     unique=True,
                     nullable=False,
                     default=lambda: unicode(uuid4()))
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    last_used_dt = db.Column(UTCDateTime, nullable=True)
    data = db.Column(JSONB, nullable=False)

    event_new = db.relationship('Event',
                                lazy=True,
                                backref=db.backref(
                                    'static_list_links',
                                    cascade='all, delete-orphan',
                                    lazy='dynamic'))

    @classmethod
    def load(cls, event, type_, uuid):
        """Load the data associated with a link

        :param event: the `Event` the link belongs to
        :param type_: the type of the link
        :param uuid: the UUID of the link
        :return: the link data or ``None`` if the link does not exist
        """
        try:
            UUID(uuid)
        except ValueError:
            return None
        static_list_link = event.static_list_links.filter_by(
            type=type_, uuid=uuid).first()
        if static_list_link is None:
            return None
        static_list_link.last_used_dt = now_utc()
        return static_list_link.data

    @classmethod
    def create(cls, event, type_, data):
        """Create a new static list link.

        If one exists with the same data, that link is used instead of
        creating a new one.

        :param event: the `Event` for which to create the link
        :param type_: the type of the link
        :param data: the data to associate with the link
        :return: the newly created `StaticListLink`
        """
        static_list_link = event.static_list_links.filter_by(
            type=type_, data=data).first()
        if static_list_link is None:
            static_list_link = cls(event_new=event, type=type_, data=data)
        else:
            # bump timestamp in case we start expiring old links
            # in the future
            if static_list_link.last_used_dt is not None:
                static_list_link.last_used_dt = now_utc()
            else:
                static_list_link.created_dt = now_utc()
        db.session.flush()
        return static_list_link

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'uuid')
Ejemplo n.º 24
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_see_editor_names(self, user, actor=None):
        """Whether the user can see the names of editing team members.

        This is always true if team anonymity is not enabled; otherwise only
        users who are member of the editing team will see names.

        If an `actor` is set, the check applies to whether the name of this
        particular user can be seen.
        """
        from indico.modules.events.editing.settings import editable_type_settings

        return (not editable_type_settings[self.type].get(
            self.event, 'anonymous_team')
                or (actor and not self.can_see_editor_names(actor))
                or 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)
Ejemplo n.º 25
0
# This file is part of Indico.
# Copyright (C) 2002 - 2020 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

from __future__ import unicode_literals

from indico.core.db import db

favorite_user_table = db.Table('favorite_users',
                               db.metadata,
                               db.Column('user_id',
                                         db.Integer,
                                         db.ForeignKey('users.users.id'),
                                         primary_key=True,
                                         nullable=False,
                                         index=True),
                               db.Column('target_id',
                                         db.Integer,
                                         db.ForeignKey('users.users.id'),
                                         primary_key=True,
                                         nullable=False,
                                         index=True),
                               schema='users')

favorite_category_table = db.Table(
    'favorite_categories',
    db.metadata,
    db.Column('user_id',
Ejemplo n.º 26
0
class Category(SearchableTitleMixin, DescriptionMixin, ProtectionManagersMixin,
               AttachedItemsMixin, db.Model):
    """An Indico category."""

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

    @strict_classproperty
    @classmethod
    def __auto_table_args(cls):
        return (
            db.CheckConstraint(
                "(icon IS NULL) = (icon_metadata::text = 'null')",
                'valid_icon'),
            db.CheckConstraint(
                "(logo IS NULL) = (logo_metadata::text = 'null')",
                'valid_logo'),
            db.CheckConstraint("(parent_id IS NULL) = (id = 0)",
                               'valid_parent'),
            db.CheckConstraint("(id != 0) OR NOT is_deleted",
                               'root_not_deleted'),
            db.CheckConstraint(
                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'])
Ejemplo n.º 27
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(JSON, 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)
        registration.transaction = new_transaction
        double_payment = False
        try:
            next_status = TransactionStatusTransition.next(
                previous_transaction, action, provider)
        except InvalidTransactionStatus as e:
            Logger.get('payment').exception("{}\nData received: {}".format(
                e, data))
            return None, None
        except InvalidManualTransactionAction as e:
            Logger.get('payment').exception(
                "Invalid manual action code '{}' on initial status\n"
                "Data received: {}".format(e, data))
            return None, None
        except InvalidTransactionAction as e:
            Logger.get('payment').exception(
                "Invalid action code '{}' on initial status\n"
                "Data received: {}".format(e, data))
            return None, None
        except IgnoredTransactionAction as e:
            Logger.get('payment').warning("{}\nData received: {}".format(
                e, data))
            return None, None
        except DoublePaymentTransaction:
            next_status = TransactionStatus.successful
            double_payment = True
            Logger.get('payment').warning(
                "Received successful payment for an already paid registration")
        new_transaction.status = next_status
        return new_transaction, double_payment
Ejemplo n.º 28
0
class EditingRevisionComment(RenderModeMixin, db.Model):
    __tablename__ = 'comments'
    __table_args__ = (db.CheckConstraint('(user_id IS NULL) = system',
                                         name='system_comment_no_user'), {
                                             'schema': 'event_editing'
                                         })

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(db.Integer, primary_key=True)
    revision_id = db.Column(db.ForeignKey('event_editing.revisions.id',
                                          ondelete='CASCADE'),
                            index=True,
                            nullable=False)
    user_id = db.Column(db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=True)
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    modified_dt = db.Column(
        UTCDateTime,
        nullable=True,
    )
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: Whether the comment is only visible to editors
    internal = db.Column(db.Boolean, nullable=False, default=False)
    #: Whether the comment is system-generated and cannot be deleted/modified.
    system = db.Column(db.Boolean, nullable=False, default=False)
    _text = db.Column('text', db.Text, nullable=False, default='')
    text = RenderModeMixin.create_hybrid_property('_text')

    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('editing_comments',
                                              lazy='dynamic'))
    revision = db.relationship(
        'EditingRevision',
        lazy=True,
        backref=db.backref(
            'comments',
            primaryjoin=(
                '(EditingRevisionComment.revision_id == EditingRevision.id) & '
                '~EditingRevisionComment.is_deleted'),
            order_by=created_dt,
            cascade='all, delete-orphan',
            passive_deletes=True,
            lazy=True,
        ))

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'revision_id',
                           'user_id',
                           internal=False,
                           _text=text_to_repr(self.text))

    @locator_property
    def locator(self):
        return dict(self.revision.locator, comment_id=self.id)

    def can_modify(self, user):
        contribution = self.revision.editable.contribution
        authorized_submitter = contribution.is_user_associated(
            user, check_abstract=True)
        authorized_editor = contribution.event.can_manage(
            user, permission='paper_editing')

        if self.user != user:
            return False
        elif self.system:
            return False
        elif self.internal and not authorized_editor:
            return False
        return authorized_editor or authorized_submitter
Ejemplo n.º 29
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
    )
    #: a unique secret used to generate signed URLs
    signing_secret = db.Column(
        UUID,
        nullable=False,
        default=lambda: unicode(uuid4())
    )

    _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_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)
    # - created_events (Event.creator)
    # - editing_comments (EditingRevisionComment.user)
    # - editing_revisions (EditingRevision.submitter)
    # - editor_for_editables (Editable.editor)
    # - editor_for_revisions (EditingRevision.editor)
    # - 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)
    # - favorite_rooms (Room.favorite_of)
    # - 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_room_acls (RoomPrincipal.user)
    # - in_session_acls (SessionPrincipal.user)
    # - in_settings_acls (SettingPrincipal.user)
    # - in_track_acls (TrackPrincipal.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 identifier(self):
        return 'User:{}'.format(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_bg_color(self):
        from indico.modules.users.util import get_color_for_username
        return get_color_for_username(self.full_name)

    @property
    def avatar_css(self):
        return 'background-color: {};'.format(self.avatar_bg_color)

    @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

    @property
    def can_get_all_multipass_groups(self):
        """Check whether it is possible to get all multipass groups the user is in."""
        return all(multipass.identity_providers[x.provider].supports_get_identity_groups
                   for x in self.identities
                   if x.provider != 'indico' and x.provider in multipass.identity_providers)

    def iter_all_multipass_groups(self):
        """Iterate over all multipass groups the user is in"""
        return itertools.chain.from_iterable(multipass.identity_providers[x.provider].get_identity_groups(x.identifier)
                                             for x in self.identities
                                             if x.provider != 'indico' and x.provider in multipass.identity_providers)

    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 reset_signing_secret(self):
        self.signing_secret = unicode(uuid4())

    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 and sync_provider.supports_refresh:
            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
Ejemplo n.º 30
0
class EventNote(LinkMixin, db.Model):
    __tablename__ = 'notes'
    allowed_link_types = LinkMixin.allowed_link_types - {LinkType.category}
    unique_links = True
    events_backref_name = 'notes'

    @declared_attr
    def __table_args__(cls):
        return auto_table_args(cls, schema='events')

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

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

    @property
    def locator(self):
        return self.linked_object.getLocator()

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

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

        :param linked_object: An event, session, contribution or
                              subcontribution.
        :param preload_event: If all notes for the same event should
                              be pre-loaded and cached in the app
                              context.
        """
        event = linked_object.getConference()
        try:
            return g.event_notes[event].get(linked_object)
        except (AttributeError, KeyError):
            if not preload_event:
                return cls.find_first(linked_object=linked_object, is_deleted=False)
            if 'event_notes' not in g:
                g.event_notes = {}
            g.event_notes[event] = {n.linked_object: n
                                    for n in EventNote.find(event_id=int(event.id), is_deleted=False)}
            return g.event_notes[event].get(linked_object)

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

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

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

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

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

    @return_ascii
    def __repr__(self):
        return '<EventNote({}, current_revision={}{}, {})>'.format(
            self.id,
            self.current_revision_id,
            ', is_deleted=True' if self.is_deleted else '',
            self.link_repr
        )