Beispiel #1
0
class UserEmail(db.Model):
    __tablename__ = 'emails'
    __table_args__ = (
        db.CheckConstraint('email = lower(email)', 'lowercase_email'),
        db.Index(None,
                 'email',
                 unique=True,
                 postgresql_where=db.text('NOT is_user_deleted')),
        db.Index(
            None,
            'user_id',
            unique=True,
            postgresql_where=db.text('is_primary AND NOT is_user_deleted')), {
                'schema': 'users'
            })

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

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

    def __repr__(self):
        return f'<UserEmail({self.id}, {self.email}, {self.is_primary})>'
Beispiel #2
0
def _make_uniques(allowed_link_types, extra_criteria=None):
    for link_type in allowed_link_types:
        where = ['link_type = {}'.format(link_type.value)]
        if extra_criteria is not None:
            where += list(extra_criteria)
        yield db.Index(None, *_columns_for_types[link_type], unique=True,
                       postgresql_where=db.text(' AND '.join(where)))
Beispiel #3
0
class Location(db.Model):
    __tablename__ = 'locations'
    __table_args__ = (db.Index(None,
                               'name',
                               unique=True,
                               postgresql_where=db.text('NOT is_deleted')), {
                                   'schema': 'roombooking'
                               })

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

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

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

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

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

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

    def __repr__(self):
        return format_repr(self, 'id', 'name', is_deleted=False)
Beispiel #4
0
 def set_manager_emails(self):
     db.session.execute(db.text("""
         UPDATE event_registration.forms rf SET manager_notification_recipients = (
             SELECT array_agg(ue.email)
             FROM events.principals p
             JOIN users.emails ue ON (ue.user_id = p.user_id AND NOT ue.is_user_deleted AND ue.is_primary)
             WHERE p.event_id = rf.event_id AND p.full_access AND p.type = 1
         )
         WHERE manager_notification_recipients = '{}' AND manager_notifications_enabled AND title = :title;
     """).bindparams(title=PARTICIPATION_FORM_TITLE))
     db.session.commit()
Beispiel #5
0
 def set_manager_emails(self):
     db.session.execute(
         db.text("""
         UPDATE event_registration.forms rf SET manager_notification_recipients = (
             SELECT array_agg(ue.email)
             FROM events.principals p
             JOIN users.emails ue ON (ue.user_id = p.user_id AND NOT ue.is_user_deleted AND ue.is_primary)
             WHERE p.event_id = rf.event_id AND p.full_access AND p.type = 1
         )
         WHERE manager_notification_recipients = '{}' AND manager_notifications_enabled AND title = :title;
     """).bindparams(title=PARTICIPATION_FORM_TITLE))
     db.session.commit()
Beispiel #6
0
 def __table_args__(cls):
     return (db.Index('ix_uq_applications_name_lower',
                      db.func.lower(cls.name),
                      unique=True),
             db.Index(
                 None,
                 cls.system_app_type,
                 unique=True,
                 postgresql_where=db.text(
                     f'system_app_type != {SystemAppType.none.value}')), {
                         'schema': 'oauth'
                     })
Beispiel #7
0
 def migrate(self):
     db.session.commit(
     )  # make sure there's no transaction open or the DISABLE TRIGGER may deadlock
     tables = ('timetable_entries', 'session_blocks', 'contributions',
               'breaks')
     for table in tables:
         db.engine.execute(
             db.text(
                 'ALTER TABLE events.{} DISABLE TRIGGER consistent_timetable'
                 .format(table)))
     try:
         self.migrate_event_data()
         db.session.commit()
     except:
         db.session.rollback()
         raise
     finally:
         for table in tables:
             db.engine.execute(
                 db.text(
                     'ALTER TABLE events.{} ENABLE TRIGGER consistent_timetable'
                     .format(table)))
     db.session.commit()
Beispiel #8
0
class MapArea(db.Model):
    __tablename__ = 'map_areas'
    __table_args__ = (db.Index(None,
                               'is_default',
                               unique=True,
                               postgresql_where=db.text('is_default')), {
                                   'schema': 'roombooking'
                               })

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False)
    is_default = db.Column(db.Boolean, nullable=False, default=False)
    top_left_latitude = db.Column(db.Float, nullable=False)
    top_left_longitude = db.Column(db.Float, nullable=False)
    bottom_right_latitude = db.Column(db.Float, nullable=False)
    bottom_right_longitude = db.Column(db.Float, nullable=False)

    def __repr__(self):
        return format_repr(self, 'id', 'name', is_default=False)
