Ejemplo n.º 1
0
class Role(SurrogatePK, Model):
    '''Model to handle view-based permissions

    Attributes:
        id: primary key
        name: role name
    '''
    __tablename__ = 'roles'
    name = Column(db.String(80), unique=True, nullable=False)

    def __repr__(self):
        return '<Role({name})>'.format(name=self.name)

    def __unicode__(self):
        return self.name

    @classmethod
    def query_factory(cls):
        '''Generates a query of all roles

        Returns:
            `sqla query`_ of all roles
        '''
        return cls.query

    @classmethod
    def no_admins(cls):
        '''Generates a query of non-admin roles

        Returns:
            `sqla query`_ of roles without administrative access
        '''
        return cls.query.filter(cls.name != 'superadmin')
Ejemplo n.º 2
0
class Category(Model):
    '''Category model for opportunities and Vendor signups

    Categories are based on the codes created by the `National Institute
    of Government Purchasing (NIGP) <http://www.nigp.org/eweb/StartPage.aspx>`_.
    The names of the categories have been re-written a bit to make them more
    human-readable and in some cases a bit more modern.

    Attributes:
        id: Primary key unique ID
        nigp_codes: Array of integers refering to NIGP codes.
        category: parent top-level category
        subcategory: NIGP designated subcategory name
        category_friendly_name: Rewritten, more human-readable subcategory name
        examples: Pipe-delimited examples of items that fall in each subcategory
        examples_tsv: TSVECTOR of the examples for that subcategory

    See Also:
        The :ref:`nigp-importer` contains more information about how NIGP codes
        are imported into the system.
    '''
    __tablename__ = 'category'

    id = Column(db.Integer, primary_key=True, index=True)
    nigp_codes = Column(ARRAY(db.Integer()))
    category = Column(db.String(255))
    subcategory = Column(db.String(255))
    category_friendly_name = Column(db.Text)
    examples = Column(db.Text)
    examples_tsv = Column(TSVECTOR)

    def __unicode__(self):
        return '{sub} (in {main})'.format(sub=self.category_friendly_name,
                                          main=self.category)

    @classmethod
    def parent_category_query_factory(cls):
        '''Query factory to return a query of all of the distinct top-level categories
        '''
        return db.session.query(db.distinct(
            cls.category).label('category')).order_by('category')

    @classmethod
    def query_factory(cls):
        '''Query factory that returns all category/subcategory pairs
        '''
        return cls.query
Ejemplo n.º 3
0
class RequiredBidDocument(Model):
    '''Model for documents that a vendor would be required to provide

    There are two types of documents associated with an opportunity -- documents
    that the City will provide (RFP/IFB/RFQ, Q&A documents, etc.), and documents
    that the bidder will need to provide upon bidding (Insurance certificates,
    Bid bonds, etc.). This model describes the latter.

    See Also:
        These models get rendered into a select multi with the descriptions rendered
        in tooltips. For more on how this works, see the
        :py:func:`~purchasing.beacon.blueprints.opportunity_view_utils.select_multi_checkbox`.

    Attributes:
        id: Primary key unique ID
        display_name: Display name for the document
        description: Description of what the document is, rendered in a tooltip
        form_href: A link to an example document
    '''
    __tablename__ = 'document'

    id = Column(db.Integer, primary_key=True, index=True)
    display_name = Column(db.String(255), nullable=False)
    description = Column(db.Text, nullable=False)
    form_href = Column(db.String(255))

    def get_choices(self):
        '''Builds a custom two-tuple for the CHOICES.

        Returns:
            Two-tuple of (ID, [name, description, href]), which can then be
            passed to :py:func:`~purchasing.beacon.blueprints.opportunity_view_utils.select_multi_checkbox`
            to generate multi-checkbox fields
        '''
        return (self.id, [self.display_name, self.description, self.form_href])

    @classmethod
    def generate_choices(cls):
        '''Builds a list of custom CHOICES

        Returns:
            List of two-tuples described in the
            :py:meth:`RequiredBidDocument.get_choices`
            method
        '''
        return [i.get_choices() for i in cls.query.all()]
