示例#1
0
class BaseAccessMixin:
    @declared_attr
    def __tablename__(cls):
        return "access"

    id = sa.Column(sa.Integer, primary_key=True)
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    mode = sa.Column(sau.ChoiceType(AccessMode, impl=sa.String()),
                     nullable=False,
                     default=AccessMode.ro)

    @declared_attr
    def user_id(cls):
        return sa.Column(sa.Integer, sa.ForeignKey('user.id'), nullable=False)

    @declared_attr
    def user(cls):
        return sa.orm.relationship('User', backref='access_grants')

    @declared_attr
    def repo_id(cls):
        return sa.Column(sa.Integer,
                         sa.ForeignKey('repository.id', ondelete="CASCADE"),
                         nullable=False)

    @declared_attr
    def repo(cls):
        return sa.orm.relationship('Repository',
                                   backref=sa.orm.backref(
                                       'access_grants', cascade="all, delete"))

    def __repr__(self):
        return '<Access {} {}->{}:{}>'.format(self.id, self.user_id,
                                              self.repo_id, self.mode)
示例#2
0
class Payment(Base):
    """Платежи от клиентов по сервисам, снятия на нужды парка и зарплату"""

    __tablename__ = 'payment'

    class Currency(enum.IntEnum):
        KZT = 398

    class Status(enum.IntEnum):
        NEW = 0
        PAID = 10

    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)

    amount = sa.Column(sa.Numeric(8, 2), nullable=False)
    """Сумма платежа"""

    invoice_id = sa.Column(sa.Integer,
                           sa.ForeignKey('invoice.id'),
                           nullable=False)
    """По какому инвойсу оплата"""

    status = sa.Column(su.ChoiceType(Status, impl=sa.Integer()))
    """Статус платежа. От нового до оплаченного"""

    client_id = sa.Column(sa.Integer,
                          sa.ForeignKey('client.id'),
                          nullable=True)
    """"""

    discounts = relationship('Discount',
                             secondary=payment_discounts,
                             back_populates='payments')

    invoice = relationship('Invoice', back_populates='payments', uselist=False)
示例#3
0
class Inventory(Base):
    """Инвентарь. Тип, описание, кому принадлежит"""
    class Type(enum.Enum):
        SKATEBOARD = 'skate'
        BMX = 'bmx'
        SCOOTER = 'scooter'
        OTHER = 'other'

    __tablename__ = 'inventory'

    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)

    description = sa.Column(sa.Text)

    type = sa.Column(su.ChoiceType(Type))

    client_id = sa.Column(sa.Integer, sa.ForeignKey('client.id'))

    client = relationship('Client',
                          lazy='joined',
                          uselist=False,
                          back_populates='inventory')

    parking = relationship('Parking',
                           lazy='joined',
                           uselist=False,
                           back_populates='inventory')
class User(Base):
    __tablename__ = 'user'
    id = sa.Column(sa.Integer, primary_key=True)
    username = sa.Column(sa.Unicode(256))
    oauth_token = sa.Column(sa.String(256), nullable=False)
    user_type = sa.Column(sau.ChoiceType(UserType, impl=sa.String()),
                          nullable=False,
                          default=UserType.unconfirmed)
示例#5
0
class CustomerContact(NameMixin, BaseMetadata, mixins.LeicaMixin, Base):
    """Customer contact."""

    _workflow = workflows.ContactWorkflow

    __summary_attributes__ = [
        'id', 'fullname', 'email', 'mobile', 'position', 'created_at',
        'updated_at', 'state'
    ]

    __listing_attributes__ = __summary_attributes__

    __colanderalchemy_config__ = {'excludes': ['state_history', 'state']}

    customer_id = sa.Column(sautils.UUIDType,
                            sa.ForeignKey('customers.id'),
                            index=True,
                            nullable=False,
                            info={
                                'colanderalchemy': {
                                    'title': 'Customer ID',
                                    'validator': colander.uuid,
                                    'typ': colander.String
                                }
                            })
    """Customer ID.

    Reference to the Customer this contact is attached to.
    """

    type = sa.Column(sautils.ChoiceType(ContactTypes, impl=sa.String()),
                     default='business',
                     nullable=False)
    """Contact type.

    This field define what type of contact this one is.
    Options come from :mod:`briefy.leica.vocabularies`.
    """

    position = sa.Column(sa.String(255), nullable=True, unique=False)
    """Position of the contact person on the customer."""

    email = sa.Column(sautils.types.EmailType(), nullable=True, unique=False)
    """Email of the contact person."""

    mobile = sa.Column(sautils.types.PhoneNumberType(),
                       nullable=True,
                       unique=False)
    """Mobile phone number of the contact person."""
    @orm.validates('mobile')
    def validate_phone_attribute(self, key: str, value: str) -> str:
        """Validate if phone attribute is in the correct format.

        :param key: Attribute name.
        :param value: Phone number
        :return: Cleansed phone number
        """
        return _validate_phone(key, value)
class Customers(db.Model):
    __tablename__ = 'customers'

    MALE = 'M'
    FEMALE = 'F'
    GENDERS = [
        (MALE, 'Male'),
        (FEMALE, 'Female'),
    ]

    customerid = db.Column(db.Integer, primary_key=True)
    firstname = db.Column(db.String(50), info={'anonymize': True})
    lastname = db.Column(db.String(50), info={'anonymize': True})
    address1 = db.Column(db.String(50), info={'anonymize': True})
    address2 = db.Column(db.String(50), info={'anonymize': True})
    city = db.Column(db.String(50), info={'anonymize': True})
    state = db.Column(db.String(50), info={'anonymize': True})
    zip = db.Column(db.Integer, info={'anonymize': True})
    country = db.Column(su.CountryType)
    region = db.Column(db.Integer)
    email = db.Column(su.EmailType(50), unique=True)
    phone = db.Column(su.PhoneNumberType(max_length=50), unique=True)
    creditcardtype = db.Column(db.Integer, info={'anonymize': True})
    creditcard = db.Column(db.String(50), info={'anonymize': True})
    creditcardexpiration = db.Column(db.String(50), info={'anonymize': True})
    username = db.Column(db.String(50), info={'anonymize': True})
    password = db.Column(su.PasswordType(schemes=['pbkdf2_sha512']))
    age = db.Column(db.Integer, info={'anonymize': True})
    income = db.Column(db.Integer, info={'anonymize': True})
    gender = db.Column(su.ChoiceType(GENDERS, impl=db.String(1)))
    _deleted_at = db.Column('deleted_at', db.DateTime)
    shopping_history = db.relationship('Orders', secondary='cust_hist')

    @property
    def is_active(self):
        return self._deleted_at is None

    @classmethod
    def from_form(cls, form):
        """Retorna um novo Customer a partir de um form"""
        customer = cls()
        form.populate_obj(customer)
        return customer

    def anonymized(self):
        """Anonimiza informacoes que identificam uma pessoa"""
        self._deleted_at = datetime.now()
        self.phone = self.phone and mask(self.phone.e164[1:], 5, 0)
        self.email = self.email and \
            mask(self.email, pattern=mask.EMAIL_ANONYMIZATION, n_mask_char=2)
        self.password = None

        for column in self.__table__.columns:
            if column.info.get('anonymize'):
                setattr(self, column.name, None)
        return self
示例#7
0
文件: wiki.py 项目: terorie/man.sr.ht
class Wiki(Base):
    __tablename__ = 'wiki'
    id = sa.Column(sa.Integer, primary_key=True)
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    name = sa.Column(sa.Unicode(256), nullable=False)
    owner_id = sa.Column(sa.Integer, sa.ForeignKey('user.id'), nullable=False)
    owner = sa.orm.relationship('User', backref=sa.orm.backref('repos'))
    path = sa.Column(sa.Unicode(1024))
    visibility = sa.Column(sau.ChoiceType(WikiVisibility, impl=sa.String()),
                           nullable=False,
                           default=WikiVisibility.public)
示例#8
0
class TestItem(db.Model):
    """Represents a single PyTest test item."""
    id = db.Column(db.Integer, primary_key=True)
    nodeid = db.Column(db.String(255), nullable=False)
    state = db.Column(sqlalchemy_utils.ChoiceType(ItemState,
                                                  impl=db.Integer()),
                      nullable=False)
    duration = db.Column(db.Float)
    session_id = db.Column(db.Integer,
                           db.ForeignKey('pytest_session.id'),
                           nullable=False)

    __table_args__ = (db.UniqueConstraint('nodeid', 'session_id'), )
示例#9
0
class Trigger(Base):
    __tablename__ = 'trigger'
    id = sa.Column(sa.Integer, primary_key=True)
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    details = sa.Column(sa.String(4096), nullable=False)
    condition = sa.Column(sau.ChoiceType(TriggerCondition, impl=sa.String()),
                          nullable=False)
    trigger_type = sa.Column(sau.ChoiceType(TriggerType, impl=sa.String()),
                             nullable=False)
    job_id = sa.Column(sa.Integer, sa.ForeignKey('job.id'))
    job = sa.orm.relationship('Job', backref=sa.orm.backref('triggers'))
    job_group_id = sa.Column(sa.Integer, sa.ForeignKey('job_group.id'))
    job_group = sa.orm.relationship('JobGroup',
                                    backref=sa.orm.backref('triggers'))

    def __init__(self, job_or_group):
        from buildsrht.types import Job, JobGroup
        if isinstance(job_or_group, Job):
            self.job_id = job_or_group.id
        if isinstance(job_or_group, JobGroup):
            self.job_group_id = job_or_group.id
示例#10
0
class Task(Base):
    __tablename__ = 'task'
    id = sa.Column(sa.Integer, primary_key=True)
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    name = sa.Column(sa.Unicode(256), nullable=False)
    status = sa.Column(sau.ChoiceType(TaskStatus, impl=sa.String()),
                       nullable=False,
                       default=TaskStatus.pending)
    job_id = sa.Column(sa.Integer, sa.ForeignKey("job.id"), nullable=False)
    job = sa.orm.relationship("Job", backref=sa.orm.backref("tasks"))

    def __init__(self, job, name):
        self.job_id = job.id
        self.name = name
示例#11
0
class User(UserMixin, TimestampMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.BigInteger, primary_key=True)
    name = db.Column(db.UnicodeText, nullable=False)
    username = db.Column(db.Unicode(64), unique=True, nullable=False)
    password = db.Column(db.UnicodeText, nullable=False)
    email = db.Column(db.UnicodeText(), nullable=True)
    phone = db.Column(db.Unicode(255), nullable=True)
    secondary_phone = db.Column(db.Unicode(255), nullable=True)
    bio = db.Column(db.UnicodeText, nullable=False)
    association = db.Column(db.Unicode(64), nullable=True)
    primary_role = db.Column(sau.ChoiceType(PRIMARY_ROLE))
    language = db.Column(db.String(2), nullable=False)
    country = db.Column(db.String(2), nullable=False)
    picture = db.Column(db.UnicodeText())

    def __setattr__(self, name, value):
        if name == 'picture':
            filename = '{}.{}'.format(self.id, value.filename.split('.')[-1])
            super().__setattr__('picture',
                                upload_file(value, 'user_pictures', filename))

        else:
            super().__setattr__(name, value)

    @property
    def picture_url(self):
        if not self.picture:
            return None
        return file_url(self.picture)

    @login_manager.user_loader
    def load_user(id):
        return User.query.get(int(id))

    def set_password(self, password):
        self.password = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password, password)

    @property
    def resources_needed(self):
        resources_needed = []
        for resource in Resource.query.filter(Resource.user_id == self.id,
                                              Resource.type == 'NEEDED'):
            resources_needed.append(resource.name)
        return ", ".join(resources_needed)