Beispiel #9
0
class RegistrationForm(db.Model):
    """A registration form for an event."""

    __tablename__ = 'forms'
    principal_type = PrincipalType.registration_form
    principal_order = 2

    __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='')
    #: If the completed registration email should include the event's iCalendar file.
    attach_ical = db.Column(db.Boolean, nullable=False, default=False)
    #: 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))

    # relationship backrefs:
    # - in_attachment_acls (AttachmentPrincipal.registration_form)
    # - in_attachment_folder_acls (AttachmentFolderPrincipal.registration_form)
    # - in_contribution_acls (ContributionPrincipal.registration_form)
    # - in_event_acls (EventPrincipal.registration_form)
    # - in_session_acls (SessionPrincipal.registration_form)

    def __contains__(self, user):
        if user is None:
            return False
        return (Registration.query.with_parent(self).join(
            Registration.registration_form).filter(
                Registration.user == user,
                Registration.state.in_(
                    [RegistrationState.unpaid,
                     RegistrationState.complete]), ~Registration.is_deleted,
                ~RegistrationForm.is_deleted).has_rows())

    @property
    def name(self):
        # needed when sorting acl entries by name
        return self.title

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

    @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

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

    def is_modification_allowed(self, registration):
        """Check 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_approved:
            return registration.state == RegistrationState.pending
        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):
        """Retrieve 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_deleted).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_deleted).first()
        if email:
            return Registration.query.with_parent(self).filter_by(
                email=email).filter(~Registration.is_deleted).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):
        """Return 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
Beispiel #10
0
class EventReminder(db.Model):
    """Email reminders for events"""
    __tablename__ = 'reminders'
    __table_args__ = (db.Index(None,
                               'scheduled_dt',
                               postgresql_where=db.text('not is_sent')), {
                                   'schema': 'events'
                               })

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

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

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

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

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

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

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

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

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

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'event_id',
                           'scheduled_dt',
                           is_sent=False)
Beispiel #11
0
class MenuEntry(MenuEntryMixin, db.Model):
    __tablename__ = 'menu_entries'
    __table_args__ = (
        db.CheckConstraint(
            '(type IN ({type.internal_link.value}, {type.plugin_link.value}) AND name IS NOT NULL) OR '
            '(type NOT IN ({type.internal_link.value}, {type.plugin_link.value}) and name IS NULL)'
            .format(type=MenuEntryType), 'valid_name'),
        db.CheckConstraint(
            '(type = {type.user_link.value}) = (link_url IS NOT NULL)'.format(
                type=MenuEntryType), 'valid_link_url'),
        db.CheckConstraint(
            '(type = {type.page.value} AND page_id IS NOT NULL) OR'
            ' (type != {type.page.value} AND page_id IS NULL)'.format(
                type=MenuEntryType), 'valid_page_id'),
        db.CheckConstraint(
            '(type = {type.plugin_link.value} AND plugin IS NOT NULL) OR'
            ' (type != {type.plugin_link.value} AND plugin IS NULL)'.format(
                type=MenuEntryType), 'valid_plugin'),
        db.CheckConstraint(
            '(type = {type.separator.value} AND title IS NULL) OR'
            ' (type IN ({type.user_link.value}, {type.page.value}) AND title IS NOT NULL) OR'
            ' (type NOT IN ({type.separator.value}, {type.user_link.value}, {type.page.value}))'
            .format(type=MenuEntryType), 'valid_title'),
        db.CheckConstraint("title != ''", 'title_not_empty'),
        db.Index(
            None,
            'event_id',
            'name',
            unique=True,
            postgresql_where=db.text(
                '(type = {type.internal_link.value} OR type = {type.plugin_link.value})'
                .format(type=MenuEntryType))), {
                    'schema': 'events'
                })

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        This includes both explicit recipients and, if enabled,
        participants/speakers of the event.
        """
        recipients = set(self.recipients)
        if self.send_to_participants:
            recipients.update(reg.email for reg in Registration.get_all_for_event(self.event))

        if self.send_to_speakers:
            recipients.update(person_link.email for person_link in self.event.person_links)

            # contribution/sub-contribution speakers are present only in meetings and conferences
            if self.event.type != EventType.lecture:
                contrib_speakers = (
                    ContributionPersonLink.query
                    .filter(
                        ContributionPersonLink.is_speaker,
                        ContributionPersonLink.contribution.has(is_deleted=False, event=self.event)
                    )
                    .all()
                )

                subcontrib_speakers = (
                    SubContributionPersonLink.query
                    .filter(
                        SubContributionPersonLink.is_speaker,
                        SubContributionPersonLink.subcontribution.has(
                            db.and_(
                                ~SubContribution.is_deleted,
                                SubContribution.contribution.has(is_deleted=False, event=self.event)
                            )
                        )
                    )
                    .all()
                )

                recipients.update(speaker.email for speaker in contrib_speakers)
                recipients.update(speaker.email for speaker in subcontrib_speakers)

        recipients.discard('')  # just in case there was an empty email address somewhere
        return recipients

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

    @is_relative.expression
    def is_relative(self):
        return self.event_start_delta.isnot(None)

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

    def _make_email(self, recipient, template, attachments):
        email_params = {
            'to_list': recipient,
            'from_address': self.reply_to_address,
            'template': template,
            'attachments': attachments
        }
        extra_params = signals.event.reminder.before_reminder_make_email.send(self, **email_params)
        for param in values_from_signal(extra_params, as_list=True):
            email_params.update(param)
        return make_email(**email_params)

    def send(self):
        """Send the reminder to its recipients."""
        self.is_sent = True
        recipients = self.all_recipients
        if not recipients:
            logger.info('Notification %s has no recipients; not sending anything', self)
            return
        email_tpl = make_reminder_email(self.event, self.include_summary, self.include_description, self.message)
        attachments = []
        if self.attach_ical:
            event_ical = event_to_ical(self.event, skip_access_check=True, method='REQUEST',
                                       organizer=(core_settings.get('site_title'), config.NO_REPLY_EMAIL))
            attachments.append(MIMECalendar('event.ics', event_ical))

        for recipient in recipients:
            email = self._make_email(recipient, email_tpl, attachments)
            send_email(email, self.event, 'Reminder', self.creator, log_metadata={'reminder_id': self.id})

    def __repr__(self):
        return format_repr(self, 'id', 'event_id', 'scheduled_dt', is_sent=False)
