Example #1
0
class Holiday(db.Model):
    __tablename__ = 'holidays'
    __table_args__ = (db.UniqueConstraint('date', 'location_id'),
                      {'schema': 'roombooking'})

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    date = db.Column(
        db.Date,
        nullable=False,
        index=True
    )
    name = db.Column(
        db.String
    )
    location_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.locations.id'),
        nullable=False
    )

    # relationship backrefs:
    # - location (Location.holidays)

    @return_ascii
    def __repr__(self):
        return u'<Holiday({}, {}, {}, {})>'.format(self.id, self.date, self.name or 'n/a', self.location.name)
Example #2
0
class PaperCompetence(db.Model):
    __tablename__ = 'competences'
    __table_args__ = (db.UniqueConstraint('user_id', 'event_id'),
                      {'schema': 'event_paper_reviewing'})

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    user_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=False
    )
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=False
    )
    competences = db.Column(
        ARRAY(db.String),
        nullable=False,
        default=[]
    )

    event = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'paper_competences',
            cascade='all, delete-orphan',
            lazy=True
        )
    )
    user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'paper_competences',
            lazy='dynamic'
        )
    )

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'user_id', 'event_id', _text=', '.join(self.competences))

    @classmethod
    def merge_users(cls, target, source):
        source_competences = source.paper_competences.all()
        target_competences_by_event = {x.event: x for x in target.paper_competences}
        for comp in source_competences:
            existing = target_competences_by_event.get(comp.event)
            if existing is None:
                comp.user_id = target.id
            else:
                existing.competences = list(set(existing.competences) | set(comp.competences))
                db.session.delete(comp)
Example #3
0
class RoomAttribute(db.Model):
    __tablename__ = 'room_attributes'
    __table_args__ = (db.UniqueConstraint('name', 'location_id'), {
        'schema': 'roombooking'
    })

    id = db.Column(db.Integer, primary_key=True)
    parent_id = db.Column(db.Integer,
                          db.ForeignKey('roombooking.room_attributes.id'))
    name = db.Column(db.String, nullable=False, index=True)
    title = db.Column(db.String, nullable=False)
    location_id = db.Column(db.Integer,
                            db.ForeignKey('roombooking.locations.id'),
                            nullable=False)
    type = db.Column(db.String, nullable=False)
    is_required = db.Column(db.Boolean, nullable=False)
    is_hidden = db.Column(db.Boolean, nullable=False)

    children = db.relationship('RoomAttribute',
                               backref=db.backref('parent', remote_side=[id]))

    # relationship backrefs:
    # - location (Location.attributes)
    # - parent (RoomAttribute.children)
    # - room_associations (RoomAttributeAssociation.attribute)

    @return_ascii
    def __repr__(self):
        return u'<RoomAttribute({}, {}, {})>'.format(self.id, self.name,
                                                     self.location.name)
Example #4
0
class PaperCompetence(db.Model):
    __tablename__ = 'competences'
    __table_args__ = (db.UniqueConstraint('user_id', 'event_id'), {
        'schema': 'event_paper_reviewing'
    })

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=False)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    competences = db.Column(ARRAY(db.String), nullable=False, default=[])

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

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'user_id',
                           'event_id',
                           _text=', '.join(self.competences))
Example #5
0
class UserSetting(SettingsBase, db.Model):
    """User-specific settings"""
    __table_args__ = (db.Index(None, 'user_id', 'module',
                               'name'), db.Index(None, 'user_id', 'module'),
                      db.UniqueConstraint('user_id', 'module', 'name'),
                      db.CheckConstraint('module = lower(module)',
                                         'lowercase_module'),
                      db.CheckConstraint('name = lower(name)',
                                         'lowercase_name'), {
                                             'schema': 'users'
                                         })

    user_id = db.Column(db.Integer,
                        db.ForeignKey(User.id),
                        nullable=False,
                        index=True)

    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('_all_settings',
                                              lazy='dynamic',
                                              cascade='all, delete-orphan'))

    @return_ascii
    def __repr__(self):
        return '<UserSetting({}, {}, {}, {!r})>'.format(
            self.user_id, self.module, self.name, self.value)