示例#12
0
class User(Base, UserMixin):
    password = sa.Column(sa.String(256), nullable=False)
    new_email = sa.Column(sa.String(256))
    confirmation_hash = sa.Column(sa.String(128))
    # TODO: Consider moving pgp key into UserMixin
    pgp_key_id = sa.Column(sa.Integer, sa.ForeignKey('pgpkey.id'))
    pgp_key = sa.orm.relationship('PGPKey', foreign_keys=[pgp_key_id])
    reset_hash = sa.Column(sa.String(128))
    reset_expiry = sa.Column(sa.DateTime())
    invites = sa.Column(sa.Integer, server_default='0')
    "Number of invites this user can send"
    stripe_customer = sa.Column(sa.String(256))
    payment_cents = sa.Column(sa.Integer, nullable=False, server_default='0')
    payment_interval = sa.Column(sau.ChoiceType(PaymentInterval,
                                                impl=sa.String()),
                                 server_default='monthly')
    payment_due = sa.Column(sa.DateTime)
    welcome_emails = sa.Column(sa.Integer, nullable=False, server_default='0')

    def __init__(self, username):
        self.username = username
        self.gen_confirmation_hash()

    def gen_confirmation_hash(self):
        self.confirmation_hash = (base64.urlsafe_b64encode(
            os.urandom(18))).decode('utf-8')
        return self.confirmation_hash

    def gen_reset_hash(self):
        self.reset_hash = (base64.urlsafe_b64encode(
            os.urandom(18))).decode('utf-8')
        self.reset_expiry = datetime.utcnow() + timedelta(hours=48)
        return self.reset_hash

    def to_dict(self, first_party=False):
        return {
            "canonical_name": self.canonical_name,
            "name": self.username,
            "email": self.email,
            "url": self.url,
            "location": self.location,
            "bio": self.bio,
            "use_pgp_key": self.pgp_key.key_id if self.pgp_key else None,
            **({
                "user_type": self.user_type.value,
            } if first_party else {})
        }
示例#13
0
class Entity(db.Model):

    __tablename__ = 'entities'

    # If you change this, be sure to update migration afdf112384be
    ownership = [('c', 'company'), ('i', 'individual'), ('m', 'media'),
                 ('o', 'other')]

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(), nullable=False)
    website = db.Column(db.String())
    wiki = relationship("WikiData", uselist=False, backref="entity")
    wiki_link = db.Column(db.String())
    category = db.Column(sqlalchemy_utils.ChoiceType(ownership))
    long_name = db.Column(db.String())
    other_groups = db.Column(db.String())
    created_at = db.Column(db.DateTime())
    updated_at = db.Column(db.DateTime())
    created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    updated_by_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    blacklist = db.Column(db.Boolean(), default=False)
    candidate = db.Column(db.Boolean(), default=False)

    def __init__(self,
                 name=None,
                 website=None,
                 wiki=None,
                 wiki_page_id=None,
                 category=None,
                 long_name=None,
                 other_groups=None):
        self.name = name
        self.website = website
        self.wiki = wiki
        self.wiki_page_id = wiki_page_id
        self.category = category
        self.long_name = long_name
        self.other_groups = other_groups

    def __repr__(self):
        return '<Entity {}: {}>'.format(self.id, self.name)

    def get_parents(self):
        return [x.parent for x in self.lower_edges]

    def get_children(self):
        return [x.child for x in self.higher_edges]
示例#14
0
class Invoice(Base):

    __tablename__ = 'invoice'

    class Status(enum.IntEnum):
        NEW = 0
        """Создан и ещё не одобрен"""
        AWAITING_PAYMENT = 1
        """Ещё не оплачен"""
        PARTIALLY_PAID = 2
        """Оплачен частично и ожидает доплаты"""
        PAID = 3
        """Оплачен полностью. Равенство суммы платежей и суммы инвойса 
        - не всегда показатель того что инвойс оплачен.
        """
        CANCELLED = 4

    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)

    amount = sa.Column(sa.Numeric(8, 2), nullable=False)

    status = sa.Column(su.ChoiceType(Status, impl=sa.Integer()))

    @hybrid_property
    def paid(self):
        """Сколько уже оплачено"""
        return sum(t.amount for t in self.payments)

    @paid.expression
    def paid(cls):
        return sa.select([sa.func.sum(Payment.amount)]). \
            where(Payment.invoice_id == cls.id). \
            label('paid')

    @hybrid_property
    def debt(self):
        return self.amount - self.paid

    service_id = sa.Column(sa.Integer,
                           sa.ForeignKey('service.id'),
                           nullable=False)
    """Один инвойс может быть выставлен только по одному сервису"""

    payments = relationship('Payment', back_populates='invoice', uselist=True)
    """Платежи, совершённые по этому инвойсу"""
示例#15
0
class Job(Base):
    __tablename__ = 'job'
    id = sa.Column(sa.Integer, primary_key=True)
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    manifest = sa.Column(sa.Unicode(16384), nullable=False)
    owner_id = sa.Column(sa.Integer, sa.ForeignKey('user.id'), nullable=False)
    owner = sa.orm.relationship('User', backref=sa.orm.backref('jobs'))
    job_group_id = sa.Column(sa.Integer, sa.ForeignKey('job_group.id'))
    job_group = sa.orm.relationship('JobGroup', backref=sa.orm.backref('jobs'))
    secrets = sa.Column(sa.Boolean, nullable=False, server_default="t")
    note = sa.Column(sa.Unicode(4096))
    tags = sa.Column(sa.String())
    runner = sa.Column(sa.String)
    status = sa.Column(sau.ChoiceType(JobStatus, impl=sa.String()),
                       nullable=False,
                       default=JobStatus.pending)

    def __init__(self, owner, manifest):
        self.owner_id = owner.id
        self.manifest = manifest

    def to_dict(self):
        # When updating this, also update worker/triggers.go
        return {
            "id":
            self.id,
            "status":
            self.status.value,
            "setup_log":
            "http://{}/logs/{}/log".format(self.runner, self.id),
            "tasks": [{
                "name":
                task.name,
                "status":
                task.status.value,
                "log":
                "http://{}/logs/{}/{}/log".format(self.runner, self.id,
                                                  task.name)
            } for task in self.tasks],
            "note":
            self.note,
            "runner":
            self.runner
        }
示例#16
0
class UserAuthFactor(Base):
    __tablename__ = 'user_auth_factor'
    id = sa.Column(sa.Integer, primary_key=True)
    user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"))
    user = sa.orm.relationship('User', backref=sa.orm.backref('auth_factors'))
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    factor_type = sa.Column(
            sau.ChoiceType(FactorType, impl=sa.String()),
            nullable=False)
    secret = sa.Column(sa.LargeBinary(4096))

    def __init__(self, user, factor_type):
        self.user_id = user.id
        self.factor_type = factor_type

    def __repr__(self):
        return '<UserAuthFactor {}>'.format(self.id)