Beispiel #13
0
class Registration(db.Model):
    """Somebody's registration for an event through a registration form"""
    __tablename__ = 'registrations'
    __table_args__ = (db.CheckConstraint('email = lower(email)', 'lowercase_email'),
                      db.Index(None, 'friendly_id', 'event_id', unique=True),
                      db.Index(None, 'registration_form_id', 'user_id', unique=True,
                               postgresql_where=db.text('NOT is_deleted AND (state NOT IN (3, 4))')),
                      db.Index(None, 'registration_form_id', 'email', unique=True,
                               postgresql_where=db.text('NOT is_deleted AND (state NOT IN (3, 4))')),
                      db.ForeignKeyConstraint(['event_id', 'registration_form_id'],
                                              ['event_registration.forms.event_id', 'event_registration.forms.id']),
                      {'schema': 'event_registration'})

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        The accepted kwargs are the possible actions. ``True`` means that the
        action occured and ``False`` that it was reverted.
        """
        if sum(action is not None for action in (approved, paid, rejected)) > 1:
            raise Exception("More than one action specified")
        initial_state = self.state
        regform = self.registration_form
        invitation = self.invitation
        moderation_required = (regform.moderation_enabled and not _skip_moderation and
                               (not invitation or not invitation.skip_moderation))
        with db.session.no_autoflush:
            payment_required = regform.event_new.has_feature('payment') and self.price
        if self.state == RegistrationState.pending:
            if approved and payment_required:
                self.state = RegistrationState.unpaid
            elif approved:
                self.state = RegistrationState.complete
            elif rejected:
                self.state = RegistrationState.rejected
        elif self.state == RegistrationState.unpaid:
            if paid:
                self.state = RegistrationState.complete
            elif approved is False:
                self.state = RegistrationState.pending
        elif self.state == RegistrationState.complete:
            if approved is False and payment_required is False and moderation_required:
                self.state = RegistrationState.pending
            elif paid is False and payment_required:
                self.state = RegistrationState.unpaid
        if self.state != initial_state:
            signals.event.registration_state_updated.send(self, previous_state=initial_state)
Beispiel #14
0
class APIKey(db.Model):
    """API keys for users."""
    __tablename__ = 'api_keys'
    __table_args__ = (db.Index(None, 'user_id', unique=True, postgresql_where=db.text('is_active')),
                      {'schema': 'users'})

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

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

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

    def register_used(self, ip, uri, authenticated):
        """Update the last used information."""
        self.last_used_dt = now_utc()
        self.last_used_ip = ip
        self.last_used_uri = uri
        self.last_used_auth = authenticated
        self.use_count = APIKey.use_count + 1
Beispiel #15
0
class Abstract(ProposalMixin, ProposalRevisionMixin, DescriptionMixin,
               CustomFieldsMixin, AuthorsSpeakersMixin, db.Model):
    """Represents an abstract that can be associated to a Contribution."""

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def user_owns(self, user):
        if not user:
            return None
        return user == self.submitter or any(x.person.user == user
                                             for x in self.person_links)
Beispiel #16
0
 def __table_args__(cls):
     return (db.Index('ix_uq_user_id_name_lower', cls.user_id, db.func.lower(cls.name), unique=True,
                      postgresql_where=db.text('revoked_dt IS NULL')),
             {'schema': 'users'})
Beispiel #17
0
class Session(DescriptionMixin, ColorMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin,
              AttachedNotesMixin, db.Model):
    __tablename__ = 'sessions'
    __auto_table_args = (db.Index(None, 'friendly_id', 'event_id', unique=True,
                                  postgresql_where=db.text('NOT is_deleted')),
                         {'schema': 'events'})
    location_backref_name = 'sessions'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    default_colors = ColorTuple('#202020', '#e3f2d3')
    allow_relationship_preloading = True

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        This only applies to the blocks themselves, not to contributions inside them.
        """
        from indico.modules.events.sessions.util import session_coordinator_priv_enabled
        if user is None:
            return False
        # full session manager can always manage blocks. this also includes event managers and higher.
        elif self.session.can_manage(user, allow_admin=allow_admin):
            return True
        # session coordiator if block management is allowed
        elif (self.session.can_manage(user, 'coordinate') and
                session_coordinator_priv_enabled(self.event, 'manage-blocks')):
            return True
        else:
            return False