Example #6
0
class EquipmentType(db.Model):
    __tablename__ = 'equipment_types'
    __table_args__ = (db.UniqueConstraint('name', 'location_id'),
                      {'schema': 'roombooking'})

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    parent_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.equipment_types.id')
    )
    name = db.Column(
        db.String,
        nullable=False,
        index=True
    )
    location_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.locations.id'),
        nullable=False
    )

    children = db.relationship(
        'EquipmentType',
        backref=db.backref(
            'parent',
            remote_side=[id]
        )
    )

    @return_ascii
    def __repr__(self):
        return u'<EquipmentType({0}, {1}, {2})>'.format(self.id, self.name, self.location_id)
Example #7
0
class Identity(db.Model):
    """Identities of Indico users."""
    __tablename__ = 'identities'
    __table_args__ = (db.UniqueConstraint('provider', 'identifier'), {
        'schema': 'users'
    })

    #: the unique id of the identity
    id = db.Column(db.Integer, primary_key=True)
    #: the id of the user this identity belongs to
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        nullable=False)
    #: the provider name of the identity
    provider = db.Column(db.String, nullable=False)
    #: the unique identifier of the user within its provider
    identifier = db.Column(db.String, nullable=False)
    #: internal data used by the flask-multipass system
    multipass_data = db.Column(JSONB, nullable=False, default=lambda: None)
    #: the user data from the user provider
    _data = db.Column('data', JSONB, nullable=False, default={})
    #: the hash of the password in case of a local identity
    password_hash = db.Column(db.String)
    #: the password of the user in case of a local identity
    password = PasswordProperty('password_hash')
    #: the timestamp of the latest login
    last_login_dt = db.Column(UTCDateTime)
    #: the ip address that was used for the latest login
    last_login_ip = db.Column(INET)

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

    @property
    def data(self):
        data = MultiDict()
        data.update(self._data)
        return data

    @data.setter
    def data(self, data):
        self._data = dict(data.lists())

    @property
    def locator(self):
        return {'identity': self.id}

    @property
    def safe_last_login_dt(self):
        """last_login_dt that is safe for sorting (no None values)."""
        return self.last_login_dt or as_utc(datetime(1970, 1, 1))

    def register_login(self, ip):
        """Update the last login information."""
        self.last_login_dt = now_utc()
        self.last_login_ip = ip

    def __repr__(self):
        return f'<Identity({self.id}, {self.user_id}, {self.provider}, {self.identifier})>'
Example #8
0
class Editable(db.Model):
    __tablename__ = 'editables'
    __table_args__ = (db.UniqueConstraint('contribution_id', 'type'), {
        'schema': 'event_editing'
    })

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

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

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

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

    # TODO: state - either a column property referencing the newest revision's state or a normal column

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

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

    def can_comment(self, user):
        return (self.event.can_manage(user, permission='paper_editing')
                or self.contribution.is_user_associated(user,
                                                        check_abstract=True))
Example #9
0
class OAuthApplicationUserLink(db.Model):
    """The authorization link between an OAuth app and a user."""

    __tablename__ = 'application_user_links'
    __table_args__ = (db.UniqueConstraint('application_id', 'user_id'), {
        'schema': 'oauth'
    })

    id = db.Column(db.Integer, primary_key=True)
    application_id = db.Column(db.Integer,
                               db.ForeignKey('oauth.applications.id',
                                             ondelete='CASCADE'),
                               nullable=False,
                               index=True)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id', ondelete='CASCADE'),
                        nullable=False,
                        index=True)
    scopes = db.Column(ARRAY(db.String), nullable=False, default=[])

    application = db.relationship('OAuthApplication',
                                  lazy=False,
                                  backref=db.backref(
                                      'user_links',
                                      lazy='dynamic',
                                      cascade='all, delete-orphan',
                                      passive_deletes=True))
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('oauth_app_links',
                                              lazy='dynamic',
                                              cascade='all, delete-orphan',
                                              passive_deletes=True))

    # relationship backrefs:
    # - tokens (OAuthToken.app_user_link)

    def __repr__(self):
        return f'<OAuthApplicationUserLink({self.application_id}, {self.user_id}, {self.scopes})>'

    def update_scopes(self, scopes: set):
        self.scopes = sorted(set(self.scopes) | scopes)