示例#17
0
class TaxInfo:
    """Tax information."""

    tax_id_type = sa.Column(
        sautils.ChoiceType(TaxIdTypes, impl=sa.String(3)),
        nullable=False,
        default='1',
        info={
            'colanderalchemy': {
                'title': 'Tax ID Type',
                'missing': colander.drop,
                'typ': colander.String
            }
        }
    )
    """Tax ID type (Vocabulary is implemented in each subclass)."""

    tax_id = sa.Column(
        sa.String(50), nullable=True,
        info={
            'colanderalchemy': {
                'title': 'Tax ID',
                'missing': colander.drop,
                'typ': colander.String
            }
        }
    )
    """Tax ID for this customer.

    i.e.: 256.018.208-49
    """

    tax_id_name = sa.Column(
        sa.String(50), nullable=True,
        info={
            'colanderalchemy': {
                'title': 'Tax ID type',
                'missing': colander.drop,
                'typ': colander.String
            }
        }
    )
    """Tax ID Name.
示例#18
0
class Secret(Base):
    __tablename__ = "secret"
    id = sa.Column(sa.Integer, primary_key=True)
    user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False)
    user = sa.orm.relationship("User", backref="secrets")
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    uuid = sa.Column(sau.UUIDType, nullable=False)
    name = sa.Column(sa.Unicode(512))
    secret_type = sa.Column(
            sau.ChoiceType(SecretType, impl=sa.String()),
            nullable=False)
    secret = sa.Column(sa.LargeBinary(16384), nullable=False)
    path = sa.Column(sa.Unicode(512))
    mode = sa.Column(sa.Integer())

    def __init__(self, user, secret_type):
        self.uuid = uuid.uuid4()
        self.user_id = user.id
        self.secret_type = secret_type
示例#19
0
class FormType(Base):
    __tablename__ = table_names.get('form_type')
    pokemon_type_id = sqlalchemy.Column(
        sqlalchemy.Integer(),
        sqlalchemy.ForeignKey(f'{table_names.get("type")}.id'),
        primary_key=True,
    )
    pokemon_type = orm.relationship('PokemonType')
    form_id = sqlalchemy.Column(
        sqlalchemy.Integer(),
        sqlalchemy.ForeignKey(f'{table_names.get("form")}.id'),
        primary_key=True,
    )
    slot = sqlalchemy.Column(
        sqlalchemy_utils.ChoiceType([
            ('P', 'Primary'),
            ('S', 'Secondary'),
        ]),
        nullable=False,
    )
示例#20
0
class BaseRepositoryMixin:
    @declared_attr
    def __tablename__(cls):
        return "repository"

    id = sa.Column(sa.Integer, primary_key=True)
    created = sa.Column(sa.DateTime, nullable=False)
    updated = sa.Column(sa.DateTime, nullable=False)
    name = sa.Column(sa.Unicode(256), nullable=False)
    description = sa.Column(sa.Unicode(1024))
    path = sa.Column(sa.Unicode(1024))
    visibility = sa.Column(sau.ChoiceType(RepoVisibility, impl=sa.String()),
                           nullable=False,
                           default=RepoVisibility.public)

    @declared_attr
    def owner_id(cls):
        return sa.Column(sa.Integer, sa.ForeignKey('user.id'), nullable=False)

    @declared_attr
    def owner(cls):
        return sa.orm.relationship('User', backref=sa.orm.backref('repos'))
示例#21
0
class FormAbility(Base):
    __tablename__ = table_names.get('form_ability')
    ability_id = sqlalchemy.Column(
        sqlalchemy.Integer(),
        sqlalchemy.ForeignKey(f'{table_names.get("ability")}.id'),
        primary_key=True,
    )
    ability = orm.relationship('Ability')
    form_id = sqlalchemy.Column(
        sqlalchemy.Integer(),
        sqlalchemy.ForeignKey(f'{table_names.get("form")}.id'),
        primary_key=True,
    )
    slot = sqlalchemy.Column(
        sqlalchemy_utils.ChoiceType([
            ('F', 'First'),
            ('S', 'Second'),
            ('H', 'Hidden'),
            ('M', 'Mega'),
        ]),
        nullable=False,
    )
示例#22
0
class Candidate(Model):

    __tablename__ = 'Candidate'

    PREDICTED_TYPES = [("real", "Real"), ("bogus", "Bogus")]

    id = sa.Column(sa.Integer, primary_key=True)
    ra = sa.Column(sa.Float, nullable=False)
    dec = sa.Column(sa.Float, nullable=False)
    mag = sa.Column(sa.Float, nullable=False)
    mag_err = sa.Column(sa.Float, nullable=False)
    predicted = sa.Column(sau.ChoiceType(PREDICTED_TYPES), nullable=True)

    pawprint_id = sa.Column(sa.Integer, sa.ForeignKey('Pawprint.id'))
    pawprint = sa.orm.relationship("Pawprint",
                                   backref=sa.orm.backref('candidates',
                                                          order_by=id))
    stack_id = sa.Column(sa.Integer, sa.ForeignKey('Stack.id'))
    stack = sa.orm.relationship("Stack",
                                backref=sa.orm.backref('candidates',
                                                       order_by=id))

    def repr(self):
        return "({}, {})".format(self.ra, self.dec)
示例#23
0
class Assignment(AssignmentDates, mixins.AssignmentRolesMixin,
                 mixins.AssignmentFinancialInfo, mixins.LeicaSubVersionedMixin,
                 Item):
    """An Assignment within an Order."""

    _workflow = workflows.AssignmentWorkflow

    __exclude_attributes__ = [
        'assets', 'comments', 'approvable_assets', 'active_order'
    ]

    __summary_attributes__ = __summary_attributes__

    __summary_attributes_relations__ = [
        'project', 'location', 'professional', 'pool', 'location', 'order'
    ]
    __listing_attributes__ = __listing_attributes__

    __to_dict_additional_attributes__ = [
        'title', 'description', 'briefing', 'assignment_date',
        'last_approval_date', 'last_submission_date',
        'last_transition_message', 'closed_on_date', 'timezone',
        'availability', 'external_state', 'set_type', 'category',
        'tech_requirements', 'assignment_internal_scout',
        'assignment_internal_qa', 'requirement_items'
    ]

    __raw_acl__ = (
        ('create', ('g:briefy_pm', 'g:briefy_finance', 'g:briefy_scout',
                    'g:system')),
        ('list', ('g:briefy_qa', 'g:briefy_scout', 'g:briefy_finance',
                  'g:system')),
        ('view', ('g:briefy_qa', 'g:briefy_scout', 'g:briefy_finance',
                  'g:system')),
        ('edit', ('g:briefy_pm', 'g:briefy_finance', 'g:briefy_scout',
                  'g:system')),
        ('delete', ('g:briefy_finance', 'g:system')),
    )

    __colanderalchemy_config__ = {
        'excludes': [
            'state_history', 'state', 'order', 'comments', 'professional',
            'assets', 'project', 'location', 'pool', 'active_order'
        ],
        'overrides':
        __colander_alchemy_config_overrides__
    }

    __parent_attr__ = 'order_id'

    @classmethod
    def create(cls, payload: dict) -> 'Item':
        """Factory that creates a new instance of this object.

        :param payload: Dictionary containing attributes and values
        :type payload: dict
        """
        payload['slug'] = create_slug_from_order(payload)
        return super().create(payload)

    @Item.slug.setter
    def slug(self, value: str):
        """Set a new slug for this object.

        Generate a slug using :func:`create_slug_from_order`
        :param value: Value of the new slug, if passed, will raise an Exception.
        """
        if value and self.slug:
            raise Exception('Assignment slug should never be updated.')
        elif value:
            self._slug = value

    refused_times = sa.Column(sa.Integer(), default=0)
    """Number times the Assignment was refused."""

    set_type = sa.Column(sautils.ChoiceType(TypesOfSetChoices,
                                            impl=sa.String()),
                         nullable=True,
                         default='new',
                         info={
                             'colanderalchemy': {
                                 'title': 'Type of Set',
                                 'default': colander.null,
                                 'missing': colander.drop,
                                 'typ': colander.String
                             }
                         })
    """Type of Set when in QA.

    Define the type of set when in QA. It will be updated by the workflow.
    """

    order_id = sa.Column(sautils.UUIDType,
                         sa.ForeignKey('orders.id'),
                         index=True,
                         nullable=False,
                         info={
                             'colanderalchemy': {
                                 'title': 'Order ID',
                                 'validator': colander.uuid,
                                 'typ': colander.String
                             }
                         })
    """Order ID.

    Relantionship to :class:`briefy.leica.models.job.order.Order`
    """

    pool_id = sa.Column(sautils.UUIDType,
                        sa.ForeignKey('pools.id'),
                        index=True,
                        nullable=True,
                        info={
                            'colanderalchemy': {
                                'title': 'Pool ID',
                                'validator': colander.uuid,
                                'typ': colander.String,
                                'missing': colander.drop,
                            }
                        })
    """Pool ID.

    Relationship to :class:`briefy.leica.models.job.pool.Pool`
    """

    # Professional
    professional_id = sa.Column(sautils.UUIDType,
                                sa.ForeignKey('professionals.id'),
                                index=True,
                                nullable=True,
                                info={
                                    'colanderalchemy': {
                                        'title': 'Professional ID',
                                        'validator': colander.uuid,
                                        'missing': colander.drop,
                                        'typ': colander.String
                                    }
                                })
    """Professional ID.

    Relationship with :class:`briefy.leica.models.professional.Professional`.

    Professional ID linked with this Assignment.
    """

    professional = orm.relationship(
        'Professional',
        foreign_keys='Assignment.professional_id',
    )
    """Relationship with :class:`briefy.leica.models.professional.Professional`.

    Professional instance linked with this Assignment.
    """

    # Assets for this Assignment
    assets = orm.relationship('Asset',
                              foreign_keys='Asset.assignment_id',
                              backref=orm.backref('assignment'),
                              lazy='dynamic')
    """Assets connected to this Assignment.

    Collection of :class:`briefy.leica.models.asset.Asset`.
    """

    approvable_assets = orm.relationship('Asset',
                                         primaryjoin="""and_(
            Asset.assignment_id == Assignment.id,
            Asset.state.in_(('approved', 'pending', 'delivered'))
        )""",
                                         viewonly=True)
    """Approvable assets connected to this Assignment.

    To be listed here, an Asset, needs to be on one of the following states:

        * approved

        * pending

        * delivered

    Collection of :class:`briefy.leica.models.asset.Asset`.
    """

    asset_types = sa.Column(JSONB,
                            info={
                                'colanderalchemy': {
                                    'title': 'Asset types.',
                                    'missing': colander.drop,
                                    'typ': schema.List(),
                                }
                            })
    """Asset types supported by this order.

    Options come from :mod:`briefy.leica.vocabularies.AssetTypes`.
    """

    @orm.validates('asset_types')
    def validate_asset_types(self, key, value):
        """Validate if values for asset_types are correct."""
        max_types = 1
        if len(value) > max_types:
            raise ValidationError(message='Invalid number of type of assets',
                                  name=key)
        members = AssetTypes.__members__
        for item in value:
            if item not in members:
                raise ValidationError(message='Invalid type of asset',
                                      name=key)
        return value

    comments = orm.relationship(
        'Comment',
        foreign_keys='Comment.entity_id',
        order_by='desc(Comment.created_at)',
        primaryjoin='Comment.entity_id == Assignment.id',
        lazy='dynamic')
    """Comments connected to this Assignment.

    Collection of :class:`briefy.leica.models.comment.Comment`.
    """

    submission_path = sa.Column(sautils.URLType,
                                nullable=True,
                                default=None,
                                info={
                                    'colanderalchemy': {
                                        'title':
                                        'Path to photographer submission',
                                        'validator': colander.url,
                                        'missing': colander.drop,
                                        'typ': colander.String
                                    }
                                })
    """Path to the assets submission.

    On Knack it usually pointed to a google-drive folder where
    the Professional have write-permission.
    This will be deprecated when assets upload is handled also using Leica.
    """

    project = association_proxy('order', 'project')
    """Project related to this Assignment.

    Instance of :class:`briefy.leica.models.project.Project`.
    """

    tech_requirements = association_proxy('order', 'tech_requirements')
    """Project tech requirements."""

    current_type = association_proxy('order', 'current_type')
    """Current type of the order."""

    location = orm.relationship(
        'OrderLocation',
        secondary='orders',
        secondaryjoin='Order.id == OrderLocation.order_id',
        primaryjoin='Order.id == Assignment.order_id',
        backref=orm.backref('assignments'),
        viewonly=True,
        uselist=False)
    """OrderLocations related to this Assignment.

    Instance of :class:`briefy.leica.models.job.location.OrderLocation`.
    """

    release_contract = sa.Column(sautils.URLType,
                                 nullable=True,
                                 info={
                                     'colanderalchemy': {
                                         'title': 'Release Contract',
                                         'validator': colander.url,
                                         'missing': colander.drop,
                                         'typ': colander.String
                                     }
                                 })

    timezone = sa.Column(TimezoneType(backend='pytz'), default='UTC')
    """Timezone in which this address is located.

    i.e.: UTC, Europe/Berlin
    Important: this will be updated by the Order timezone observer.
    """

    @sautils.aggregated('assets', sa.Column(sa.Integer, default=0))
    def total_assets(self):
        """Total number of assets.

        Counter of the number of assets in this Assignment.
        """
        return sa.func.count('1')

    @sautils.aggregated('approvable_assets', sa.Column(sa.Integer, default=0))
    def total_approvable_assets(self):
        """Total number of assets that can be approved.

        Counter of the number of assets in this Assignment that can be approved.
        """
        return sa.func.count('1')

    @property
    def approvable(self) -> bool:
        """Check if this Assignment could be approved.

        :returns: Boolean indicating if it is possible to approve this Assignment.
        """
        approvable_assets_count = self.total_approvable_assets
        check_images = self.order.number_required_assets <= approvable_assets_count
        return check_images

    @hybrid_property
    def title(self) -> str:
        """Return the title of an Order."""
        return self.order.title

    @title.comparator
    def title(cls) -> str:
        """Return the title of an Order."""
        return AssignmentTitleComparator(cls)

    @declared_attr
    def delivery(cls) -> str:
        """Return the delivery of an Order."""
        return association_proxy('order', 'delivery')

    @declared_attr
    def description(cls) -> str:
        """Return the description of an Order."""
        return association_proxy('order', 'description')

    @declared_attr
    def customer_order_id(cls) -> str:
        """Return the customer_order_id of an Order."""
        return association_proxy('order', 'customer_order_id')

    @declared_attr
    def order_slug(cls) -> str:
        """Return the order_id (slug) of an Order."""
        return association_proxy('order', 'slug')

    @declared_attr
    def requirements(cls) -> str:
        """Return the requirements of an Order."""
        return association_proxy('order', 'requirements')

    @declared_attr
    def requirement_items(cls) -> t.List[dict]:
        """Return the requirement items of an Order."""
        return association_proxy('order', 'requirement_items')

    @declared_attr
    def category(cls) -> str:
        """Return the category of an Order."""
        return association_proxy('order', 'category')

    @declared_attr
    def number_required_assets(cls) -> str:
        """Return the number_required_assets of an Order."""
        return association_proxy('order', 'number_required_assets')

    @declared_attr
    def availability(cls) -> list:
        """Return the availability dates of an Order."""
        return association_proxy('order', 'availability')

    customer_approval_date = sa.Column(AwareDateTime(),
                                       nullable=True,
                                       index=True,
                                       info={
                                           'colanderalchemy': {
                                               'title':
                                               'Customer approval date.',
                                               'missing': colander.drop,
                                               'typ': colander.DateTime
                                           }
                                       })
    """Last Accept/Refusal date for the parent order."""

    def _update_dates_from_history(self, keep_updated_at: bool = False):
        """Update dates from history."""
        updated_at = self.updated_at
        state_history = self.state_history

        def number_of_transitions(transition_name):
            """Return the number of times one transition happened."""
            total = [
                t for t in state_history if t['transition'] == transition_name
            ]
            return len(total)

        # updated refused times
        self.refused_times = number_of_transitions('refuse')

        def updated_if_changed(attr, t_list, first=False):
            """Update only if changed."""
            existing = getattr(self, attr)
            new = get_transition_date_from_history(t_list,
                                                   state_history,
                                                   first=first)
            if new != existing:
                setattr(self, attr, new)

        transitions = (
            'assign',
            'self_assign',
            'assign_pool',
        )
        updated_if_changed('assignment_date', transitions, False)

        transitions = (
            'approve',
            'reject',
        )
        updated_if_changed('last_approval_date', transitions, False)

        transitions = ('upload', )
        updated_if_changed('submission_date', transitions, True)
        updated_if_changed('last_submission_date', transitions, False)

        transitions = ('approve', 'refuse')
        updated_if_changed('customer_approval_date', transitions, False)

        if keep_updated_at:
            self.updated_at = updated_at

    @sautils.observes('order_id')
    def _order_id_observer(self, order_id):
        """Update path when order id changes."""
        if order_id:
            order = Item.get(order_id)
            self.path = order.path + [self.id]

    # Relevant dates
    @sautils.observes('state')
    def dates_observer(self, state) -> datetime:
        """Calculate dates on a change of a state."""
        # Update all dates
        self._update_dates_from_history()

    @property
    def closed_on_date(self) -> datetime:
        """Return the date of the closing info for this assignment."""
        state_history = self.state_history
        transitions = ('approve', 'perm_reject', 'cancel')
        return get_transition_date_from_history(transitions, state_history)

    @property
    def external_state(self) -> str:
        """Return the external state for this assignment."""
        state_history = self.state_history
        state = self.state
        set_type = self.set_type
        last_transition = state_history[-1]
        if state == 'in_qa' and set_type == 'refused_customer':
            state = 'in_qa_further_revision'
        elif state == 'awaiting_assets':
            if last_transition == 'reject':
                state = 'awaiting_assets_edit_needed'
            elif last_transition == 'invalidate_assets':
                state = 'awaiting_assets_technical_error'
        return state

    @property
    def last_transition_message(self) -> str:
        """Return the message from the most recent transition."""
        message = ''
        state_history = self.state_history
        last_transition = state_history[-1]
        if last_transition:
            message = last_transition['message']
        return message

    @property
    def briefing(self) -> str:
        """Return the briefing URL for the parent project."""
        return self.order.project.briefing

    @property
    def assigned(self) -> bool:
        """Return if this Assignment is assigned or not."""
        return True if (self.assignment_date
                        and self.professional_id) else False

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_summary_dict(self) -> dict:
        """Return a summarized version of the dict representation of this Class.

        Used to serialize this object within a parent object serialization.
        :returns: Dictionary with fields and values used by this Class
        """
        data = super().to_summary_dict()
        data['category'] = self.category
        return data

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_listing_dict(self) -> dict:
        """Return a summarized version of the dict representation of this Class.

        Used to serialize this object within a parent object serialization.
        :returns: Dictionary with fields and values used by this Class
        """
        data = super().to_listing_dict()
        data['set_type'] = self.set_type
        data['category'] = self.category
        data['external_state'] = self.external_state
        data = self._apply_actors_info(data)
        return data

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_dict(self, excludes: list = None, includes: list = None):
        """Return a dict representation of this object."""
        data = super().to_dict(excludes=excludes, includes=includes)

        if data['project']:
            # Project delivery data used on the 'approve' transition
            # to deliver assets. (copying over and renaming - takes place
            # on ms.laure)
            data['project']['delivery'] = self.project.delivery

        # Workflow history
        if includes and 'state_history' in includes:
            add_user_info_to_state_history(self.state_history)

        # Apply actor information to data
        data['internal_pm'] = self.order.project.internal_pm
        data = self._apply_actors_info(data, additional_actors=['internal_pm'])
        return data
示例#24
0
class LocalRoleDeprecated(Identifiable, Timestamp, Base):
    """Local role support for Briefy."""

    __tablename__ = 'localroles_deprecated'

    entity_type = sa.Column(
        sa.String(255),
        index=True,
        nullable=False
    )
    """Entity type.

    Name of the entity -- as in its classname.
    """

    entity_id = sa.Column(
        sautils.UUIDType(),
        nullable=False,
        index=True,
        info={
            'colanderalchemy': {
                'title': 'Entity ID',
                'validator': colander.uuid,
                'typ': colander.String
            }
        }
    )
    """Entity ID.

    ID of the entity that will receive this Local Role.
    """

    user_id = sa.Column(
        sautils.UUIDType(),
        nullable=False,
        index=True,
        info={
            'colanderalchemy': {
                'title': 'User ID',
                'validator': colander.uuid,
                'typ': colander.String
            }
        }
    )
    """User ID.

    User ID assigned the local role here.
    """

    role_name = sa.Column(
        sautils.ChoiceType(LocalRolesChoices, impl=sa.String()),
        nullable=False,
        index=True,
    )
    """Local role name.

    i.e: project_manager
    """

    can_create = sa.Column(
        sa.Boolean(),
        nullable=False,
        default=False,
        index=True
    )
    """Boolean indicating a user can create sub-objects."""

    can_delete = sa.Column(
        sa.Boolean(),
        nullable=False,
        default=False,
        index=True
    )
    """Boolean indicating a user can delete this object."""

    can_edit = sa.Column(
        sa.Boolean(),
        nullable=False,
        default=False,
        index=True
    )
    """Boolean indicating a user can update this object."""

    can_list = sa.Column(
        sa.Boolean(),
        nullable=False,
        default=False,
        index=True
    )
    """Boolean indicating a user can list this object."""

    can_view = sa.Column(
        sa.Boolean(),
        nullable=False,
        default=False,
        index=True
    )
    """Boolean indicating a user can read this object."""

    def __repr__(self) -> str:
        """Representation of this object."""
        return (
            """<{0}(id='{1}' entity='{2}' entity_id='{3}' user_id='{4}' role='{5}')>""").format(
                self.__class__.__name__,
                self.id,
                self.entity_type,
                self.entity_id,
                self.user_id,
                self.role_name.value
        )
示例#25
0
class Chore(db.Model):
    __tablename__ = 'chores'

    assignees = [
        ('0', 'Alex'),
        ('1', 'Marissa'),
        ('2', 'Rodney'),
    ]

    cadences = [
        ('0', 'Weekly'),
        ('1', 'Monthly'),
        ('2', 'Every Two Weeks'),
        ('3', 'Every Two Months'),
        ('4', 'Every Three Months'),
    ]

    id = sa.Column(sau.UUIDType(binary=False),
                   primary_key=True,
                   default=uuid.uuid4,
                   unique=True,
                   nullable=False)
    name = sa.Column(sa.String(80), nullable=False)
    assignee = sa.Column(sau.ChoiceType(assignees), nullable=False)
    cadence = sa.Column(sau.ChoiceType(cadences), nullable=False)
    next_due_date = sa.Column(sa.Date, nullable=False)

    @staticmethod
    def validate_missing(data):
        required = ['name', 'assignee', 'cadence', 'next_due_date']
        return [f for f in required if not data.get(f)]

    @staticmethod
    def validate_values(data):
        invalid, clean = [], {}

        name = data.get('name', '')
        if len(name) > 80:
            invalid.append('name is greater than 80 chars')
        else:
            clean['name'] = name

        try:
            assignee = str(int(data.get('assignee')))
            valid_assignees = [a[0] for a in Chore.assignees]
            if assignee not in valid_assignees:
                invalid.append(f'assignee not one of {valid_assignees}')
            else:
                clean['assignee'] = assignee
        except (TypeError, ValueError):
            invalid.append('assignee is not an integer')

        try:
            cadence = str(int(data.get('cadence')))
            valid_cadences = [a[0] for a in Chore.cadences]
            if cadence not in valid_cadences:
                invalid.append(f'cadence not one of {valid_cadences}')
            else:
                clean['cadence'] = cadence
        except (TypeError, ValueError):
            invalid.append('cadence is not an integer')

        try:
            clean['next_due_date'] = datetime.datetime.strptime(
                data.get('next_due_date', ''), '%Y-%m-%d')
        except ValueError:
            invalid.append('next_due_date is not in %Y-%m-%d format')

        return invalid, clean

    @staticmethod
    def validate(data):
        missing = Chore.validate_missing(data)
        invalid, clean = Chore.validate_values(data)
        return missing, invalid, clean

    def toJsonSafe(self):
        return {
            'id': str(self.id),
            'name': self.name,
            'assignee': str(self.assignee.code),
            'cadence': str(self.cadence.code),
            'next_due_date': str(self.next_due_date),
        }

    @property
    def next_due_delta(self):
        cadence = int(self.cadence.code)

        if cadence == 0:
            return relativedelta(days=7)
        elif cadence == 1:
            return relativedelta(months=+1)
        elif cadence == 2:
            return relativedelta(days=+14)
        elif cadence == 3:
            return relativedelta(months=+2)
        elif cadence == 4:
            return relativedelta(months=+3)
        else:
            raise ValueError(f'unexpected cadence type {self.cadence}')

    def find_next_due_date(self):
        today = datetime.datetime.now().date()
        next_due = copy.copy(self.next_due_date)

        if next_due > today:  # eager beaver, advance into future
            return next_due + self.next_due_delta
        else:  # slacker, keep advancing until caught up
            while not (next_due > today):
                next_due += self.next_due_delta

            return next_due
示例#26
0
class CustomerBillingInfo(BillingInfo):
    """Billing information for a Professional."""

    __tablename__ = 'customer_billing_infos'
    _workflow = workflows.CustomerBillingInfoWorkflow

    __colanderalchemy_config__ = {
        'excludes': ['state_history', 'state', 'type', 'customer']
    }

    __exclude_attributes__ = ['customer']

    __raw_acl__ = (
        ('create', ('g:briefy_bizdev', 'g:briefy_finance', 'g:system')),
        ('list', ('g:briefy', 'g:system')),
        ('view', ('g:briefy', 'g:system')),
        ('edit', ('g:briefy_bizdev', 'g:briefy_finance', 'g:system')),
        ('delete', ('g:briefy_finance', 'g:system')),
    )

    id = sa.Column(UUIDType(),
                   sa.ForeignKey('billing_infos.id'),
                   index=True,
                   unique=True,
                   primary_key=True,
                   info={
                       'colanderalchemy': {
                           'title': 'Billing Info id',
                           'validator': colander.uuid,
                           'missing': colander.drop,
                           'typ': colander.String
                       }
                   })

    customer_id = sa.Column(UUIDType(),
                            sa.ForeignKey('customers.id'),
                            index=True,
                            unique=True,
                            info={
                                'colanderalchemy': {
                                    'title': 'Customer id',
                                    'validator': colander.uuid,
                                    'missing': colander.drop,
                                    'typ': colander.String
                                }
                            })

    customer = orm.relationship('Customer')

    tax_id_status = sa.Column(sautils.ChoiceType(TaxIdStatusCustomers,
                                                 impl=sa.String(3)),
                              nullable=True,
                              info={
                                  'colanderalchemy': {
                                      'title': 'Tax ID Status',
                                      'missing': colander.drop,
                                      'typ': colander.String
                                  }
                              })
    """Tax ID Status.

    Internal codes used by Finance to determine tax rates to be applied to this customer.
    """
    @hybrid_property
    def legal_name(self) -> str:
        """Company legal name.

        :return: legal name from title.
        """
        return self.title

    @legal_name.comparator
    def legal_name(cls) -> LegalNameComparator:
        """Billing address legal_name comparator.

        :return: _title class attribute.
        """
        return LegalNameComparator(cls)
示例#27
0
class Project(CommercialInfoMixin, mixins.ProjectRolesMixin,
              mixins.LeicaSubMixin, Item):
    """A Project in Briefy."""

    _workflow = workflows.ProjectWorkflow

    __summary_attributes__ = [
        'id', 'title', 'description', 'created_at', 'updated_at', 'state',
        'slug', 'asset_types', 'order_type', 'project_type'
    ]

    __summary_attributes_relations__ = ['customer', 'pool', 'customer_users']

    __listing_attributes__ = __summary_attributes__ + [
        'total_orders', 'total_leadorders', 'customer', 'category',
        'internal_pm'
    ]

    __exclude_attributes__ = ['orders', 'leadorders']

    __to_dict_additional_attributes__ = [
        'price', 'total_orders', 'total_leadorders'
    ]

    __raw_acl__ = (
        ('create', ('g:briefy_pm', 'g:briefy_bizdev', 'g:briefy_finance',
                    'g:system')),
        ('list', ('g:briefy_qa', 'g:briefy_bizdev', 'g:briefy_scout',
                  'g:briefy_finance', 'g:system')),
        ('view', ('g:briefy_qa', 'g:briefy_bizdev', 'g:briefy_scout',
                  'g:briefy_finance', 'g:system')),
        ('edit', ('g:briefy_pm', 'g:briefy_bizdev', 'g:briefy_finance',
                  'g:system')),
        ('delete', ('g:briefy_finance', 'g:system')),
    )

    __colanderalchemy_config__ = {
        'excludes': [
            'state_history',
            'state',
            'customer',
            'pool',
        ],
        'overrides':
        mixins.ProjectRolesMixin.__colanderalchemy_config__['overrides']
    }

    __parent_attr__ = 'customer_id'

    customer_id = sa.Column(sautils.UUIDType,
                            sa.ForeignKey('customers.id'),
                            index=True,
                            nullable=False,
                            info={
                                'colanderalchemy': {
                                    'title': 'Customer',
                                    'validator': colander.uuid,
                                    'typ': colander.String
                                }
                            })
    """Customer ID.

    Builds the relation with :class:`briefy.leica.models.customer.Customer`.
    """

    customer_users = orm.relationship(
        'CustomerUserProfile',
        primaryjoin='and_('
        'foreign(CustomerUserProfile.customer_id)==Project.customer_id,'
        'LocalRole.principal_id==foreign(CustomerUserProfile.id),'
        'LocalRole.item_id==Project.id)',
        lazy='dynamic',
        info={
            'colanderalchemy': {
                'title': 'Customer User Profiles',
                'missing': colander.drop,
            }
        })
    """List of customer user profiles connected to this project.

    Returns a collection of :class:`briefy.leica.models.user.CustomerUserProfile`.
    """

    abstract = sa.Column('abstract',
                         sa.Text,
                         nullable=True,
                         info={
                             'colanderalchemy': {
                                 'title': 'Abstract',
                                 'missing': colander.drop,
                                 'typ': colander.String
                             }
                         })
    """Abstract for a project.

    Text field allowing a small, but meaningful description for an object.
    Used to store Bizdev comments.
    """

    order_type = sa.Column(sautils.ChoiceType(OrderTypeChoices,
                                              impl=sa.String()),
                           default='order',
                           nullable=False,
                           info={
                               'colanderalchemy': {
                                   'title': 'Type of Order',
                                   'missing': colander.drop,
                                   'typ': colander.String
                               }
                           })
    """Type of order the project support."""

    project_type = sa.Column(sautils.ChoiceType(ProjectTypeChoices,
                                                impl=sa.String()),
                             default='on-demand',
                             nullable=False,
                             info={
                                 'colanderalchemy': {
                                     'title': 'Type of Project',
                                     'missing': colander.drop,
                                     'typ': colander.String
                                 }
                             })
    """Type of package the project support."""

    leadorder_confirmation_fields = sa.Column(
        JSONB,
        default=['availability'],
        nullable=True,
        info={
            'colanderalchemy': {
                'title': 'Fieldnames required to confirm a LeadOrder.',
                'missing': colander.drop,
                'typ': schema.List()
            }
        })
    """List with fieldnames required to confirm a LeadOrder."""

    number_required_assets = sa.Column(sa.Integer(), default=10)
    """Number of required assets of a Project to be used in the Order as default value."""

    asset_types = sa.Column(JSONB,
                            info={
                                'colanderalchemy': {
                                    'title': 'Asset types.',
                                    'missing': colander.drop,
                                    'typ': schema.List()
                                }
                            })
    """Asset types supported by this project.

    Options come from :mod:`briefy.leica.vocabularies.AssetTypes`.
    """
    @orm.validates('asset_types')
    def validate_asset_types(self, key, value):
        """Validate if values for asset_types are correct."""
        members = AssetTypes.__members__
        for item in value:
            if item not in members:
                raise ValidationError(message='Invalid type of asset',
                                      name=key)
        return value

    category = sa.Column(sautils.ChoiceType(CategoryChoices, impl=sa.String()),
                         default='undefined',
                         nullable=False)
    """Category of this Project.

    Options come from :mod:`briefy.common.vocabularies.categories`.
    """

    tech_requirements = sa.Column(
        JSONB,
        default=dict,
        info={
            'colanderalchemy': {
                'title': 'Technical Requirements for this project.',
                'missing': colander.drop,
                'typ': schema.JSONType
            }
        })
    """Technical requirements for orders in this project.

    It stores a dictionary of requirements to be fulfilled by each asset of each Assignment.

    i.e.  - for a project delivering only photos, its value might be::

        [
            {
                "asset_type": "Image",
                "set": {
                    "minimum_number": 10  # (aliased to 'minimum_number_of_photos' (deprecated))
                },
                "asset": {
                    "dimensions": [
                    {
                        "value": "4000x3000",
                        "operator": "min"
                    }
                    ],
                    "orientation": [
                    {
                        "value": "landscape",
                        "operator": "eq"
                    }
                    ]
                }
            },
            {
                "asset_type": "Video",
                "set": {
                    "minimum_number": 2
                },
                "asset": {
                    "duration": {"value": "30", "operator" :"min"}
                },
                "actions": [
                    {
                        "state": "post_processing",
                        "action": "copy",
                        "settings": {
                            "driver": "gdrive",
                            "parentId": "",
                            "subfolders": true,
                            "images": true,
                            "other": true,
                            "name": "order.customer_order_id",
                            "resize": []
                        }
                    },
                    ...
                ]
            },
            ...
        ]


    If there is a single asset type for the project, the outermost list may be omitted -
    and a single copy of the inner dictionary is used. The inner dictionary should
    have the keys "asset_type", "set" for validation constraints that apply to
    all assets of that type   taken together,
    and "asset" for denoting constraints for each asset of that type.

    (Deprecated: for compatibility reasons, ms.laure code will understand a
    missing "asset_type" key will default it to "Image".)
    """

    delivery = sa.Column(JSONB,
                         default=DEFAULT_DELIVERY_CONFIG,
                         info={
                             'colanderalchemy': {
                                 'title':
                                 'Delivery information for this project.',
                                 'missing': colander.drop,
                                 'typ': schema.JSONType
                             }
                         })
    """Delivery configuration for orders in this project.

    It stores a dictionary of configurations to be used by the delivery mechanism.

    i.e::

        {
          "approve": {
            "archive": {
              "driver": "gdrive",
              "parentId": "",
              "subfolders": true,
              "images": true,
              "other": true,
              "name": "order.customer_order_id",
              "resize": []
            },
            "gdrive": {
              "driver": "gdrive",
              "parentId": "",
              "subfolders": false,
              "images": true,
              "other": false,
              "name": "order.customer_order_id",
              "resize": []
            },
          },
          "accept": {
            "sftp": {
              "driver": "sftp",
              "subfolders": false,
              "images": true,
              "other": false,
              "name": "order.customer_order_id",
              "resize": [
                {"name": "resized", "filter": "maxbytes": 4000000}
              ]
            }
          }
        }

    """

    cancellation_window = sa.Column(sa.Integer, default=1)
    """Period, in hours, before the shooting, an Assignment can be cancelled.

    i.e.: 24 would mean an Assignment in this project could be cancelled with
    at least 24 hour notice. Zero means no cancellation is possible.
    """

    availability_window = sa.Column(sa.Integer, default=6)
    """Period, in days, an availability date can be inputed.

    i.e.: 6 would mean an Order would have availability dates for, at least, 6 days in the future.
    Zero means no check is done.
    """

    approval_window = sa.Column(sa.Integer, default=5)
    """Period (business days), after the delivery, an Order has will be automatic accepted.

    If an Order is delivered and not rejected by customer it will be automatic accepted by a task.
    i.e.: 10 would mean an Order in this project could be approved up to 10 days after its delivery.
    Zero means a Order will be automatically approved.
    """

    add_order_roles = sa.Column(JSONB,
                                default=DEFAULT_ADD_ORDER_ROLES,
                                info={
                                    'colanderalchemy': {
                                        'title':
                                        'Roles allowed to add an order.',
                                        'missing': colander.drop,
                                        'typ': schema.List()
                                    }
                                })
    """Roles allowed to add orders on this project.

    Options come from :mod:`briefy.common.vocabularies.roles.Groups`.
    """

    @orm.validates('add_order_roles')
    def validate_add_order_roles(self, key, value):
        """Validate if values for add_order_roles are correct."""
        all_groups = [item.value for item in Groups]
        for item in value:
            if item not in all_groups:
                raise ValidationError(message='Invalid role', name=key)
        return value

    @property
    def settings(self) -> Objectify:
        """Project settings.

        Aggregate settings information about a project.
        :return: Dictionary with all settings for a project.
        """
        # TODO: These settings are in a transitional state while
        # we move other configuration-related fields here.
        # To preserve backwards compability, we simply proxy those
        # fields - but their use should be deprecated as possible.

        # (NB. Even with Objectify, there is no provision
        # for write-back any of the "dates" subfields yet)

        return Objectify({
            'tech_requirements': self.tech_requirements,
            'delivery_config': self.delivery,
            'dates': {
                'cancellation_window': self.cancellation_window,
                'availability_window': self.availability_window,
                'approval_window': self.approval_window,
            },
            'permissions': {
                'add_order': self.add_order_roles
            },
            'order_type': self.order_type,
            'project_type': self.project_type
        })

    @settings.setter
    def settings(self, value: t.Union[Objectify, t.Mapping]):
        """Project settings.

        Set all settings for a project.
        :value: Dictionary with all settings for a project.
        """
        # 'PUT' semmantics setter. This will destroy everything on the way.
        # to change a single sub-field, consider changing the just the desired
        # entry along with a call to
        # sqlalchemy.orm.attributes.flag_modified(obj, data_field_name)
        # (check the correct underlying field_name on the settings.getter
        # above while we are in this transitional stage)

        value = Objectify(value, sentinel=None)
        self.tech_requirements = value.tech_requirements or {}
        self.delivery = value.delivery_config or {}
        self.add_order_roles = value.permissions.add_order or []
        self.cancellation_window = value.dates.cancellation_window or 0
        self.availability_window = value.dates.availability_window or 0
        self.approval_window = value.dates.approval_window or 0

    orders = orm.relationship('Order',
                              foreign_keys='Order.project_id',
                              primaryjoin="""and_(
            Order.current_type=='order',
            foreign(Order.project_id)==Project.id,
        )""",
                              lazy='dynamic')
    """List of Orders of this project.

    Returns a collection of :class:`briefy.leica.models.job.order.Order`.
    """

    leadorders = orm.relationship('LeadOrder',
                                  foreign_keys='LeadOrder.project_id',
                                  primaryjoin="""and_(
            LeadOrder.current_type=='leadorder',
            foreign(LeadOrder.project_id)==Project.id,
        )""",
                                  lazy='dynamic')
    """List of LeadOrders of this project.

    Returns a collection of :class:`briefy.leica.models.job.leadorder.LeadOrder`.
    """

    @hybrid_property
    def total_orders(self) -> int:
        """Return the Project total number of orders."""
        return self.orders.count()

    @total_orders.expression
    def total_orders(cls) -> int:
        """Return the Project total number of orders."""
        return sa.func.count(cls.orders)

    @hybrid_property
    def total_leadorders(self) -> sa.sql.func:
        """Return the Project total number of leadorders."""
        return self.leadorders.count()

    @total_leadorders.expression
    def total_leadorders(cls) -> sa.sql.func:
        """Return the Project total number of leadorders."""
        return sa.func.count(cls.leadorders)

    # Formerly known as brief
    briefing = sa.Column(sautils.URLType,
                         nullable=True,
                         info={
                             'colanderalchemy': {
                                 'title': 'Briefing link',
                                 'validator': colander.url,
                                 'missing': colander.drop,
                                 'typ': colander.String
                             }
                         })
    """Path to briefing file regarding this Project."""

    release_template = sa.Column(sautils.URLType,
                                 nullable=True,
                                 info={
                                     'colanderalchemy': {
                                         'title': 'Release template',
                                         'validator': colander.url,
                                         'missing': colander.drop,
                                         'typ': colander.String
                                     }
                                 })
    """Path to release template file."""

    pool_id = sa.Column(sautils.UUIDType,
                        sa.ForeignKey('pools.id'),
                        index=True,
                        nullable=True,
                        info={
                            'colanderalchemy': {
                                'title': 'Pool ID',
                                'validator': colander.uuid,
                                'missing': colander.drop,
                                'typ': colander.String
                            }
                        })
    """Pool ID.

    Relationship between a project and a Pool.
    """

    @sautils.observes('customer_id')
    def _customer_id_observer(self, customer_id):
        """Update path when customer id changes."""
        if customer_id:
            customer = Item.get(customer_id)
            self.path = customer.path + [self.id]

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_summary_dict(self) -> dict:
        """Return a summarized version of the dict representation of this Class.

        Used to serialize this object within a parent object serialization.
        :returns: Dictionary with fields and values used by this Class
        """
        data = super().to_summary_dict()
        return data

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_listing_dict(self) -> dict:
        """Return a summarized version of the dict representation of this Class.

        Used to serialize this object within a parent object serialization.
        :returns: Dictionary with fields and values used by this Class
        """
        data = super().to_listing_dict()
        data = self._apply_actors_info(data)
        return data

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_dict(self, excludes: list = None, includes: list = None):
        """Return a dict representation of this object."""
        data = super().to_dict(excludes=excludes, includes=includes)
        data['settings'] = self.settings._get()
        if includes and 'state_history' in includes:
            # Workflow history
            add_user_info_to_state_history(self.state_history)
        # Apply actor information to data
        data = self._apply_actors_info(data)
        return data
示例#28
0
class Order(mixins.OrderFinancialInfo, mixins.LeicaSubVersionedMixin,
            mixins.OrderRolesMixin, Item):
    """An Order from the customer."""

    _workflow = workflows.OrderWorkflow

    __summary_attributes__ = __summary_attributes__
    __summary_attributes_relations__ = [
        'assignment', 'assignments', 'customer', 'project', 'location'
    ]
    __listing_attributes__ = __listing_attributes__

    __exclude_attributes__ = ['comments']

    __to_dict_additional_attributes__ = [
        'availability', 'delivery', 'tech_requirements', 'price'
    ]

    __raw_acl__ = (
        ('create', ('g:briefy_pm', 'g:briefy_finance', 'g:briefy_bizdev',
                    'g:system')),
        ('list', ('g:briefy_qa', 'g:briefy_bizdev', 'g:briefy_scout',
                  'g:briefy_finance', 'g:system')),
        ('view', ('g:briefy_qa', 'g:briefy_bizdev', 'g:briefy_scout',
                  'g:briefy_finance', 'g:system')),
        ('edit', ('g:briefy_pm', 'g:briefy_finance', 'g:briefy_bizdev',
                  'g:system')),
        ('delete', ('g:briefy_finance', 'g:system')),
    )

    __colanderalchemy_config__ = {
        'excludes': [
            'state_history', 'state', 'project', 'comments', 'customer',
            'type', '_project_manager', '_scout_manager', '_customer_user',
            'assignment', 'assignments', '_project_managers',
            '_scout_managers', '_customer_users', 'total_order_price'
        ],
        'overrides':
        __colander_alchemy_config_overrides__
    }

    __versioned__ = {
        'exclude':
        ['state_history', '_state_history', 'scheduled_datetime', 'timezone']
    }
    """SQLAlchemy Continuum settings.

    By default we do not keep track of state_history and helper columns.
    """

    __parent_attr__ = 'project_id'

    current_type = sa.Column(
        sa.String(50),
        index=True,
        default=default_current_type,
    )
    """Type of the Order during its life cycle."""
    @classmethod
    def create(cls, payload: dict) -> 'Item':
        """Factory that creates a new instance of this object.

        :param payload: Dictionary containing attributes and values
        :type payload: dict
        """
        payload['slug'] = create_order_slug()
        return super().create(payload)

    @Item.slug.setter
    def slug(self, value: str):
        """Set a new slug for this object.

        Generate a slug using :func:`create_order_slug`
        :param value: Value of the new slug, if passed, will raise an Exception.
        """
        if value and self.slug:
            raise Exception('Order slug should never be updated.')
        elif value:
            self._slug = value

    customer_order_id = sa.Column(sa.String,
                                  default='',
                                  index=True,
                                  info={
                                      'colanderalchemy': {
                                          'title': 'Customer Order ID',
                                          'typ': colander.String
                                      }
                                  })
    """Customer Order ID.

    Reference for the customer to find this order. On Knack this field was refered as 'job_id'
    """

    job_id = sa.Column(sa.String,
                       nullable=True,
                       index=True,
                       info={
                           'colanderalchemy': {
                               'title': 'Internal Job ID (deprecated)',
                               'missing': colander.drop,
                               'typ': colander.String
                           }
                       })
    """Order ID was the main Briefy id for an Order.

    This field was used on Knack as an auto-incremented field named 'internal_job_id'.
    """

    # Customer
    customer_id = sa.Column(sautils.UUIDType,
                            sa.ForeignKey('customers.id'),
                            index=True,
                            default=get_customer_id_from_project,
                            nullable=False,
                            info={
                                'colanderalchemy': {
                                    'title': 'Customer ID',
                                    'validator': colander.uuid,
                                    'missing': None,
                                    'typ': colander.String
                                }
                            })
    """Customer ID.

    Relationship with :class:`briefy.leica.models.customer.Customer`.
    """

    # Project
    project_id = sa.Column(sautils.UUIDType,
                           sa.ForeignKey('projects.id'),
                           index=True,
                           nullable=False,
                           info={
                               'colanderalchemy': {
                                   'title': 'Project ID',
                                   'validator': colander.uuid,
                                   'missing': None,
                                   'typ': colander.String
                               }
                           })
    """Project ID.

    Relationship with :class:`briefy.leica.models.project.Project`.
    """

    project = orm.relationship('Project', foreign_keys='Order.project_id')
    """Project.

    Relationship with :class:`briefy.leica.models.project.Project`.
    """

    category = sa.Column(sautils.ChoiceType(CategoryChoices, impl=sa.String()),
                         default=get_category_from_project,
                         nullable=False)
    """Category of this Order.

    Options come from :mod:`briefy.common.vocabularies.categories`.
    """

    source = sa.Column(sautils.ChoiceType(OrderInputSource, impl=sa.String()),
                       default='briefy',
                       nullable=False)
    """Source of this Order.

    This field stores which part created this Order, Customer or Briefy.
    Options come from :mod:`briefy.leica.vocabularies`.
    """

    refused_times = sa.Column(sa.Integer(), default=0)
    """Number times the Order was refused."""

    requirement_items = sa.Column(JSONB,
                                  nullable=True,
                                  info={
                                      'colanderalchemy': {
                                          'title': 'Requirement Items',
                                          'typ': colander.List,
                                          'missing': None,
                                      }
                                  })
    """Structured list of specific with all the requirement by category.

    This list of maps have the following structure:

        [
            {
                "id": "18a1d349-e432-4ca9-9617-81bb005d26bc",
                "category": "Room 47",
                "min_number_assets": 15,
                "description": "Shoot the television and the bed",
                "tags": ["room", "luxury", "double"]
            },
            {
                "id": "33261769-20e6-4ec6-b793-d1147de98a90",
                "category": "Bathroom 47",
                "min_number_assets": 15,
                "description": "",
                "tags": ["bathroom"]
            },
            {
                "id": "48401f9a-a243-4606-a29c-cb1514fae722",
                "category": "Room 32",
                "min_number_assets": 15,
                "description": "Shoot the ceiling",
                "tags": ["room", "standard", "single"]
            },
        ]
    """

    @orm.validates('requirement_items')
    def validate_requirement_items(self, key: str,
                                   values: t.Sequence) -> t.Sequence:
        """Validate if requirement_items payload is in the correct format.

        :param key: Attribute name.
        :param values: Requirement items payload.
        :return: Validated payload.
        """
        request = self.request
        user_id = str(request.user.id) if request else None
        current_value = list(
            self.requirement_items) if self.requirement_items else []
        if values:
            for item in values:
                if not item.get('created_by') and user_id:
                    item['created_by'] = user_id
                if not item.get('created_at'):
                    item['created_at'] = datetime_utcnow().isoformat()

        if values or current_value:
            requirements_schema = RequirementItems()
            try:
                values = requirements_schema.deserialize(values)
            except colander.Invalid as exc:
                raise ValidationError(
                    message='Invalid payload for requirement_items', name=key)

        return values

    number_required_assets = sa.Column('number_required_assets',
                                       sa.Integer(),
                                       default=10)
    """Number of required assets of an Order."""

    @orm.validates('number_required_assets')
    def validate_number_required_assets(self, key: str, value: int) -> int:
        """Validate number_required_assets checking if the order is using requirement_items.

        :param key: Attribute name.
        :param value: Number of required assets value.
        :return: Number of required after validation.
        """
        if value and self.requirement_items:
            logger.warn(
                'Number of required assets will not be set when using requirement items.'
            )

        if self.requirement_items:
            value = 0
            for item in self.requirement_items:
                value += item.get('min_number_assets')

        return value

    requirements = sa.Column('requirements', sa.Text, default='')
    """Human-readable requirements for an Order."""

    @orm.validates('requirements')
    def validate_requirements(self, key: str, value: str) -> str:
        """Validate requirements checking if the order is using requirement_items.

        :param key: Attribute name.
        :param value: Requirements value.
        :return: Requirements after validation.
        """
        if value or self.requirement_items:
            logger.warn(
                'Requirements will not be set when using requirement items.')

        if self.requirement_items:
            value = ''
            for item in self.requirement_items:
                category = item.get('category')
                description = item.get('description')
                min_number_assets = item.get('min_number_assets')
                value += f'Category: {category}: {min_number_assets}\n' \
                         f'Description: {description}\n\n'

        return value

    actual_order_price = sa.Column('actual_order_price',
                                   sa.Integer,
                                   nullable=True,
                                   default=default_actual_order_price,
                                   info={
                                       'colanderalchemy': {
                                           'title': 'Actual Order Price',
                                           'missing': None,
                                           'typ': colander.Integer
                                       }
                                   })
    """Price to be paid, by the customer, for this order.

    Amount to be paid by the customer for this order.
    For Orders this value will be the same of Order.price, on creation.
    For LeadOrders this value will be 0.

    This value is expressed in cents.
    """

    additional_charges = sa.Column(JSONB,
                                   nullable=True,
                                   info={
                                       'colanderalchemy': {
                                           'title': 'Additional Charges',
                                           'typ': colander.List,
                                           'missing': None,
                                       }
                                   })
    """Additional charges to be applied to this Order."""

    @orm.validates('additional_charges')
    def validate_additional_charges(self, key: str,
                                    value: t.Sequence) -> t.Sequence:
        """Validate if additional_charges payload is in the correct format.

        :param key: Attribute name.
        :param value: Additional charges payload.
        :return: Validated payload
        """
        current_value = list(
            self.additional_charges) if self.additional_charges else []
        if value or current_value:
            charges_schema = OrderCharges()
            try:
                new_value = charges_schema.deserialize(value)
            except colander.Invalid as e:
                raise ValidationError(
                    message='Invalid payload for additional_charges', name=key)

            value = order_charges_update(current_value, new_value)

            # Force total_order_price recalculation
            flag_modified(self, 'actual_order_price')
            flag_modified(self, 'additional_charges')
        return value

    @property
    def total_additional_charges(self) -> int:
        """Return the total of additional charges."""
        total = 0
        additional_charges = self.additional_charges
        if additional_charges:
            for charge in additional_charges:
                total += charge['amount']
        return total

    total_order_price = sa.Column(sa.Integer,
                                  nullable=True,
                                  default=default_actual_order_price,
                                  info={
                                      'colanderalchemy': {
                                          'title': 'Total Order Price',
                                          'missing': None,
                                          'typ': colander.Integer
                                      }
                                  })
    """Total price to be paid, by the customer, for this order.

    Total amount to be paid by the customer for this order.
    This value is a sum of actual_order_price and all additional_charges for this Order.

    This value is expressed in cents.
    """

    # Calculate total_order_price
    @sautils.observes('actual_order_price')
    def _calculate_total_order_price(self, actual_order_price: int):
        """Calculate the total order price.

        :param actual_order_price: Order price to be charged to the customer.
        """
        actual_order_price = actual_order_price if actual_order_price else 0
        total_additional_charges = self.total_additional_charges
        self.total_order_price = actual_order_price + total_additional_charges

    _location = orm.relationship('OrderLocation',
                                 uselist=False,
                                 backref=orm.backref('order'),
                                 info={
                                     'colanderalchemy': {
                                         'title': 'Location',
                                         'missing': colander.drop
                                     }
                                 })
    """Order Location.

    Relationship with :class:`briefy.leica.models.job.location.OrderLocation`.
    """

    location = UnaryRelationshipWrapper('_location', OrderLocation, 'order_id')
    """Descriptor to handle location get, set and delete."""

    # Assignments
    assignments = orm.relationship('Assignment',
                                   foreign_keys='Assignment.order_id',
                                   order_by='asc(Assignment.created_at)',
                                   backref=orm.backref('order'))
    """Assignments.

    Relationship with :class:`briefy.leica.models.job.Assignment`.
    """

    assignment = orm.relationship(
        'Assignment',
        uselist=False,
        viewonly=True,
        order_by='desc(Assignment.created_at)',
        backref=orm.backref('active_order'),
        primaryjoin="""and_(
            Order.id == Assignment.order_id,
            not_(Assignment.state.in_(('cancelled', 'perm_reject')))
        )""",
    )
    """Current Assignment connect to this order.

    Collection of :class:`briefy.leica.models.job.Assignment`.
    """
    """ # TODO: enable this!
    _tech_requirements = sa.Column(
        sautils.JSONType,
        info={
            'colanderalchemy': {
                'title': 'Technical Requirements for this assignemnt.',
                'missing': colander.drop,
                'typ': colander.String
            }
        }
    )
    """
    """Technical requirements for orders in this order.

    It stores a dictionary of requirements to be fulfilled by each asset of each Assignment.
    If missing, the Project's technical requirements are used in its place.
    """

    _availability = sa.Column(
        'availability',
        JSONB,
        info={
            'colanderalchemy': {
                'title': 'Availability for scheduling this Order.',
                'missing': colander.drop,
                'typ': colander.List
            }
        })
    """Availability attribute.

    Access to it should be done using the hybrid_property availability.
    """

    asset_types = sa.Column(JSONB,
                            info={
                                'colanderalchemy': {
                                    'title': 'Asset types.',
                                    'missing': colander.drop,
                                    'typ': schema.List(),
                                }
                            })
    """Asset types supported by this order.

    Options come from :mod:`briefy.leica.vocabularies.AssetTypes`.
    """

    @orm.validates('asset_types')
    def validate_asset_types(self, key, value):
        """Validate if values for asset_types are correct."""
        max_types = 1
        if len(value) > max_types:
            raise ValidationError(message='Invalid number of type of assets',
                                  name=key)
        members = AssetTypes.__members__
        for item in value:
            if item not in members:
                raise ValidationError(message='Invalid type of asset',
                                      name=key)
        return value

    comments = orm.relationship('Comment',
                                foreign_keys='Comment.entity_id',
                                order_by='desc(Comment.created_at)',
                                primaryjoin='Comment.entity_id == Order.id',
                                lazy='dynamic')
    """Comments connected to this order.

    Collection of :class:`briefy.leica.models.comment.Comment`.
    """

    scheduled_datetime = sa.Column(AwareDateTime(),
                                   nullable=True,
                                   index=True,
                                   info={
                                       'colanderalchemy': {
                                           'title':
                                           'Scheduled date for shooting',
                                           'missing': colander.drop,
                                           'typ': colander.DateTime
                                       }
                                   })
    """Scheduled date time of shooting."""

    deliver_date = sa.Column(AwareDateTime(),
                             nullable=True,
                             index=True,
                             info={
                                 'colanderalchemy': {
                                     'title': 'Delivery date',
                                     'missing': colander.drop,
                                     'typ': colander.DateTime
                                 }
                             })
    """Delivery date of this Order."""

    last_deliver_date = sa.Column(AwareDateTime(),
                                  nullable=True,
                                  index=True,
                                  info={
                                      'colanderalchemy': {
                                          'title': 'Delivery date',
                                          'missing': colander.drop,
                                          'typ': colander.DateTime
                                      }
                                  })
    """Last delivery date of this Order."""

    accept_date = sa.Column(AwareDateTime(),
                            nullable=True,
                            index=True,
                            info={
                                'colanderalchemy': {
                                    'title': 'Acceptance date',
                                    'missing': colander.drop,
                                    'typ': colander.DateTime
                                }
                            })
    """Acceptance date of this Order."""

    @hybrid_property
    def availability(self) -> list:
        """Return availability for an Order.

        This should return a list with zero or more available dates for
        scheduling this Order.
        i.e.::

            [
              datetime(2016, 12, 21, 12, 0, 0),
              datetime(2016, 12, 22, 14, 0, 0),
            ]

        """
        availability = self._availability
        return availability

    @availability.setter
    def availability(self, value: list):
        """Set availabilities for an Order."""
        project = self.project
        timezone = self.timezone
        if isinstance(timezone, str):
            timezone = pytz.timezone(timezone)
        user = self.workflow.context
        not_pm = 'g:briefy_pm' not in user.groups if user else True
        validated_value = []

        if value and len(value) != len(set(value)):
            msg = 'Availability dates should be different.'
            raise ValidationError(message=msg, name='availability')

        if value and timezone and project:
            if not_pm:
                availability_window = project.availability_window
            else:
                # allow less than 24hs for PMs but not in the past
                availability_window = 0
            now = datetime.now(tz=timezone)
            for availability in value:
                if isinstance(availability, str):
                    availability = parse(availability)
                tz_availability = availability.astimezone(timezone)
                date_diff = tz_availability - now
                if date_diff.days < availability_window:
                    msg = 'Both availability dates must be at least {window} days from now.'
                    msg = msg.format(window=availability_window)
                    raise ValidationError(message=msg, name='availability')

                validated_value.append(availability.isoformat())
        elif value:
            logger.warn(
                'Could not check availability dates. Order {id}'.format(
                    id=self.id))

        self._availability = validated_value if validated_value else value

    _delivery = sa.Column('delivery',
                          JSONB,
                          info={
                              'colanderalchemy': {
                                  'title': 'Delivery information.',
                                  'missing': colander.drop,
                                  'typ': schema.JSONType
                              }
                          })
    """Delivery links.

    JSON with a collection of delivery links. Should be accessed using the 'delivery' attribute.
    """

    @hybrid_property
    def delivery(self) -> dict:
        """Return delivery info for an Order.

        This should return a dict with the delivery method and URL
        i.e.::

            {
                'sftp': 'sftp://[email protected]/bali/3456/',
                'gdrive': 'https://drive.google.com/foo/bar',
            }

        (Do not confuse this attribute with project.delivery which contains information on
        how to deliver the assets)
        """
        delivery = self._delivery
        return delivery

    @delivery.setter
    def delivery(self, value: dict):
        """Set delivery information for an Order."""
        self._delivery = value
        # Ensure the correct key is updated and object is set as dirty
        flag_modified(self, '_delivery')

    # TODO: If on the future there is the need to override project.delivery
    # for specific orders, create an order.delivery_info JSON field
    # which will override the information on project.delivery
    # There is partial support for an eventual order.delivery.info attribute on ms.laure

    @property
    def assets(self):
        """Return Assets from this Order.

        Collection of :class:`briefy.leica.models.asset.Asset`.
        """
        from briefy.leica.models.asset import Asset
        query = Asset.query().filter(
            Asset.c.job_id.in_(
                [a.id for a in self.assignments if a.state == 'approved']), )
        return query

    tech_requirements = association_proxy('project', 'tech_requirements')
    """Project tech requirements."""

    timezone = sa.Column(TimezoneType(backend='pytz'), default='UTC')
    """Timezone in which this address is located.

    i.e.: UTC, Europe/Berlin
    """
    # Deal with timezone changes
    @sautils.observes('_location.timezone')
    def _timezone_observer(self, timezone):
        """Update timezone on this object."""
        if timezone:
            self.timezone = timezone
            for assignment in self.assignments:
                assignment.timezone = timezone

    @sautils.observes('project_id')
    def _project_id_observer(self, project_id):
        """Update path when project id changes."""
        if project_id:
            project = Item.get(project_id)
            self.path = project.path + [self.id]

    def _update_dates_from_history(self, keep_updated_at: bool = False):
        """Update dates from history."""
        updated_at = self.updated_at
        state_history = self.state_history

        def number_of_transitions(transition_name):
            """Return the number of times one transition happened."""
            total = [
                t for t in state_history if t['transition'] == transition_name
            ]
            return len(total)

        # updated refused times
        self.refused_times = number_of_transitions('refuse')

        def updated_if_changed(attr, t_list, first=False):
            """Update only if changed."""
            existing = getattr(self, attr)
            new = get_transition_date_from_history(t_list,
                                                   state_history,
                                                   first=first)
            if new != existing:
                setattr(self, attr, new)

        # Set first deliver date
        transitions = ('deliver', )
        updated_if_changed('deliver_date', transitions, True)

        # Set last deliver date
        transitions = ('deliver', )
        updated_if_changed('last_deliver_date', transitions, False)

        # Set acceptance date
        transitions = ('accept', 'refuse')
        updated_if_changed('accept_date', transitions, False)

        if keep_updated_at:
            self.updated_at = updated_at

    # Deal with assignment changes
    @sautils.observes('assignment.scheduled_datetime')
    def _scheduled_datetime_observer(self, scheduled_datetime):
        """Update scheduled_datetime on this object."""
        existing = self.scheduled_datetime
        if scheduled_datetime and scheduled_datetime != existing:
            self.scheduled_datetime = scheduled_datetime

    # Relevant dates
    @sautils.observes('state')
    def _dates_observer(self, state):
        """Calculate dates on a change of a state."""
        # Update all dates
        self._update_dates_from_history()

    def update(self, values: dict):
        """Custom update method to handle special case.

        :param values: Dictionary containing attributes and values
        :type values: dict
        """
        if 'requirement_items' in values:
            # force update requirement items before all other fields
            self.requirement_items = values.pop('requirement_items')
        super().update(values)

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_summary_dict(self) -> dict:
        """Return a summarized version of the dict representation of this Class.

        Used to serialize this object within a parent object serialization.
        :returns: Dictionary with fields and values used by this Class
        """
        data = super().to_summary_dict()
        return data

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_listing_dict(self) -> dict:
        """Return a summarized version of the dict representation of this Class.

        Used to serialize this object within a parent object serialization.
        :returns: Dictionary with fields and values used by this Class
        """
        data = super().to_listing_dict()
        return data

    @cache_region.cache_on_arguments(should_cache_fn=enable_cache)
    def to_dict(self, excludes: list = None, includes: list = None):
        """Return a dict representation of this object."""
        data = super().to_dict(excludes=excludes, includes=includes)
        data['assignments'] = []
        for assignment in self.assignments:
            assignment_data = assignment.to_summary_dict()
            assignment_data = assignment._apply_actors_info(assignment_data)
            data['assignments'].append(assignment_data)

        if data['assignments']:
            data['assignment'] = data['assignments'][-1]

        data['briefing'] = self.project.briefing
        data['scheduled_datetime'] = self.deliver_date

        # add_user_info_to_state_history(self.state_history)
        if includes and 'state_history' in includes:
            # Workflow history
            add_user_info_to_state_history(self.state_history)

        # HACK: An issue with cache prevents us from adding a specialized to_dict on LeadOrder
        # so, the quick solution is to check subtype here.
        if self.type == 'leadorder':
            data['confirmation_fields'] = self.confirmation_fields or []

        # Apply actor information to data
        data = self._apply_actors_info(data)
        return data
示例#29
0
class Participant(BaseModel):
    GENDER = (
        ('', _('Unspecified')),
        ('F', _('Female')),
        ('M', _('Male')),
    )

    __tablename__ = 'participant'

    id = db.Column(db.Integer, primary_key=True)
    full_name_translations = db.Column(JSONB)
    first_name_translations = db.Column(JSONB)
    other_names_translations = db.Column(JSONB)
    last_name_translations = db.Column(JSONB)
    participant_id = db.Column(db.String)
    role_id = db.Column(
        db.Integer, db.ForeignKey('participant_role.id', ondelete='SET NULL'))
    partner_id = db.Column(
        db.Integer, db.ForeignKey('participant_partner.id',
                                  ondelete='SET NULL'))
    supervisor_id = db.Column(
        db.Integer, db.ForeignKey('participant.id', ondelete='SET NULL'))
    gender = db.Column(sqlalchemy_utils.ChoiceType(GENDER))
    email = db.Column(db.String)
    location_id = db.Column(db.Integer,
                            db.ForeignKey('location.id', ondelete='CASCADE'))
    participant_set_id = db.Column(db.Integer,
                                   db.ForeignKey('participant_set.id',
                                                 ondelete='CASCADE'),
                                   nullable=False)
    message_count = db.Column(db.Integer, default=0)
    accurate_message_count = db.Column(db.Integer, default=0)
    completion_rating = db.Column(db.Float, default=1)
    device_id = db.Column(db.String)
    password = db.Column(db.String)
    extra_data = db.Column(JSONB)

    full_name = translation_hybrid(full_name_translations)
    first_name = translation_hybrid(first_name_translations)
    other_names = translation_hybrid(other_names_translations)
    last_name = translation_hybrid(last_name_translations)

    location = db.relationship('Location', backref='participants')
    participant_set = db.relationship('ParticipantSet',
                                      backref=db.backref('participants',
                                                         cascade='all, delete',
                                                         lazy='dynamic',
                                                         passive_deletes=True))
    groups = db.relationship('ParticipantGroup',
                             secondary=groups_participants,
                             backref='participants')
    role = db.relationship('ParticipantRole', backref='participants')
    partner = db.relationship('ParticipantPartner', backref='participants')
    phone_contacts = db.relationship('PhoneContact',
                                     backref=db.backref('participants',
                                                        cascade='all, delete'))
    supervisor = db.relationship('Participant', remote_side=id)

    def __str__(self):
        return self.name or ''

    @property
    def primary_phone(self):
        if not self.id:
            return None

        p_phone = PhoneContact.query.filter_by(
            participant_id=self.id,
            verified=True).order_by(PhoneContact.updated.desc()).first()

        return p_phone.number if p_phone else None

    @property
    def other_phones(self):
        if not self.id:
            return None

        phone_primary = PhoneContact.query.filter_by(
            participant_id=self.id,
            verified=True).order_by(PhoneContact.updated.desc()).first()

        if phone_primary:
            other_phones = PhoneContact.query.filter(
                PhoneContact.participant_id == self.id,  # noqa
                PhoneContact.id != phone_primary.id,  # noqa
                PhoneContact.verified == True  # noqa
            ).order_by(PhoneContact.updated.desc()).with_entities(
                PhoneContact.number).all()

            return list(chain(*other_phones))

        return []

    @property
    def phones(self):
        if not self.id:
            return None

        if not hasattr(self, '_phones'):
            phones = PhoneContact.query.filter_by(
                participant_id=self.id).order_by(PhoneContact.updated).all()
            self._phones = phones

        return self._phones

    @property
    def gender_display(self):
        if not self.gender:
            return Participant.GENDER[0][1]

        d = dict(Participant.GENDER)
        return d.get(self.gender, Participant.GENDER[0][1])

    @property
    def last_contacted(self):
        contact = ContactHistory.query.filter(
            ContactHistory.participant == self).order_by(
                ContactHistory.created.desc()).first()

        if contact:
            return contact.created
        else:
            return None

    @property
    def name(self):
        if self.full_name:
            return self.full_name

        names = [self.first_name, self.other_names, self.last_name]
        names = [n for n in names if n]

        return ' '.join(names)
示例#30
0
class ProfessionalBillingInfo(BillingInfo):
    """Billing information for a Professional."""

    __tablename__ = 'professional_billing_infos'
    _workflow = workflows.ProfessionalBillingInfoWorkflow

    __colanderalchemy_config__ = {
        'excludes': ['state_history', 'state', 'type', 'professional']
    }

    __raw_acl__ = (
        ('create', ('g:briefy_scout', 'g:briefy_finance', 'g:system')),
        ('list', ('g:briefy', 'g:system')),
        ('view', ('g:briefy', 'g:system')),
        ('edit', ('g:briefy_scout', 'g:briefy_finance', 'g:system')),
        ('delete', ('g:briefy_finance', 'g:system')),
    )

    id = sa.Column(UUIDType(),
                   sa.ForeignKey('billing_infos.id'),
                   index=True,
                   unique=True,
                   primary_key=True,
                   info={
                       'colanderalchemy': {
                           'title': 'Billing Info id',
                           'validator': colander.uuid,
                           'missing': colander.drop,
                           'typ': colander.String
                       }
                   })

    professional_id = sa.Column(UUIDType(),
                                sa.ForeignKey('professionals.id'),
                                index=True,
                                unique=True,
                                info={
                                    'colanderalchemy': {
                                        'title': 'Professional id',
                                        'validator': colander.uuid,
                                        'missing': colander.drop,
                                        'typ': colander.String
                                    }
                                })

    professional = orm.relationship('Professional')

    tax_id_status = sa.Column(sautils.ChoiceType(TaxIdStatusProfessionals,
                                                 impl=sa.String(3)),
                              nullable=True,
                              info={
                                  'colanderalchemy': {
                                      'title': 'Tax ID Status',
                                      'missing': colander.drop,
                                      'typ': colander.String
                                  }
                              })
    """Tax ID Status.

    Internal codes used by Finance to determine tax rates to be applied to this customer.
    """

    _payment_info = sa.Column('payment_info',
                              JSONB,
                              info={
                                  'colanderalchemy': {
                                      'title': 'Payment information',
                                      'missing': colander.drop,
                                      'typ': colander.List
                                  }
                              })
    """Payment information used for this professional."""
    @hybrid_property
    def payment_info(self) -> list:
        """Return list of payment information."""
        info = self._payment_info
        info = info if info else []
        return info

    @payment_info.setter
    def payment_info(self, value: list):
        """Set payment information for a professional."""
        self._payment_info = value

    @hybrid_property
    def default_payment_method(self) -> str:
        """Return the type of the preferred payment method used."""
        method_name = ''
        info = self.payment_info
        if info and len(info) > 0:
            method_name = info[0].get('type_')
        return method_name

    @hybrid_property
    def secondary_payment_method(self) -> str:
        """Return the type of the payment method used."""
        method_name = ''
        info = self.payment_info
        if info and len(info) > 1:
            method_name = info[1].get('type_')
        return method_name

    def to_dict(self, excludes: list = None, includes: list = None) -> dict:
        """Return a dictionary with fields and values used by this Class.

        :param excludes: attributes to exclude from dict representation.
        :param includes: attributes to include from dict representation.
        :returns: Dictionary with fields and values used by this Class
        """
        data = super().to_dict(excludes, includes)
        data['payment_info'] = self.payment_info
        data['default_payment_method'] = self.default_payment_method
        data['secondary_payment_method'] = self.secondary_payment_method
        data['professional'] = self.professional.to_summary_dict()
        return data