Beispiel #18
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_category_role = False
    is_registration_form = 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'),
                      db.CheckConstraint("(picture IS NULL) = (picture_metadata::text = 'null')", 'valid_picture'),
                      {'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: str(uuid4())
    )
    #: the user profile picture
    picture = db.deferred(db.Column(
        db.LargeBinary,
        nullable=True
    ))
    #: user profile picture metadata
    picture_metadata = db.Column(
        JSONB,
        nullable=False,
        default=lambda: None
    )
    #: user profile picture source
    picture_source = db.Column(
        PyIntEnum(ProfilePictureSource),
        nullable=False,
        default=ProfilePictureSource.standard,
    )

    _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,
        sync_backref=False,
        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)
    # - category_roles (CategoryRole.members)
    # - 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_app_links (OAuthApplicationUserLink.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 f'User:{self.id}'

    @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 external_identities(self):
        """The external identities of the user."""
        return {x for x in self.identities if x.provider != 'indico'}

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

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

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

    @property
    def last_login_dt(self):
        """The datetime when the user last logged in."""
        if not self.identities:
            return None
        return max(self.identities, key=attrgetter('safe_last_login_dt')).last_login_dt

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

    @cached_property
    def settings(self):
        """Return 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}

    @property
    def has_picture(self):
        return self.picture_metadata is not None

    @property
    def avatar_url(self):
        if self.is_system:
            return url_for('assets.image', filename='robot.svg')
        slug = self.picture_metadata['hash'] if self.picture_metadata else 'default'
        return url_for('users.user_profile_picture_display', self, slug=slug)

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

    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().get_full_name(*args, **kwargs)

    def make_email_primary(self, email):
        """Promote 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 = str(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
Beispiel #19
0
class Contribution(DescriptionMixin, ProtectionManagersMixin, LocationMixin,
                   AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin,
                   CustomFieldsMixin, db.Model):
    __tablename__ = 'contributions'
    __auto_table_args = (
        db.Index(None,
                 'friendly_id',
                 'event_id',
                 unique=True,
                 postgresql_where=db.text('NOT is_deleted')),
        db.Index(None, 'event_id',
                 'track_id'), db.Index(None, 'event_id', 'abstract_id'),
        db.Index(None,
                 'abstract_id',
                 unique=True,
                 postgresql_where=db.text('NOT is_deleted')),
        db.CheckConstraint(
            "session_block_id IS NULL OR session_id IS NOT NULL",
            'session_block_if_session'),
        db.ForeignKeyConstraint(
            ['session_block_id', 'session_id'],
            ['events.session_blocks.id', 'events.session_blocks.session_id']),
        {
            'schema': 'events'
        })
    location_backref_name = 'contributions'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    description_wrapper = MarkdownText
    allow_relationship_preloading = True

    PRELOAD_EVENT_ATTACHED_ITEMS = True
    PRELOAD_EVENT_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id'

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

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

    event_new = 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))
    #: 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))

    # relationship backrefs:
    # - attachment_folders (AttachmentFolder.contribution)
    # - legacy_mapping (LegacyContributionMapping.contribution)
    # - note (EventNote.contribution)
    # - paper_files (PaperFile.contribution)
    # - paper_reviewing_roles (PaperReviewingRole.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))
        return db.column_property(query, deferred=True)

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

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

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

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

    @property
    def track(self):
        return self.event_new.as_legacy.getTrackById(str(self.track_id))

    @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 speakers(self):
        return [
            person_link for person_link in self.person_links
            if person_link.is_speaker
        ]

    @property
    def speaker_names(self):
        return [
            person_link.full_name for person_link in self.person_links
            if person_link.is_speaker
        ]

    @property
    def primary_authors(self):
        return {
            person_link
            for person_link in self.person_links
            if person_link.author_type == AuthorType.primary
        }

    @property
    def secondary_authors(self):
        return {
            person_link
            for person_link in self.person_links
            if person_link.author_type == AuthorType.secondary
        }

    @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_new.locator, contrib_id=self.id)

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

    def can_manage(self,
                   user,
                   role=None,
                   allow_admin=True,
                   check_parent=True,
                   explicit_role=False):
        if super(Contribution, self).can_manage(user,
                                                role,
                                                allow_admin=allow_admin,
                                                check_parent=check_parent,
                                                explicit_role=explicit_role):
            return True
        if (check_parent and self.session_id is not None
                and self.session.can_manage(user,
                                            'coordinate',
                                            allow_admin=allow_admin,
                                            explicit_role=explicit_role)
                and session_coordinator_priv_enabled(self.event_new,
                                                     '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 get_field_value(self, field_id, raw=False):
        fv = next((v for v in self.field_values
                   if v.contribution_field_id == field_id), None)
        if raw:
            return fv
        else:
            return fv.friendly_data if fv else ''
Beispiel #20
0
class Contribution(DescriptionMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin,
                   PersonLinkDataMixin, AuthorsSpeakersMixin, CustomFieldsMixin, db.Model):
    __tablename__ = 'contributions'
    __auto_table_args = (db.Index(None, 'friendly_id', 'event_id', unique=True,
                                  postgresql_where=db.text('NOT is_deleted')),
                         db.Index(None, 'event_id', 'track_id'),
                         db.Index(None, 'event_id', 'abstract_id'),
                         db.Index(None, 'abstract_id', unique=True, postgresql_where=db.text('NOT is_deleted')),
                         db.CheckConstraint("session_block_id IS NULL OR session_id IS NOT NULL",
                                            'session_block_if_session'),
                         db.ForeignKeyConstraint(['session_block_id', 'session_id'],
                                                 ['events.session_blocks.id', 'events.session_blocks.session_id']),
                         {'schema': 'events'})
    location_backref_name = 'contributions'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    possible_render_modes = {RenderMode.html, RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    allow_relationship_preloading = True

    PRELOAD_EVENT_ATTACHED_ITEMS = True
    PRELOAD_EVENT_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id'

    @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] = 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)
                    .as_scalar())
        return db.relationship(
            'PaperRevision',
            uselist=False,
            lazy=True,
            viewonly=True,
            primaryjoin=db.and_(PaperRevision._contribution_id == cls.id, PaperRevision.submitted_dt == subquery)
        )

    # relationship backrefs:
    # - _paper_files (PaperFile._contribution)
    # - _paper_revisions (PaperRevision._contribution)
    # - attachment_folders (AttachmentFolder.contribution)
    # - 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))
        return db.column_property(query, deferred=True)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def can_manage(self, user, permission=None, allow_admin=True, check_parent=True, explicit_permission=False):
        if super(Contribution, self).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)