Example #10
0
class Editable(db.Model):
    __tablename__ = 'editables'
    __table_args__ = (db.UniqueConstraint('contribution_id', 'type'), {
        'schema': 'event_editing'
    })

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

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

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

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'contribution_id', 'type')
Example #11
0
class ContributionLimit(db.Model):
    __tablename__ = 'contribution_limits'
    __table_args__ = (db.UniqueConstraint('event_id', 'track_id', 'type_id'), {
        'schema': 'events'
    })

    id = db.Column(db.Integer, primary_key=True)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    track_id = db.Column(db.Integer,
                         db.ForeignKey('events.tracks.id',
                                       ondelete='SET NULL'),
                         index=True,
                         nullable=True)
    type_id = db.Column(db.Integer,
                        db.ForeignKey('events.contribution_types.id',
                                      ondelete='SET NULL'),
                        index=True,
                        nullable=True)
    value = db.Column(db.Integer, nullable=True)
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('contribution_limits',
                                               cascade='all, delete-orphan',
                                               lazy='dynamic'))
    track = db.relationship('Track',
                            lazy=True,
                            backref=db.backref('contribution_limits',
                                               cascade='all, delete-orphan',
                                               lazy='dynamic'))
    type = db.relationship('ContributionType',
                           lazy=True,
                           backref=db.backref('contribution_limits',
                                              cascade='all, delete-orphan',
                                              lazy='dynamic'))
Example #12
0
class ContributionField(db.Model):
    __tablename__ = 'contribution_fields'
    __table_args__ = (db.UniqueConstraint('event_id', 'legacy_id'), {
        'schema': 'events'
    })

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

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

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

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

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

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

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

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

    @locator_property
    def locator(self):
        return dict(self.event.locator, contrib_field_id=self.id)
Example #13
0
class Agreement(db.Model):
    """Agreements between a person and Indico"""
    __tablename__ = 'agreements'
    __table_args__ = (db.UniqueConstraint('event_id', 'type', 'identifier'),
                      {'schema': 'events'})

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def is_orphan(self):
        definition = self.definition
        if definition is None:
            raise ServiceUnavailable('This agreement type is currently not available.')
        return definition.is_agreement_orphan(self.event, self)
Example #14
0
class PaymentTransaction(db.Model):
    """Payment transactions"""
    __tablename__ = 'payment_transactions'
    __table_args__ = (db.CheckConstraint('amount > 0', 'positive_amount'),
                      db.UniqueConstraint('event_id', 'registrant_id', 'timestamp'),
                      {'schema': 'events'})

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

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

    @property
    def registrant(self):
        return self.event.getRegistrantById(str(self.registrant_id))

    @registrant.setter
    def registrant(self, registrant):
        self.registrant_id = int(registrant.getId())
        self.event_id = int(registrant.getConference().getId())

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

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

    @return_ascii
    def __repr__(self):
        # in case of a new object we might not have the default status set
        status = TransactionStatus(self.status).name if self.status is not None else None
        return '<PaymentTransaction({}, {}, {}, {}, {}, {} {}, {})>'.format(self.id, self.event_id, self.registrant_id,
                                                                            status, self.provider, self.amount,
                                                                            self.currency, self.timestamp)

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

    @classmethod
    def create_next(cls, registrant, amount, currency, action, provider='_manual', data=None):
        event = registrant.getConference()
        new_transaction = PaymentTransaction(event_id=event.getId(), registrant_id=registrant.getId(), amount=amount,
                                             currency=currency, provider=provider, data=data)
        double_payment = False
        previous_transaction = cls.find_latest_for_registrant(registrant)
        try:
            next_status = TransactionStatusTransition.next(previous_transaction, action, provider)
        except InvalidTransactionStatus as e:
            Logger.get('payment').exception("{}\nData received: {}".format(e, data))
            return None, None
        except InvalidManualTransactionAction as e:
            Logger.get('payment').exception("Invalid manual action code '{}' on initial status\n"
                                            "Data received: {}".format(e, data))
            return None, None
        except InvalidTransactionAction as e:
            Logger.get('payment').exception("Invalid action code '{}' on initial status\n"
                                            "Data received: {}".format(e, data))
            return None, None
        except IgnoredTransactionAction as e:
            Logger.get('payment').warning("{}\nData received: {}".format(e, data))
            return None, None
        except DoublePaymentTransaction:
            next_status = TransactionStatus.successful
            double_payment = True
            Logger.get('payment').warning("Received successful payment for an already paid registrant")
        new_transaction.status = next_status
        return new_transaction, double_payment

    @staticmethod
    def find_latest_for_registrant(registrant):
        """Returns the newest transaction for a given registrant.

        :param registrant: the registrant to find the transaction for
        :return: a :class:`PaymentTransaction` or `None`
        """
        return (PaymentTransaction.find(event_id=registrant.getConference().getId(), registrant_id=registrant.getId())
                                  .order_by(PaymentTransaction.timestamp.desc())
                                  .first())
