示例#1
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)
    duration = db.Column(db.Interval, nullable=False)
    board_number = db.Column(db.String, nullable=False, default='')
    keywords = db.Column(ARRAY(db.String), nullable=False, default=[])
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: The last user-friendly sub-contribution ID
    _last_friendly_subcontribution_id = db.deferred(
        db.Column('last_friendly_subcontribution_id',
                  db.Integer,
                  nullable=False,
                  default=0))

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

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

    # relationship backrefs:
    # - _paper_files (PaperFile._contribution)
    # - _paper_revisions (PaperRevision._contribution)
    # - attachment_folders (AttachmentFolder.contribution)
    # - legacy_mapping (LegacyContributionMapping.contribution)
    # - note (EventNote.contribution)
    # - 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

    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)
示例#2
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)

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

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

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

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

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

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

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

    @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 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] = 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.items():
                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

    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)
示例#3
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
    possible_render_modes = {RenderMode.html, RenderMode.markdown}
    default_render_mode = RenderMode.markdown
    allow_relationship_preloading = True

    PRELOAD_EVENT_ATTACHED_ITEMS = True
    PRELOAD_EVENT_NOTES = True
    ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id'

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

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: The human-friendly ID for the contribution
    friendly_id = db.Column(
        db.Integer,
        nullable=False,
        default=_get_next_friendly_id
    )
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=False
    )
    session_id = db.Column(
        db.Integer,
        db.ForeignKey('events.sessions.id'),
        index=True,
        nullable=True
    )
    session_block_id = db.Column(
        db.Integer,
        db.ForeignKey('events.session_blocks.id'),
        index=True,
        nullable=True
    )
    track_id = db.Column(
        db.Integer,
        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 ''