Beispiel #21
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)
    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 checked-in status should be displayed in the event pages and participant list
    publish_checkin_enabled = db.Column(db.Boolean,
                                        nullable=False,
                                        default=False)
    #: Whether registrations must be approved by a manager
    moderation_enabled = db.Column(db.Boolean, nullable=False, default=False)
    #: The base fee users have to pay when registering
    base_price = db.Column(
        db.Numeric(8, 2),  # max. 999999.99
        nullable=False,
        default=0)
    #: Currency for prices in the registration form
    currency = db.Column(db.String, nullable=False)
    #: Notifications sender address
    notification_sender_address = db.Column(db.String, nullable=True)
    #: Custom message to include in emails for pending registrations
    message_pending = db.Column(db.Text, nullable=False, default='')
    #: Custom message to include in emails for unpaid registrations
    message_unpaid = db.Column(db.Text, nullable=False, default='')
    #: Custom message to include in emails for complete registrations
    message_complete = db.Column(db.Text, nullable=False, default='')
    #: Whether the manager notifications for this event are enabled
    manager_notifications_enabled = db.Column(db.Boolean,
                                              nullable=False,
                                              default=False)
    #: List of emails that should receive management notifications
    manager_notification_recipients = db.Column(ARRAY(db.String),
                                                nullable=False,
                                                default=[])
    #: Whether tickets are enabled for this form
    tickets_enabled = db.Column(db.Boolean, nullable=False, default=False)
    #: Whether to send tickets by e-mail
    ticket_on_email = db.Column(db.Boolean, nullable=False, default=True)
    #: Whether to show a ticket download link on the event homepage
    ticket_on_event_page = db.Column(db.Boolean, nullable=False, default=True)
    #: Whether to show a ticket download link on the registration summary page
    ticket_on_summary_page = db.Column(db.Boolean,
                                       nullable=False,
                                       default=True)

    #: The Event containing this registration form
    event_new = db.relationship('Event',
                                lazy=True,
                                backref=db.backref('registration_forms',
                                                   lazy='dynamic'))
    # 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 != None) & (cls.end_dt <= now_utc())  # noqa

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

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

    @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 != None)  # noqa

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

    @property
    def locator(self):
        return dict(self.event.getLocator(), 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 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
    def active_registrations(self):
        return [r for r in self.registrations if r.is_active]

    @property
    def sender_address(self):
        return self.notification_sender_address or self.event.getSupportInfo(
        ).getEmail()

    @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:
            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
Beispiel #22
0
class PaperRevision(ProposalRevisionMixin, RenderModeMixin, db.Model):
    __tablename__ = 'revisions'
    __table_args__ = (
        db.Index(None,
                 'contribution_id',
                 unique=True,
                 postgresql_where=db.text('state = {}'.format(
                     PaperRevisionState.accepted))),
        db.UniqueConstraint('contribution_id', 'submitted_dt'),
        db.CheckConstraint(
            '(state IN ({}, {}, {})) = (judge_id IS NOT NULL)'.format(
                PaperRevisionState.accepted, PaperRevisionState.rejected,
                PaperRevisionState.to_be_corrected),
            name='judge_if_judged'),
        db.CheckConstraint(
            '(state IN ({}, {}, {})) = (judgment_dt IS NOT NULL)'.format(
                PaperRevisionState.accepted, PaperRevisionState.rejected,
                PaperRevisionState.to_be_corrected),
            name='judgment_dt_if_judged'), {
                'schema': 'event_paper_reviewing'
            })

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

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

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

    judgment_comment = RenderModeMixin.create_hybrid_property(
        '_judgment_comment')

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

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

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

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

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

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

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

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

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

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

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

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

    def get_reviewed_for_groups(self, user, include_reviewed=False):
        from indico.modules.events.papers.models.reviews import PaperTypeProxy
        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, basestring):
                review_type = PaperReviewType[review_type]
            return any(review.user == user and review.type == review_type
                       for review in self.reviews)
        else:
            layout_review = next(
                (review for review in self.reviews if review.user == user
                 and review.type == PaperReviewType.layout), None)
            content_review = next(
                (review for review in self.reviews if review.user == user
                 and review.type == PaperReviewType.content), None)
            if user in self._contribution.paper_layout_reviewers and user in self._contribution.paper_content_reviewers:
                return bool(layout_review and content_review)
            elif user in self._contribution.paper_layout_reviewers:
                return bool(layout_review)
            elif user in self._contribution.paper_content_reviewers:
                return bool(content_review)

    def get_spotlight_file(self):
        pdf_files = [
            paper_file for paper_file in self.files
            if paper_file.content_type == 'application/pdf'
        ]
        return pdf_files[0] if len(pdf_files) == 1 else None