Example #15
0
class OAuthToken(db.Model):
    """OAuth tokens."""

    __tablename__ = 'tokens'
    __table_args__ = (db.UniqueConstraint('application_id', 'user_id'), {
        'schema': 'oauth'
    })

    #: the unique identifier of the token
    id = db.Column(db.Integer, primary_key=True)
    #: the identifier of the linked application
    application_id = db.Column(db.Integer,
                               db.ForeignKey('oauth.applications.id'),
                               nullable=False)
    #: the identifier of the linked user
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        nullable=False,
                        index=True)
    #: an unguessable unique string of characters
    access_token = db.Column(UUID, unique=True, nullable=False)
    #: the list of scopes the linked application has access to
    _scopes = db.Column('scopes', ARRAY(db.String))
    #: the last time the token was used by the application
    last_used_dt = db.Column(UTCDateTime, nullable=True)

    #: application authorized by this token
    application = db.relationship('OAuthApplication',
                                  lazy=True,
                                  backref=db.backref(
                                      'tokens',
                                      lazy='dynamic',
                                      cascade='all, delete-orphan'))
    #: the user who owns this token
    user = db.relationship('User',
                           lazy=False,
                           backref=db.backref('oauth_tokens',
                                              lazy='dynamic',
                                              cascade='all, delete-orphan'))

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

    @property
    def expires(self):
        return None

    @property
    def scopes(self):
        """The set of scopes the linked application has access to."""
        return set(self._scopes)

    @scopes.setter
    def scopes(self, value):
        self._scopes = sorted(value)

    @property
    def type(self):
        return 'bearer'

    def __repr__(self):  # pragma: no cover
        return f'<OAuthToken({self.id}, {self.application}, {self.user})>'