Ejemplo n.º 4
0
class Department(SurrogatePK, Model):
    '''Department model

    Attributes:
        name: Name of department
    '''
    __tablename__ = 'department'

    name = Column(db.String(255), nullable=False, unique=True)

    def __unicode__(self):
        return self.name

    @classmethod
    def query_factory(cls):
        '''Generate a department query factory.

        Returns:
            Department query with new users filtered out
        '''
        return cls.query.filter(cls.name != 'New User')

    @classmethod
    def get_dept(cls, dept_name):
        '''Query Department by name.

        Arguments:
            dept_name: name used for query

        Returns:
            an instance of Department
        '''
        return cls.query.filter(
            db.func.lower(cls.name) == dept_name.lower()).first()

    @classmethod
    def choices(cls, blank=False):
        '''Query available departments by name and id.

        Arguments:
            blank: adds none choice to list when True,
                only returns Departments when False. Defaults to False.

        Returns:
            list of (department id, department name) tuples
        '''
        departments = [(i.id, i.name) for i in cls.query_factory().all()]
        if blank:
            departments = [(None, '-----')] + departments
        return departments
Ejemplo n.º 5
0
class JobStatus(Model):
    '''Model to track nightly job status and reporting

    JobStatus has a primary compound key of name + date

    Attributes:
        name: Name of the job
        date: Date the job is scheduled for
        status: String of the job status, defaults to 'new',
            set to 'started', 'success', 'failure', or 'skipped'
        info: Any additional reporting about the job status,
            such as an error message if the job fails
    '''
    __tablename__ = 'job_status'

    name = db.Column(db.String(255), primary_key=True)
    date = db.Column(db.DateTime, primary_key=True)
    status = db.Column(db.String, default='new')
    info = db.Column(db.Text)
Ejemplo n.º 6
0
class OpportunityDocument(Model):
    '''Model for bid documents associated with opportunities

    Attributes:
        id: Primary key unique ID
        opportunity_id: Foreign Key relationship back to the related
            :py:class:`~purchasing.models.front.Opportunity`
        opportunity: Sqlalchemy relationship back to the related
            :py:class:`~purchasing.models.front.Opportunity`
        name: Name of the document for display
        href: Link to the document
    '''
    __tablename__ = 'opportunity_document'

    id = Column(db.Integer, primary_key=True, index=True)
    opportunity_id = ReferenceCol('opportunity', ondelete='cascade')
    opportunity = db.relationship('Opportunity',
                                  backref=backref(
                                      'opportunity_documents',
                                      lazy='dynamic',
                                      cascade='all, delete-orphan'))

    name = Column(db.String(255))
    href = Column(db.Text())

    def get_href(self):
        '''Builds link to the file

        Returns:
            S3 link if using S3, local filesystem link otherwise
        '''
        if current_app.config['UPLOAD_S3']:
            return self.href
        else:
            if self.href.startswith('http'):
                return self.href
            return 'file://{}'.format(self.href)

    def clean_name(self):
        '''Replaces underscores with spaces
        '''
        return self.name.replace('_', ' ')
Ejemplo n.º 7
0
class AppStatus(Model):
    '''Model of current application status

    Attributes:
        id: Primary key
        status: Current application status
        last_updated: Datetime of the last time the status was updated
        county_max_deadline: Datetime of the last time the county scraper
            was updated
        message: If the status is an error, the message will have more
            information about the nature of the error
        last_beacon_newsletter: Datetime of the last time a beacon
            newsletter was sent
    '''
    __tablename__ = 'app_status'

    id = Column(db.Integer, primary_key=True)
    status = Column(db.String(255))
    last_updated = Column(db.DateTime)
    county_max_deadline = Column(db.DateTime)
    message = Column(db.Text)
    last_beacon_newsletter = Column(db.DateTime)
Ejemplo n.º 8
0
class OpportunityType(Model):
    '''Model for opportunity types

    Attributes:
        id: Primary key unique ID
        name: Name of the contract type
        opportunity_response_instructions: HTML string of instructions
            for bidders on how to respond to opportunities of this
            type
    '''
    __tablename__ = 'opportunity_type'

    id = Column(db.Integer, primary_key=True, index=True)
    name = Column(db.String(255))
    opportunity_response_instructions = Column(db.Text)

    def __unicode__(self):
        return self.name if self.name else ''

    @classmethod
    def query_factory_all(cls):
        '''Query factory to return all contract types
        '''
        return cls.query.order_by(cls.name)

    @classmethod
    def get_type(cls, type_name):
        '''Get an individual type based on a passed type name

        Arguments:
            type_name: Name of the type to look up

        Returns:
            One :py:class:`~purchasing.data.contracts.ContractType` object
        '''
        return cls.query.filter(
            db.func.lower(cls.name) == type_name.lower()).first()