Beispiel #23
0
 def __table_args__(cls):
     return (db.Index('ix_uq_applications_name_lower', db.func.lower(cls.name), unique=True),
             db.Index(None, cls.system_app_type, unique=True,
                      postgresql_where=db.text('system_app_type != {}'.format(SystemAppType.none.value))),
             {'schema': 'oauth'})
Beispiel #24
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(
            f'NOT is_manager_only OR 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(
            f'is_enabled OR 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(
                     f'type = {RegistrationFormItemType.section_pd}')),
        db.Index('ix_uq_form_items_pd_field',
                 'registration_form_id',
                 'personal_data_type',
                 unique=True,
                 postgresql_where=db.text(
                     f'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(JSONB, 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):
        """Return 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

    def __repr__(self):
        return format_repr(self,
                           'id',
                           'registration_form_id',
                           is_enabled=True,
                           is_deleted=False,
                           is_manager_only=False,
                           _text=self.title)
Beispiel #25
0
class Registration(db.Model):
    """Somebody's registration for an event through a registration form."""
    __tablename__ = 'registrations'
    __table_args__ = (
        db.CheckConstraint('email = lower(email)', 'lowercase_email'),
        db.Index(None,
                 'friendly_id',
                 'event_id',
                 unique=True,
                 postgresql_where=db.text('NOT is_deleted')),
        db.Index(None,
                 'registration_form_id',
                 'user_id',
                 unique=True,
                 postgresql_where=db.text(
                     'NOT is_deleted AND (state NOT IN (3, 4))')),
        db.Index(None,
                 'registration_form_id',
                 'email',
                 unique=True,
                 postgresql_where=db.text(
                     'NOT is_deleted AND (state NOT IN (3, 4))')),
        db.ForeignKeyConstraint(['event_id', 'registration_form_id'], [
            'event_registration.forms.event_id', 'event_registration.forms.id'
        ]), {
            'schema': 'event_registration'
        })

    #: The ID of the object
    id = db.Column(db.Integer, primary_key=True)
    #: The unguessable ID for the object
    uuid = db.Column(UUID,
                     index=True,
                     unique=True,
                     nullable=False,
                     default=lambda: str(uuid4()))
    #: The human-friendly ID for the object
    friendly_id = db.Column(db.Integer,
                            nullable=False,
                            default=_get_next_friendly_id)
    #: The ID of the event
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    #: The ID of the registration form
    registration_form_id = db.Column(
        db.Integer,
        db.ForeignKey('event_registration.forms.id'),
        index=True,
        nullable=False)
    #: The ID of the user who registered
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=True)
    #: The ID of the latest payment transaction associated with this registration
    transaction_id = db.Column(db.Integer,
                               db.ForeignKey('events.payment_transactions.id'),
                               index=True,
                               unique=True,
                               nullable=True)
    #: The state a registration is in
    state = db.Column(
        PyIntEnum(RegistrationState),
        nullable=False,
    )
    #: The base registration fee (that is not specific to form items)
    base_price = db.Column(
        db.Numeric(11, 2),  # max. 999999999.99
        nullable=False,
        default=0)
    #: The price modifier applied to the final calculated price
    price_adjustment = db.Column(
        db.Numeric(11, 2),  # max. 999999999.99
        nullable=False,
        default=0)
    #: Registration price currency
    currency = db.Column(db.String, nullable=False)
    #: The date/time when the registration was recorded
    submitted_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc,
    )
    #: The email of the registrant
    email = db.Column(db.String, nullable=False)
    #: The first name of the registrant
    first_name = db.Column(db.String, nullable=False)
    #: The last name of the registrant
    last_name = db.Column(db.String, nullable=False)
    #: If the registration has been deleted
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: The unique token used in tickets
    ticket_uuid = db.Column(UUID,
                            index=True,
                            unique=True,
                            nullable=False,
                            default=lambda: str(uuid4()))
    #: Whether the person has checked in. Setting this also sets or clears
    #: `checked_in_dt`.
    checked_in = db.Column(db.Boolean, nullable=False, default=False)
    #: The date/time when the person has checked in
    checked_in_dt = db.Column(UTCDateTime, nullable=True)
    #: If given a reason for rejection
    rejection_reason = db.Column(
        db.String,
        nullable=False,
        default='',
    )
    #: Type of consent given to publish this registration
    consent_to_publish = db.Column(PyIntEnum(RegistrationVisibility),
                                   nullable=False,
                                   default=RegistrationVisibility.nobody)
    #: Management-set override for visibility
    participant_hidden = db.Column(db.Boolean, nullable=False, default=False)
    #: The Event containing this registration
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('registrations',
                                               lazy='dynamic'))
    # The user linked to this registration
    user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'registrations',
            lazy='dynamic'
            # XXX: a delete-orphan cascade here would delete registrations when NULLing the user
        ))
    #: The latest payment transaction associated with this registration
    transaction = db.relationship('PaymentTransaction',
                                  lazy=True,
                                  foreign_keys=[transaction_id],
                                  post_update=True)
    #: The registration this data is associated with
    data = db.relationship('RegistrationData',
                           lazy=True,
                           cascade='all, delete-orphan',
                           backref=db.backref('registration', lazy=True))
    #: The registration tags assigned to this registration
    tags = db.relationship('RegistrationTag',
                           secondary=registrations_tags_table,
                           passive_deletes=True,
                           collection_class=set,
                           backref=db.backref('registrations', lazy=False))

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

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

    @classmethod
    def merge_users(cls, target, source):
        target_regforms_used = {
            r.registration_form
            for r in target.registrations if not r.is_deleted
        }
        for r in source.registrations.all():
            if r.registration_form not in target_regforms_used:
                r.user = target

    @hybrid_method
    def is_publishable(self, is_participant):
        if self.visibility == RegistrationVisibility.nobody or not self.is_state_publishable:
            return False
        if (self.registration_form.publish_registrations_duration is not None
                and self.event.end_dt +
                self.registration_form.publish_registrations_duration <=
                now_utc()):
            return False
        if self.visibility == RegistrationVisibility.participants:
            return is_participant
        if self.visibility == RegistrationVisibility.all:
            return True
        return False

    @is_publishable.expression
    def is_publishable(cls, is_participant):
        from indico.modules.events import Event
        from indico.modules.events.registration.models.forms import RegistrationForm

        def _has_regform_publish_mode(mode):
            if is_participant:
                return cls.registration_form.has(
                    publish_registrations_participants=mode)
            else:
                return cls.registration_form.has(
                    publish_registrations_public=mode)

        consent_criterion = (cls.consent_to_publish.in_([
            RegistrationVisibility.all, RegistrationVisibility.participants
        ]) if is_participant else cls.consent_to_publish
                             == RegistrationVisibility.all)
        return db.and_(
            ~cls.participant_hidden, cls.is_state_publishable,
            cls.registration_form.has(
                db.or_(
                    RegistrationForm.publish_registrations_duration.is_(None),
                    cls.event.has(
                        (Event.end_dt +
                         RegistrationForm.publish_registrations_duration
                         ) > now_utc()))),
            ~_has_regform_publish_mode(PublishRegistrationsMode.hide_all),
            _has_regform_publish_mode(PublishRegistrationsMode.show_all)
            | consent_criterion)

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

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

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

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

    @hybrid_property
    def is_state_publishable(self):
        return self.is_active and self.state in (RegistrationState.complete,
                                                 RegistrationState.unpaid)

    @is_state_publishable.expression
    def is_state_publishable(cls):
        return cls.is_active & cls.state.in_(
            (RegistrationState.complete, RegistrationState.unpaid))

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

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

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

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

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

    @property
    def can_be_withdrawn(self):
        from indico.modules.events.registration.models.forms import ModificationMode
        if not self.is_active:
            return False
        elif self.is_paid:
            return False
        elif self.event.end_dt < now_utc():
            return False
        elif self.registration_form.modification_mode == ModificationMode.not_allowed:
            return False
        elif self.registration_form.modification_end_dt and self.registration_form.modification_end_dt < now_utc(
        ):
            return False
        else:
            return True

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

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

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

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

    @property
    def avatar_url(self):
        """Return the url of the user's avatar."""
        return url_for('event_registration.registration_avatar', self)

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

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

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

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

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

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

    @property
    def summary_data(self):
        """Export registration data nested in sections and fields."""
        def _fill_from_regform():
            for section in self.registration_form.sections:
                if not section.is_visible:
                    continue
                summary[section] = {}
                for field in section.fields:
                    if not field.is_visible:
                        continue
                    if field in field_summary:
                        summary[section][field] = field_summary[field]

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

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

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

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

    @property
    def visibility_before_override(self):
        if self.registration_form.publish_registrations_participants == PublishRegistrationsMode.hide_all:
            return RegistrationVisibility.nobody
        vis = self.consent_to_publish
        if self.registration_form.publish_registrations_participants == PublishRegistrationsMode.show_all:
            if self.registration_form.publish_registrations_public == PublishRegistrationsMode.hide_all:
                return RegistrationVisibility.participants
            if self.registration_form.publish_registrations_public == PublishRegistrationsMode.show_all:
                return RegistrationVisibility.all
            return max(vis, RegistrationVisibility.participants)
        elif self.registration_form.publish_registrations_public == PublishRegistrationsMode.hide_all:
            return min(vis, RegistrationVisibility.participants)
        return vis

    @property
    def visibility(self):
        if self.participant_hidden:
            return RegistrationVisibility.nobody
        return self.visibility_before_override

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

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

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

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

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

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

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

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

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

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

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

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

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

        The accepted kwargs are the possible actions. ``True`` means that the
        action occured and ``False`` that it was reverted.
        """
        if sum(action is not None
               for action in (approved, paid, rejected, withdrawn)) > 1:
            raise Exception('More than one action specified')
        initial_state = self.state
        regform = self.registration_form
        invitation = self.invitation
        moderation_required = (regform.moderation_enabled
                               and not _skip_moderation
                               and (not invitation
                                    or not invitation.skip_moderation))
        with db.session.no_autoflush:
            payment_required = regform.event.has_feature('payment') and bool(
                self.price)
        if self.state == RegistrationState.pending:
            if approved and payment_required:
                self.state = RegistrationState.unpaid
            elif approved:
                self.state = RegistrationState.complete
            elif rejected:
                self.state = RegistrationState.rejected
            elif withdrawn:
                self.state = RegistrationState.withdrawn
        elif self.state == RegistrationState.unpaid:
            if paid:
                self.state = RegistrationState.complete
            elif approved is False:
                self.state = RegistrationState.pending
            elif withdrawn:
                self.state = RegistrationState.withdrawn
        elif self.state == RegistrationState.complete:
            if approved is False and payment_required is False and moderation_required:
                self.state = RegistrationState.pending
            elif paid is False and payment_required:
                self.state = RegistrationState.unpaid
            elif withdrawn:
                self.state = RegistrationState.withdrawn
        elif self.state == RegistrationState.rejected:
            if rejected is False and moderation_required:
                self.state = RegistrationState.pending
            elif rejected is False and payment_required:
                self.state = RegistrationState.unpaid
            elif rejected is False:
                self.state = RegistrationState.complete
        elif self.state == RegistrationState.withdrawn:
            if withdrawn is False and moderation_required:
                self.state = RegistrationState.pending
            elif withdrawn is False and payment_required:
                self.state = RegistrationState.unpaid
            elif withdrawn is False:
                self.state = RegistrationState.complete
        if self.state != initial_state:
            signals.event.registration_state_updated.send(
                self, previous_state=initial_state)

    def has_conflict(self):
        """Check if there are other valid registrations for the same user.

        This is intended for cases where this registration is currenly invalid
        (rejected or withdrawn) to determine whether it would be acceptable to
        restore it.
        """
        conflict_criteria = [Registration.email == self.email]
        if self.user_id is not None:
            conflict_criteria.append(Registration.user_id == self.user_id)
        return (Registration.query.with_parent(self.registration_form).filter(
            Registration.id != self.id, ~Registration.is_deleted,
            db.or_(*conflict_criteria),
            Registration.state.notin_(
                [RegistrationState.rejected,
                 RegistrationState.withdrawn])).has_rows())

    def log(self, *args, **kwargs):
        """Log with prefilled metadata for the registration."""
        self.event.log(*args, meta={'registration_id': self.id}, **kwargs)

    def is_pending_transaction_expired(self):
        """Check if the registration has a pending transaction that expired."""
        if not self.transaction or self.transaction.status != TransactionStatus.pending:
            return False
        return self.transaction.is_pending_expired()