Example #16
0
class Abstract(DescriptionMixin, db.Model):
    """Represents an abstract that can be associated to a Contribution."""

    __tablename__ = 'abstracts'
    __auto_table_args = (db.UniqueConstraint('friendly_id', 'event_id'), {
        'schema': 'event_abstracts'
    })

    description_wrapper = MarkdownText

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

    id = db.Column(db.Integer, primary_key=True)
    #: The friendly ID for the abstract (same as the legacy id in ZODB)
    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.contribution_types.id'),
                        nullable=True,
                        index=True)
    accepted_track_id = db.Column(db.Integer, nullable=True, index=True)
    accepted_type_id = db.Column(db.Integer,
                                 db.ForeignKey('events.contribution_types.id'),
                                 nullable=True,
                                 index=True)
    event_new = db.relationship('Event',
                                lazy=True,
                                backref=db.backref(
                                    'abstracts',
                                    cascade='all, delete-orphan',
                                    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))
    type = db.relationship('ContributionType',
                           lazy=True,
                           foreign_keys=[type_id],
                           backref=db.backref('abstracts', lazy=True))
    accepted_type = db.relationship('ContributionType',
                                    lazy=True,
                                    foreign_keys=[accepted_type_id],
                                    backref=db.backref('accepted_as_abstracts',
                                                       lazy=True))
    # relationship backrefs:
    # - contribution (Contribution.abstract)
    # - judgments (Judgment.abstract)

    @locator_property
    def locator(self):
        return dict(self.event_new.locator,
                    abstractId=self.friendly_id,
                    confId=self.event_id)

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', friendly_id=self.friendly_id)

    def get_field_value(self, field_id):
        return next((v.friendly_data for v in self.field_values
                     if v.contribution_field_id == field_id), '')

    @property
    def as_legacy(self):
        amgr = self.event_new.as_legacy.getAbstractMgr()
        return amgr.getAbstractById(str(self.friendly_id))

    @property
    def accepted_track(self):
        return self.event_new.as_legacy.getTrackById(
            str(self.accepted_track_id))
Example #17
0
class Editable(db.Model):
    __tablename__ = 'editables'
    __table_args__ = (db.UniqueConstraint('contribution_id', 'type'), {
        'schema': 'event_editing'
    })

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def can_see_editor_names(self, user, actor=None):
        """Whether the user can see the names of editing team members.

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

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

        return (not editable_type_settings[self.type].get(
            self.event, 'anonymous_team')
                or (actor and not self.can_see_editor_names(actor))
                or self._has_general_editor_permissions(user))

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

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

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

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

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

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

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

    def log(self, *args, **kwargs):
        """Log with prefilled metadata for the editable."""
        self.event.log(*args, meta={'editable_id': self.id}, **kwargs)
Example #18
0
class Editable(db.Model):
    __tablename__ = 'editables'
    __table_args__ = (db.UniqueConstraint('contribution_id', 'type'), {
        'schema': 'event_editing'
    })

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

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

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

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

    # TODO: state - either a column property referencing the newest revision's state or a normal column

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

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

    def can_comment(self, user):
        return (
            self.event.can_manage(user, permission=self.type.editor_permission)
            or self.contribution.is_user_associated(user, check_abstract=True))

    @property
    def review_conditions_valid(self):
        from indico.modules.events.editing.models.review_conditions import EditingReviewCondition
        query = EditingReviewCondition.query.with_parent(
            self.event).filter_by(type=self.type)
        review_conditions = [{ft.id
                              for ft in cond.file_types} for cond in query]
        file_types = {file.file_type_id for file in self.revisions[-1].files}
        if not review_conditions:
            return True
        return any(file_types >= cond for cond in review_conditions)
Example #19
0
class Editable(db.Model):
    __tablename__ = 'editables'
    __table_args__ = (db.UniqueConstraint('contribution_id', 'type'), {
        'schema': 'event_editing'
    })

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

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

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

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

    # TODO: state - either a column property referencing the newest revision's state or a normal column

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

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

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

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

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

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

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

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

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

        These are actions usually made by the assigned Editor of the
        editable, such as making changes, asking the user to make changes,
        or approving/rejecting the editable.
        """
        # If the user can't even see the timeline, we never allow any modifications
        if not self.can_see_timeline(user):
            return False
        # Editing/event managers can perform actions without being the assigned editor
        # XXX: Do we want this? Or should they have to assign themselves first if they
        #      want to do actions that would usually be done by the assigned editor?
        if self.event.can_manage(user, permission='editing_manager'):
            return True
        # Editors need the permission on the editable type and also be the assigned editor
        if self.editor == user and self.event.can_manage(
                user, permission=self.type.editor_permission):
            return True
        return False

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

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

    @property
    def review_conditions_valid(self):
        from indico.modules.events.editing.models.review_conditions import EditingReviewCondition
        query = EditingReviewCondition.query.with_parent(
            self.event).filter_by(type=self.type)
        review_conditions = [{ft.id
                              for ft in cond.file_types} for cond in query]
        file_types = {file.file_type_id for file in self.revisions[-1].files}
        if not review_conditions:
            return True
        return any(file_types >= cond for cond in review_conditions)