Ejemplo n.º 9
0
class AcceptedEmailDomains(Model):
    '''Model of permitted email domains for new user creation

    Because authentication is handled by `persona
    <https://login.persona.org/about>`_, we still need to control
    some level of authorization. We do this on two levels. First,
    we use Role-based permissions using the :py:class:`~purchasing.data.models.Role`
    class and the :py:func:`~purchasing.decorators.requires_roles` method.
    We also do this by restricting new user creation to people who have
    a certain set of email domains.

    See Also:
        :ref:`persona`

    Attributes:
        id (int): Primary key
        domain (str): string of an acceptable domain (for example, ``pittsburghpa.gov``)
    '''
    __tablename__ = 'accepted_domains'

    id = Column(db.Integer, primary_key=True)
    domain = Column(db.String(255), unique=True)

    @classmethod
    def valid_domain(cls, domain_to_lookup):
        '''Check if a domain is in the valid domains

        Args:
            domain_to_lookup (str): string of domain to be checked

        Returns:
            bool: True if domain is valid, False otherwise
        '''
        return cls.query.filter(
            str(domain_to_lookup).lower() == db.func.lower(
                cls.domain)).count() > 0
Ejemplo n.º 10
0
class Vendor(Model):
    '''Base Vendor model for businesses interested in Beacon

    The primary driving thought behind Beacon is that it should be as
    easy as possible to sign up to receive updates about new front.
    Therefore, there are no Vendor accounts or anything like that, just
    email addresses and business names.

    Attributes:
        id: Primary key unique ID
        business_name: Name of the business, required
        email: Email address for the vendor, required
        first_name: First name of the vendor
        last_name: Last name of the vendor
        phone_number: Phone number for the vendor
        fax_number: Fax number for the vendor
        minority_owned: Whether the vendor is minority owned
        veteran_owned: Whether the vendor is veteran owned
        woman_owned: Whether the vendor is woman owned
        disadvantaged_owned: Whether the vendor is any class
            of Disadvantaged Business Enterprise (DBE)
        categories: Many-to-many relationship with
            :py:class:`~purchasing.models.front.Category`;
            describes what the vendor is subscribed to
        opportunities: Many-to-many relationship with
            :py:class:`~purchasing.models.front.Opportunity`;
            describes what opportunities the vendor is subscribed to
        subscribed_to_newsletter: Whether the vendor is subscribed to
            receive the biweekly newsletter of all opportunities
    '''
    __tablename__ = 'vendor'

    id = Column(db.Integer, primary_key=True, index=True)
    business_name = Column(db.String(255), nullable=False)
    email = Column(db.String(80), unique=True, nullable=False)
    first_name = Column(db.String(30), nullable=True)
    last_name = Column(db.String(30), nullable=True)
    phone_number = Column(db.String(20))
    fax_number = Column(db.String(20))
    minority_owned = Column(db.Boolean())
    veteran_owned = Column(db.Boolean())
    woman_owned = Column(db.Boolean())
    disadvantaged_owned = Column(db.Boolean())
    categories = db.relationship('Category',
                                 secondary=category_vendor_association_table,
                                 backref='vendors',
                                 collection_class=set)
    opportunities = db.relationship(
        'Opportunity',
        secondary=opportunity_vendor_association_table,
        backref='vendors',
        collection_class=set)

    subscribed_to_newsletter = Column(db.Boolean(),
                                      default=False,
                                      nullable=False)

    @classmethod
    def newsletter_subscribers(cls):
        '''Query to return all vendors signed up to the newsletter
        '''
        return cls.query.filter(cls.subscribed_to_newsletter == True).all()

    def build_downloadable_row(self):
        '''Take a Vendor object and build a list for a .tsv download

        Returns:
            List of all vendor fields in order for a bulk vendor download
        '''
        return [
            self.first_name, self.last_name, self.business_name, self.email,
            self.phone_number, self.minority_owned, self.woman_owned,
            self.veteran_owned, self.disadvantaged_owned,
            build_downloadable_groups('category_friendly_name',
                                      self.categories),
            build_downloadable_groups('title', self.opportunities)
        ]

    def __unicode__(self):
        return self.email
