class User(UserMixin, SurrogatePK, Model): __tablename__ = 'users' email = Column(db.String(80), unique=True, nullable=False, index=True) created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow) first_name = Column(db.String(30), nullable=True) last_name = Column(db.String(30), nullable=True) department = Column(db.String(255), nullable=False) active = Column(db.Boolean(), default=True) role_id = ReferenceCol('roles', ondelete='SET NULL', nullable=True) @property def full_name(self): return "{0} {1}".format(self.first_name, self.last_name) def __repr__(self): return '<User({email!r})>'.format(email=self.email) def __unicode__(self): return self.email def get_starred(self): return [i.id for i in self.contracts_starred] def get_following(self): return [i.id for i in self.contracts_following]
class CompanyContact(Model): __tablename__ = 'company_contact' id = Column(db.Integer, primary_key=True, index=True) company = db.relationship( 'Company', backref=backref('contacts', lazy='dynamic', cascade='all, delete-orphan') ) company_id = ReferenceCol('company', ondelete='cascade') first_name = Column(db.String(255)) last_name = Column(db.String(255)) addr1 = Column(db.String(255)) addr2 = Column(db.String(255)) city = Column(db.String(255)) state = Column(db.String(255)) zip_code = Column(db.Integer) phone_number = Column(db.String(255)) fax_number = Column(db.String(255)) email = Column(db.String(255)) def __unicode__(self): return '{first} {last} - {email}'.format( first=self.first_name, last=self.last_name, email=self.email )
class Category(Model): __tablename__ = 'category' id = Column(db.Integer, primary_key=True, index=True) nigp_code = Column(db.Integer) category = Column(db.String(255)) subcategory = Column(db.String(255)) def __unicode__(self): return '{sub} (in {main})'.format(sub=self.subcategory, main=self.category)
class StageProperty(Model): __tablename__ = 'stage_property' id = Column(db.Integer, primary_key=True, index=True) stage = db.relationship('Stage', backref=backref( 'properties', lazy='dynamic', cascade='all, delete-orphan' )) stage_id = ReferenceCol('stage', ondelete='CASCADE') key = Column(db.String(255), nullable=False) value = Column(db.String(255)) def __unicode__(self): return '{key}: {value}'.format(key=self.key, value=self.value)
class ContractProperty(RefreshSearchViewMixin, Model): '''Model for contract properties The contract property model effectively serves as a key-value storage unit for properties that exist on a subset of contracts. For example, a common unit for County contracts is the so-called "spec number", an identified used by Allegheny County for their electronic bidding system. Other contract types (such as PA state and COSTARS contracts), do not have this property but do have others (such as manufacturers offered, etc.). Therefore, we use this model as an extended key-value store for the base :py:class:`~purchasing.data.contracts.ContractBase` model Attributes: id: Primary key unique ID contract: Sqlalchemy relationship to :py:class:`~purchasing.data.contracts.ContractBase` contract_id: Foreign key to :py:class:`~purchasing.data.contracts.ContractBase` key: The key for the property (for example, Spec Number) value: The value for the property (for example, 7137) ''' __tablename__ = 'contract_property' id = Column(db.Integer, primary_key=True, index=True) contract = db.relationship('ContractBase', backref=backref('properties', lazy='joined', cascade='all, delete-orphan')) contract_id = ReferenceCol('contract', ondelete='CASCADE') key = Column(db.String(255), nullable=False) value = Column(db.Text) def __unicode__(self): return u'{key}: {value}'.format(key=self.key, value=self.value)
class Company(RefreshSearchViewMixin, Model): '''Model for individual Compnaies Attributes: id: Primary key unique ID company_name: Name of the company contracts: Many-to-many relationship with the :py:class:` purchasing.data.contracts.ContractBase` model ''' __tablename__ = 'company' id = Column(db.Integer, primary_key=True, index=True) company_name = Column(db.String(255), nullable=False, unique=True, index=True) contracts = db.relationship( 'ContractBase', secondary=company_contract_association_table, backref='companies', ) def __repr__(self): return self.company_name def __unicode__(self): return self.company_name @classmethod def all_companies_query_factory(cls): '''Query factory of all company ids and names ordered by name ''' return db.session.query( db.distinct(cls.id).label('id'), cls.company_name).order_by(cls.company_name)
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')
class Stage(Model): '''Model for individual conductor stages Attributes: id: Primary key unique ID name: Name of the stage post_opportunities: Whether you can post :py:class:`~purchasing.opportunities.models.Opportunity` objects to :doc:`/beacon` from this stage default_message: Message to autopopulate the :py:class:`~purchasing.conductor.forms.SendUpdateForm` message body ''' __tablename__ = 'stage' id = Column(db.Integer, primary_key=True, index=True) name = Column(db.String(255)) post_opportunities = Column(db.Boolean, default=False, nullable=False) default_message = Column(db.Text) def __unicode__(self): return self.name @classmethod def choices_factory(cls): '''Return a two-tuple of (stage id, stage name) for all stages ''' return [(i.id, i.name) for i in cls.query.all()]
class ContractStageActionItem(Model): __tablename__ = 'contract_stage_action_item' id = Column(db.Integer, primary_key=True, index=True) contract_stage_id = ReferenceCol('contract_stage', ondelete='CASCADE', index=True) contract_stage = db.relationship('ContractStage', backref=backref( 'contract_stage_actions', lazy='dynamic', cascade='all, delete-orphan' )) action_type = Column(db.String(255)) action_detail = Column(JSON) taken_at = Column(db.DateTime, default=datetime.datetime.now()) taken_by = ReferenceCol('users', ondelete='SET NULL', nullable=True) def __unicode__(self): return self.action def get_sort_key(self): # if we are reversion, we need to get the timestamps from there if self.action_type == 'reversion': return datetime.datetime.strptime( self.action_detail['timestamp'], '%Y-%m-%dT%H:%M:%S' ) # otherwise, return the taken_at time else: return self.taken_at if self.taken_at else datetime.datetime(1970, 1, 1)
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 u'{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
class AppStatus(Model): __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)
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.opportunities.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.opportunities.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()]
class LineItem(RefreshSearchViewMixin, Model): '''Model for contract line items Attributes: id: Primary key unique ID contract: Sqlalchemy relationship to :py:class:`~purchasing.data.contracts.ContractBase` contract_id: Foreign key to :py:class:`~purchasing.data.contracts.ContractBase` description: Description of the line item in question manufacturer: Name of the manufacturer of the line item model_number: A model number for the item quantity: The quantity of the item on contract unit_of_measure: The unit of measure (for example EACH) unit_cost: Cost on a per-unit basis total_cost: Total cost (unit_cost * quantity) percentage: Whether or not the unit cost should be represented as a percentage (NOTE: on the BidNet system, there is no differentiation between a percentage discount off of an item and actual unit cost for an item) company_name: Name of the company that is providing the good company_id: Foreign key to :py:class:`~purchasing.data.companies.Company` ''' __tablename__ = 'line_item' id = Column(db.Integer, primary_key=True, index=True) contract = db.relationship('ContractBase', backref=backref('line_items', lazy='dynamic', cascade='all, delete-orphan')) contract_id = ReferenceCol('contract', ondelete='CASCADE') description = Column(db.Text, nullable=False, index=True) manufacturer = Column(db.Text) model_number = Column(db.Text) quantity = Column(db.Integer) unit_of_measure = Column(db.String(255)) unit_cost = Column(db.Float) total_cost = Column(db.Float) percentage = Column(db.Boolean) company_name = Column(db.String(255), nullable=True) company_id = ReferenceCol('company', nullable=True) def __unicode__(self): return self.description
class Role(SurrogatePK, RoleMixin, Model): '''Model to handle view-based permissions Attributes: id: primary key name: role name description: description of an individual role ''' __tablename__ = 'roles' name = Column(db.String(80), unique=True, nullable=False) description = Column(db.String(255), nullable=True) 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') @classmethod def staff_factory(cls): '''Factory to return the staff role Returns: Role object with the name 'staff' ''' return cls.query.filter(cls.name == 'staff')
class Stage(Model): __tablename__ = 'stage' id = Column(db.Integer, primary_key=True, index=True) name = Column(db.String(255)) send_notifs = Column(db.Boolean, default=False, nullable=False) post_opportunities = Column(db.Boolean, default=False, nullable=False) def __unicode__(self): return self.name
class Role(SurrogatePK, Model): __tablename__ = 'roles' name = Column(db.String(80), unique=True, nullable=False) users = db.relationship('User', lazy='dynamic', backref='role') def __repr__(self): return '<Role({name})>'.format(name=self.name) def __unicode__(self): return self.name
class CompanyContact(RefreshSearchViewMixin, Model): '''Model for Company Contacts Attributes: id: Primary key unique ID company_id: Foreign key relationship to a :py:class:`~purchasing.data.companies.Company` company: Sqlalchemy relationship with a :py:class:`~purchasing.data.companies.Company` first_name: First name of the contact last_name: Last name of the contact addr1: First line of the contact's address addr2: Second line of the contract's address city: Contact address city state: Contact address state zip_code: Contact address zip code phone_number: Contact phone number fax_number: Contact fax number email: Contact email ''' __tablename__ = 'company_contact' id = Column(db.Integer, primary_key=True, index=True) company = db.relationship( 'Company', backref=backref('contacts', lazy='dynamic', cascade='all, delete-orphan') ) company_id = ReferenceCol('company', ondelete='cascade') first_name = Column(db.String(255)) last_name = Column(db.String(255)) addr1 = Column(db.String(255)) addr2 = Column(db.String(255)) city = Column(db.String(255)) state = Column(db.String(255)) zip_code = Column(db.String(255)) phone_number = Column(db.String(255)) fax_number = Column(db.String(255)) email = Column(db.String(255)) def __unicode__(self): return '{first} {last}'.format( first=self.first_name, last=self.last_name )
class LineItem(Model): __tablename__ = 'line_item' id = Column(db.Integer, primary_key=True, index=True) contract = db.relationship('ContractBase', backref=backref( 'line_items', lazy='dynamic', cascade='all, delete-orphan' )) contract_id = ReferenceCol('contract', ondelete='CASCADE') description = Column(db.Text, nullable=False, index=True) manufacturer = Column(db.Text) model_number = Column(db.Text) quantity = Column(db.Integer) unit_of_measure = Column(db.String(255)) unit_cost = Column(db.Float) total_cost = Column(db.Float) percentage = Column(db.Boolean) company_name = Column(db.String(255), nullable=True) company_id = ReferenceCol('company', nullable=True) def __unicode__(self): return self.description
class Company(Model): __tablename__ = 'company' id = Column(db.Integer, primary_key=True, index=True) company_name = Column(db.String(255), nullable=False, unique=True, index=True) contracts = db.relationship( 'ContractBase', secondary=company_contract_association_table, backref='companies', ) def __unicode__(self): return self.company_name
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
class Opportunity(Model): __tablename__ = 'opportunity' id = Column(db.Integer, primary_key=True) contract_id = ReferenceCol('contract', ondelete='cascade') created_at = Column(db.DateTime, default=datetime.datetime.utcnow()) # Also the opportunity open date title = Column(db.String(255)) department = Column(db.String(255)) # Autopopulated using title and department plus boilerplate copy? description = Column(db.Text) category_id = ReferenceCol('category', ondelete='SET NULL') category = db.relationship('Category', lazy='subquery') # Date department opens bids bid_open = Column(db.DateTime) # Created from contract created_from = db.relationship('ContractBase', lazy='subquery', backref='opportunities')
class SearchView(Model): '''SearchView is a materialized view with all of our text columns See Also: For more detailed information about how this materialized view is set up, please refer to `Multi-Table Full Text Search with Postgres, Flask, and Sqlalchemy (Part I) <http://bensmithgall.com/blog/full-text-search-flask-sqlalchemy/>`_. For more information about Postgres Full-text search and TSVectors, refer to the `Postgres documentation about full-text search <http://www.postgresql.org/docs/current/static/textsearch-intro.html>`_ Attributes: id: Primary key unique ID for result contract_id: Unique ID for one contract company_id: Unique ID for one company financial_id: Financial ID for a contract expiration_date: Date a contract expires contract_description: Description of the goods or services provided by a :py:class:`~purchasing.data.contracts.ContractBase` tsv_contract_description: `TSVECTOR`_ of the contract description company_name: Name of the company providing services from the :py:class:`~purchasing.data.companies.Company` model tsv_company_name: `TSVECTOR`_ of the company name detail_key: :py:class:`~purchasing.data.contracts.ContractProperty` key detail_value: :py:class:`~purchasing.data.contracts.ContractProperty` value tsv_detail_value: `TSVECTOR`_ of the detail_value line_item_description: Description of a line item from the :py:class:`~purchasing.data.contracts.LineItem` model tsv_line_item_description: `TSVECTOR`_ of the line item description ''' __tablename__ = 'search_view' id = Column(db.Text, primary_key=True, index=True) contract_id = Column(db.Integer) company_id = Column(db.Integer) financial_id = Column(db.String(255)) expiration_date = Column(db.Date) contract_description = Column(db.Text) tsv_contract_description = Column(TSVECTOR) company_name = Column(db.Text) tsv_company_name = Column(TSVECTOR) detail_key = Column(db.Text) detail_value = Column(db.Text) tsv_detail_value = Column(TSVECTOR) line_item_description = Column(db.Text) tsv_line_item_description = Column(TSVECTOR)
class ContractType(Model): '''Model for contract types Attributes: id: Primary key unique ID name: Name of the contract type allow_opportunities: Boolean flag as to whether to allow opportunities to be posted managed_by_conductor: Boolean flag as to whether contracts of these types are managed through :doc:`/conductor` opportunity_response_instructions: HTML string of instructions for bidders on how to respond to opportunities of this type ''' __tablename__ = 'contract_type' id = Column(db.Integer, primary_key=True, index=True) name = Column(db.String(255)) allow_opportunities = Column(db.Boolean, default=False) managed_by_conductor = Column(db.Boolean, default=False) opportunity_response_instructions = Column(db.Text) def __unicode__(self): return self.name if self.name else '' @classmethod def opportunity_type_query(cls): '''Query factory filtered to include only types that allow opportunities ''' return cls.query.filter(cls.allow_opportunities == True) @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()
class ContractBase(Model): __tablename__ = 'contract' id = Column(db.Integer, primary_key=True) financial_id = Column(db.Integer) created_at = Column(db.DateTime, default=datetime.datetime.utcnow()) updated_at = Column(db.DateTime, default=datetime.datetime.utcnow(), onupdate=db.func.now()) contract_type = Column(db.String(255)) expiration_date = Column(db.Date) description = Column(db.Text, index=True) contract_href = Column(db.Text) current_flow = db.relationship('Flow', lazy='subquery') flow_id = ReferenceCol('flow', ondelete='SET NULL', nullable=True) current_stage = db.relationship('Stage', lazy='subquery') current_stage_id = ReferenceCol('stage', ondelete='SET NULL', nullable=True) followers = db.relationship( 'User', secondary=contract_user_association_table, backref='contracts_following', ) starred = db.relationship( 'User', secondary=contract_starred_association_table, backref='contracts_starred', ) assigned_to = ReferenceCol('users', ondelete='SET NULL', nullable=True) assigned = db.relationship('User', backref=backref( 'assignments', lazy='dynamic', cascade='none' )) is_archived = Column(db.Boolean, default=False, nullable=False) parent_id = Column(db.Integer, db.ForeignKey('contract.id')) children = db.relationship('ContractBase', backref=backref( 'parent', remote_side=[id] )) def __unicode__(self): return self.description def get_spec_number(self): '''Returns the spec number for a given contract ''' try: return [i for i in self.properties if i.key.lower() == 'spec number'][0] except IndexError: return ContractProperty()
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)
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.opportunities.models.Opportunity` opportunity: Sqlalchemy relationship back to the related :py:class:`~purchasing.opportunities.models.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('_', ' ')
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)
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
class Vendor(Model): __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) created_at = Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) 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') def __unicode__(self): return self.email
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 opportunities. 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.opportunities.models.Category`; describes what the vendor is subscribed to opportunities: Many-to-many relationship with :py:class:`~purchasing.opportunities.models.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