Example #20
0
 def __auto_table_args():
     return (db.Index(None, 'category_id', 'module',
                      'name'), db.Index(None, 'category_id', 'module'),
             db.UniqueConstraint('category_id', 'module', 'name'), {
                 'schema': 'categories'
             })
Example #21
0
class RegistrationInvitation(db.Model):
    """An invitation for someone to register."""
    __tablename__ = 'invitations'
    __table_args__ = (db.CheckConstraint(
        "(state = {state}) OR (registration_id IS NULL)".format(
            state=InvitationState.accepted),
        name='registration_state'),
                      db.UniqueConstraint('registration_form_id', 'email'), {
                          'schema': 'event_registration'
                      })

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

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

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

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

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

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

    def __repr__(self):
        full_name = f'{self.first_name} {self.last_name}'
        return format_repr(self,
                           'id',
                           'registration_form_id',
                           'email',
                           'state',
                           _text=full_name)
Example #22
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
Example #23
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
Example #24
0
class SessionBlock(LocationMixin, db.Model):
    __tablename__ = 'session_blocks'
    __auto_table_args = (db.UniqueConstraint('id', 'session_id'),  # useless but needed for the compound fkey
                         {'schema': 'events'})
    location_backref_name = 'session_blocks'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', _text=self.title or None)
Example #25
0
class Abstract(DescriptionMixin, CustomFieldsMixin, AuthorsSpeakersMixin,
               db.Model):
    """Represents an abstract that can be associated to a Contribution."""

    __tablename__ = 'abstracts'
    __auto_table_args = (
        db.UniqueConstraint('friendly_id', 'event_id'),
        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'}

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

    id = db.Column(db.Integer, primary_key=True)
    #: The friendly ID for the abstract (same as the legacy id in ZODB)
    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'),
        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'),
                                  nullable=True,
                                  index=True)
    accepted_contrib_type_id = db.Column(
        db.Integer,
        db.ForeignKey('events.contribution_types.id'),
        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_new = 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))
    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))
    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))
    #: 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))
    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))
    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',
                                   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_new.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)

    @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_new.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):
        if not user:
            return False
        if not self.event_new.cfa.allow_comments:
            return False
        if self.user_owns(
                user) and self.event_new.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_new.can_manage(
                user, role='track_convener', explicit_role=True):
            return False
        elif self.event_new 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_new.can_manage(
                user, role='abstract_reviewer', explicit_role=True):
            return False
        elif self.event_new 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_new.can_manage(user):
            return True
        elif self.event_new.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_new.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_new.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_new.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(track=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_tracks_by_user(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_new 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

    @memoize_request
    def get_reviewer_render_data(self, user):
        tracks = self.get_reviewed_for_tracks_by_user(user,
                                                      include_reviewed=True)
        reviews = {x.track: x for x in self.get_reviews(user=user)}
        reviewed_tracks = {x.track for x in reviews.itervalues()}
        missing_tracks = tracks - reviewed_tracks
        return {
            'tracks': tracks,
            'reviewed_tracks': reviewed_tracks,
            'missing_tracks': missing_tracks,
            'reviews': reviews
        }

    def get_reviews(self, track=None, user=None):
        """Get all reviews on a particular track and/or by a particular user.

        :param track: will show only reviews for the given track
        :param user: will show only review by the given user
        """
        reviews = self.reviews[:]
        if track:
            reviews = [x for x in reviews if x.track == track]
        if user:
            reviews = [x for x in reviews if x.user == user]
        return reviews

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

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

    def user_owns(self, user):
        if not user:
            return None
        return user == self.submitter or any(x.person.user == user
                                             for x in self.person_links)
Example #26
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
Example #27
0
 def __auto_table_args():
     return db.UniqueConstraint('event_id', 'module', 'name'),