Ejemplo n.º 11
0
class Opportunity(Model):
    '''Base Opportunity Model -- the central point for Beacon

    The Beacon model is centered around three dates:
    :py:attr:`~purchasing.models.front.Opportunity.planned_publish`,
    :py:attr:`~purchasing.models.front.Opportunity.planned_submission_start`,
    and :py:attr:`~purchasing.models.front.Opportunity.planned_submission_end`.
    The publish date is when opportunities that are approved appear on Beacon. The
    publication date also is when vendors are notified via email.

    Attributes:
        id: Primary key unique ID
        title: Title of the Opportunity
        description: Short (maximum 500-word) description of the opportunity
        planned_publish: Date when the opportunity should show up on Beacon
        planned_submission_start: Date when vendors can begin submitting
            responses to the opportunity
        planned_submission_end: Deadline for submitted responses to the
            Opportunity
        vendor_documents_needed: Array of integers that relate to
            :py:class:`~purchasing.models.front.RequiredBidDocument` ids
        is_public: True if opportunity is approved (publicly visible), False otherwise
        is_archived: True if opportunity is archived (not visible), False otherwise
        published_at: Date when an alert email was sent out to relevant vendors
        publish_notification_sent: True is notification sent, False otherwise
        department_id: ID of primary :py:class:`~purchasing.models.users.Department`
            for this opportunity
        department: Sqlalchemy relationship to primary
            :py:class:`~purchasing.models.users.Department`
            for this opportunity
        contact_id: ID of the :py:class:`~purchasing.models.users.User` for this opportunity
        contact: Sqlalchemy relationship to :py:class:`~purchasing.models.users.User`
            for this opportunity
        categories: Many-to-many relationship of the
            :py:class:`~purchasing.models.front.Category` objects
            for this opportunity
        opportunity_type_id: ID of the :py:class:`~beacon.models.front.OpportunityType`
        opportunity_type: Sqlalchemy relationship to the :py:class:`~beacon.models.front.OpportunityType`

    See Also:
        For more on the Conductor <--> Beacon relationship, look at the
        :py:func:`~purchasing.conductor.handle_form()` Conductor utility method and the
        :py:class:`~purchasing.conductor.forms.PostOpportunityForm` Conductor Form
    '''
    __tablename__ = 'opportunity'

    id = Column(db.Integer, primary_key=True)
    title = Column(db.String(255))
    description = Column(db.Text)
    planned_publish = Column(db.DateTime, nullable=False)
    planned_submission_start = Column(db.DateTime, nullable=False)
    planned_submission_end = Column(db.DateTime, nullable=False)
    vendor_documents_needed = Column(ARRAY(db.Integer()))
    is_public = Column(db.Boolean(), default=False)
    is_archived = Column(db.Boolean(), default=False, nullable=False)

    published_at = Column(db.DateTime, nullable=True)
    publish_notification_sent = Column(db.Boolean,
                                       default=False,
                                       nullable=False)

    department_id = ReferenceCol('department',
                                 ondelete='SET NULL',
                                 nullable=True)
    department = db.relationship('Department',
                                 backref=backref('opportunities',
                                                 lazy='dynamic'))

    contact_id = ReferenceCol('users', ondelete='SET NULL')
    contact = db.relationship('User',
                              backref=backref('opportunities', lazy='dynamic'),
                              foreign_keys='Opportunity.contact_id')

    categories = db.relationship(
        'Category',
        secondary=category_opportunity_association_table,
        backref='opportunities',
        collection_class=set)

    opportunity_type_id = ReferenceCol('opportunity_type',
                                       ondelete='SET NULL',
                                       nullable=True)
    opportunity_type = db.relationship(
        'OpportunityType',
        backref=backref('opportunities', lazy='dynamic'),
    )

    @classmethod
    def create(cls, data, user, documents, publish=False):
        '''Create a new opportunity

        Arguments:
            data: dictionary of fields needed to populate new
                opportunity object
            user: :py:class:`~purchasing.models.users.User` object
                creating the new opportunity
            documents: The documents FieldList from the
                :py:class:`~purchasing.forms.front.OpportunityForm`

        Keyword Arguments:
            publish: Boolean as to whether to publish this document. If
                True, it will set ``is_public`` to True.

        See Also:
            The :py:class:`~purchasing.forms.front.OpportunityForm`
            and :py:class:`~purchasing.forms.front.OpportunityDocumentForm`
            have more information about the documents.

        '''
        opportunity = Opportunity(**data)

        current_app.logger.info(
            '''BEACON NEW - New Opportunity Created: Department: {} | Title: {} | Publish Date: {} | Submission Start Date: {} | Submission End Date: {}
            '''.format(
                opportunity.id,
                opportunity.department.name if opportunity.department else '',
                opportunity.title.encode('ascii', 'ignore'),
                str(opportunity.planned_publish),
                str(opportunity.planned_submission_start),
                str(opportunity.planned_submission_end)))

        if not (user.is_conductor() or publish):
            # only send 'your post has been sent/a new post needs review'
            # emails when 1. the submitter isn't from OMB and 2. they are
            # saving a draft as opposed to publishing the opportunity
            opportunity.notify_approvals(user)

        opportunity._handle_uploads(documents)
        opportunity._publish(publish)

        return opportunity

    def raw_update(self, **kwargs):
        '''Performs a basic update based on the passed kwargs.

        Arguments:
            **kwargs: Keyword arguments of fields to be updated in
                the existing Opportunity model
        '''
        super(Opportunity, self).update(**kwargs)

    def update(self, data, user, documents, publish=False):
        '''Performs an update, uploads new documents, and publishes

        Arguments:
            data: dictionary of fields needed to populate new
                opportunity object
            user: :py:class:`~purchasing.models.users.User` object
                updating the opportunity
            documents: The documents FieldList from the
                :py:class:`~purchasing.forms.front.OpportunityForm`

        Keyword Arguments:
            publish: Boolean as to whether to publish this document. If
                True, it will set ``is_public`` to True.
        '''
        data.pop('publish_notification_sent', None)
        for attr, value in data.iteritems():
            setattr(self, attr, value)

        current_app.logger.info(
            '''BEACON Update - Opportunity Updated: ID: {} | Title: {} | Publish Date: {} | Submission Start Date: {} | Submission End Date: {}
            '''.format(self.id, self.title.encode('ascii', 'ignore'),
                       str(self.planned_publish),
                       str(self.planned_submission_start),
                       str(self.planned_submission_end)))

        self._handle_uploads(documents)
        self._publish(publish)

    @property
    def is_published(self):
        '''Determine if an opportunity can be displayed

        Returns:
            True if the planned publish date is before or on today,
            and the opportunity is approved, False otherwise
        '''
        return self.coerce_to_date(
            self.planned_publish) <= localize_today() and self.is_public

    @property
    def is_upcoming(self):
        '''Determine if an opportunity is upcoming

        Returns:
            True if the planned publish date is before or on today, is approved,
            is not accepting submissions, and is not closed; False otherwise
        '''
        return self.coerce_to_date(self.planned_publish) <= localize_today() and \
            not self.is_submission_start and not self.is_submission_end and self.is_public

    @property
    def is_submission_start(self):
        '''Determine if the oppportunity is accepting submissions

        Returns:
            True if the submission start date and planned publish date are
            before or on today, is approved, and the opportunity is not closed;
            False otherwise
        '''
        return self.coerce_to_date(self.planned_submission_start) <= localize_today() and \
            self.coerce_to_date(self.planned_publish) <= localize_today() and \
            not self.is_submission_end and self.is_public

    @property
    def is_submission_end(self):
        '''Determine if an opportunity is closed to new submissions

        Returns:
            True if the submission end date is on or before today,
            and it is approved
        '''
        return pytz.UTC.localize(self.planned_submission_end).astimezone(
            current_app.config['DISPLAY_TIMEZONE']
        ) <= localize_now() and \
            self.is_public

    @property
    def has_docs(self):
        '''True if the opportunity has at least one document, False otherwise
        '''
        return self.opportunity_documents.count() > 0

    def estimate_submission_start(self):
        '''Returns the month/year based on submission start date
        '''
        return self.planned_submission_start.strftime('%B %d, %Y')

    def estimate_submission_end(self):
        '''Returns the localized date and time based on submission end date
        '''
        return pytz.UTC.localize(self.planned_submission_end).astimezone(
            current_app.config['DISPLAY_TIMEZONE']).strftime(
                '%B %d, %Y at %I:%M%p %Z')

    def can_view(self, user):
        '''Check if a user can see opportunity detail

        Arguments:
            user: A :py:class:`~purchasing.models.users.User` object

        Returns:
            Boolean indiciating if the user can view this opportunity
        '''
        return False if user.is_anonymous() and not self.is_published else True

    def can_edit(self, user):
        '''Check if a user can edit the contract

        Arguments:
            user: A :py:class:`~purchasing.models.users.User` object

        Returns:
            Boolean indiciating if the user can edit this opportunity.
            Conductors, the opportunity creator, and the primary opportunity
            contact can all edit the opportunity before it is published. After
            it is published, only conductors can edit it.
        '''
        if self.is_public and user.role.name in ('conductor', 'admin',
                                                 'superadmin'):
            return True
        elif not self.is_public and \
            (user.role.name in ('conductor', 'admin', 'superadmin') or
                user.id in (self.created_by_id, self.contact_id)):
            return True
        return False

    def coerce_to_date(self, field):
        '''Coerces the input field to a datetime.date object

        Arguments:
            field: A datetime.datetime or datetime.date object

        Returns:
            A datetime.date object
        '''
        if isinstance(field, datetime.datetime):
            return field.date()
        if isinstance(field, datetime.date):
            return field
        return field

    def get_vendor_emails(self):
        '''Return list of all signed up vendors
        '''
        return [i.email for i in self.vendors]

    def has_vendor_documents(self):
        '''Returns a Boolean for whether there are required bid documents

        See Also:
            :py:class:`~purchasing.models.front.RequiredBidDocument`
        '''
        return self.vendor_documents_needed and len(
            self.vendor_documents_needed) > 0

    def get_vendor_documents(self):
        '''Returns a list of documents the the vendor will need to provide

        See Also:
            :py:class:`~purchasing.models.front.RequiredBidDocument`
        '''
        if self.has_vendor_documents():
            return RequiredBidDocument.query.filter(
                RequiredBidDocument.id.in_(
                    self.vendor_documents_needed)).all()
        return []

    def get_events(self):
        '''Returns the opportunity dates out as a nice ordered list for rendering
        '''
        return [{
            'event': 'bid_submission_start',
            'classes': 'event event-submission_start',
            'date': self.estimate_submission_start(),
            'description': 'Opportunity opens for submissions.'
        }, {
            'event': 'bid_submission_end',
            'classes': 'event event-submission_end',
            'date': self.estimate_submission_end(),
            'description': 'Deadline to submit proposals.'
        }]

    def _handle_uploads(self, documents):
        opp_documents = self.opportunity_documents.all()

        for document in documents.entries:
            if document.title.data == '':
                continue

            _id = self.id if self.id else random_id(6)

            _file = document.document.data
            if _file.filename in [i.name for i in opp_documents]:
                continue

            filename, filepath = document.upload_document(_id)
            if filepath:
                self.opportunity_documents.append(
                    OpportunityDocument(name=document.title.data,
                                        href=filepath))

    def _publish(self, publish):
        if not self.is_public:
            if publish:
                self.is_public = True

    def notify_approvals(self, user):
        '''Send the approval notifications to everyone with approval rights

        Arguments:
            user: A :py:class:`~purchasing.models.users.User` object
        '''
        Notification(to_email=[user.email],
                     subject='Your post has been sent to OMB for approval',
                     html_template='beacon/emails/staff_postsubmitted.html',
                     txt_template='beacon/emails/staff_postsubmitted.txt',
                     opportunity=self).send(multi=True)

        Notification(to_email=db.session.query(User.email).join(
            Role, User.role_id == Role.id).filter(
                Role.name.in_(['conductor', 'admin', 'superadmin'])).all(),
                     subject='A new Beacon post needs review',
                     html_template='beacon/emails/admin_postforapproval.html',
                     txt_template='beacon/emails/admin_postforapproval.txt',
                     opportunity=self).send(multi=True)

    def get_category_ids(self):
        '''Returns the IDs from the Opportunity's related categories
        '''
        return [i.id for i in self.categories]

    def send_publish_email(self):
        '''Sends the "new opportunity available" email to subscribed vendors

        If a new Opportunity is created and it has a publish date before or
        on today's date, it will trigger an immediate publish email send. This
        operates in a very similar way to the nightly
        :py:class:`~purchasing.jobs.beacon_nightly.BeaconNewOppotunityOpenJob`.
        It will build a list of all vendors signed up to the Opportunity
        or to any of the categories that describe the Opportunity.
        '''
        if self.is_published and not self.publish_notification_sent:
            vendors = Vendor.query.filter(
                Vendor.categories.any(Category.id.in_(
                    self.get_category_ids()))).all()

            Notification(
                to_email=[i.email for i in vendors],
                subject='A new City of Pittsburgh opportunity from Beacon!',
                html_template='beacon/emails/newopp.html',
                txt_template='beacon/emails/newopp.txt',
                opportunity=self).send(multi=True)

            self.publish_notification_sent = True
            self.published_at = datetime.datetime.utcnow()

            current_app.logger.info(
                '''BEACON PUBLISHED:  ID: {} | Title: {} | Publish Date: {} | Submission Start Date: {} | Submission End Date: {}
                '''.format(self.id, self.title.encode('ascii', 'ignore'),
                           str(self.planned_publish),
                           str(self.planned_submission_start),
                           str(self.planned_submission_end)))
            return True
        return False
Ejemplo n.º 12
0
class User(UserMixin, SurrogatePK, Model):
    '''User model

    Attributes:
        id: primary key
        email: user email address
        first_name: first name of user
        last_name: last name of user
        active: whether user is currently active or not
        role_id: foreign key of user's role
        role: relationship of user to role table
        department_id: foreign key of user's department
        department: relationship of user to department table
    '''

    __tablename__ = 'users'
    email = Column(db.String(80), unique=True, nullable=False, index=True)
    first_name = Column(db.String(30), nullable=True)
    last_name = Column(db.String(30), nullable=True)
    active = Column(db.Boolean(), default=True)

    role_id = ReferenceCol('roles', ondelete='SET NULL', nullable=True)
    role = db.relationship('Role',
                           backref=backref('users', lazy='dynamic'),
                           foreign_keys=role_id,
                           primaryjoin='User.role_id==Role.id')

    department_id = ReferenceCol('department',
                                 ondelete='SET NULL',
                                 nullable=True)
    department = db.relationship(
        'Department',
        backref=backref('users', lazy='dynamic'),
        foreign_keys=department_id,
        primaryjoin='User.department_id==Department.id')

    def __repr__(self):
        return '<User({email!r})>'.format(email=self.email)

    def __unicode__(self):
        return self.email

    @property
    def full_name(self):
        '''Build full name of user

        Returns:
            concatenated string of first_name and last_name values
        '''
        return "{0} {1}".format(self.first_name, self.last_name)

    @classmethod
    def department_user_factory(cls, department_id):
        return cls.query.filter(
            cls.department_id == department_id,
            db.func.lower(Department.name) !=
            'equal opportunity review commission')

    @classmethod
    def county_purchaser_factory(cls):
        return cls.query.join(
            Role, User.role_id == Role.id).filter(Role.name == 'county')

    @classmethod
    def eorc_user_factory(cls):
        return cls.query.join(Department,
                              User.department_id == Department.id).filter(
                                  db.func.lower(Department.name) ==
                                  'equal opportunity review commission')

    @classmethod
    def get_subscriber_groups(cls, department_id):
        return [
            cls.department_user_factory(department_id).all(),
            cls.county_purchaser_factory().all(),
            cls.eorc_user_factory().all()
        ]

    def get_following(self):
        '''Generate user contract subscriptions

        Returns:
            list of ids for contracts followed by user
        '''
        return [i.id for i in self.contracts_following]

    def is_conductor(self):
        '''Check if user can access conductor application

        Returns:
            True if user's role is either conductor, admin, or superadmin,
            False otherwise
        '''
        return self.role.name in ('conductor', 'admin', 'superadmin')

    def print_pretty_name(self):
        '''Generate long version text representation of user

        Returns:
            full_name if first_name and last_name exist, email otherwise
        '''
        if self.first_name and self.last_name:
            return self.full_name
        else:
            return self.email

    def print_pretty_first_name(self):
        '''Generate abbreviated text representation of user

        Returns:
            first_name if first_name exists,
            `localpart <https://en.wikipedia.org/wiki/Email_address#Local_part>`_
            otherwise
        '''
        if self.first_name:
            return self.first_name
        else:
            return self.email.split('@')[0]

    @classmethod
    def conductor_users_query(cls):
        '''Query users with access to conductor

        Returns:
            list of users with ``is_conductor`` value of True
        '''
        return [i for i in cls.query.all() if i.is_conductor()]