def test_notification_flatten(self, render_template): '''Test notification kwarg flattener ''' obj = MagicMock() obj.__unicode__ = lambda x: 'quux' notification = Notification(from_email='*****@*****.**', foo='bar', baz=['qux1', obj]) self.assertEquals( {'foo': 'bar', 'baz': 'qux1; qux2'}, notification.convert_models(dict(foo='bar', baz=['qux1', 'qux2'])) )
def test_notification_build_multi(self, current_app, render_template): '''Test single build messages have multiple recipients ''' current_app.logger = Mock(info=Mock()) notification = Notification(to_email=['*****@*****.**', '*****@*****.**'], from_email='*****@*****.**') # should build two messages on multi send msgs = notification._build(multi=False) self.assertTrue(len(msgs), 1) for msg in msgs: self.assertEquals(len(msg.recipients), 2)
def test_notification_send_multi(self, send, send_email, render_template): '''Test multi builds multiple message objects ''' notification = Notification(to_email=['*****@*****.**', '*****@*****.**'], from_email='*****@*****.**') notification.build_msg = Mock() notification.build_msg.return_value = [] # should build two messages on multi send notification.send(multi=True) self.assertTrue(notification.build_msg.called) self.assertEquals(notification.build_msg.call_count, 2)
def test_notification_send_single(self, send, send_email, render_template): '''Test non-multi only builds one message even with multiple emails ''' notification = Notification(to_email=['*****@*****.**', '*****@*****.**'], from_email='*****@*****.**') notification.build_msg = Mock() notification.build_msg.return_value = [] # should build two messages on multi send notification.send(multi=False) self.assertTrue(notification.build_msg.called) self.assertEquals(notification.build_msg.call_count, 1)
def test_notification_reshape(self, render_template): '''Test notification recipient flattener ''' notification = Notification(to_email='*****@*****.**', from_email='*****@*****.**') test_recips = [('a',), ('multi',), ['nested', 'thing']] self.assertEquals( ['a', 'multi', 'nested', 'thing'], notification.flatten(test_recips) ) test_recips_complex = ['a', ['b', ['c', 'd']], ['e']] self.assertEquals( ['a', 'b', 'c', 'd', 'e'], notification.flatten(test_recips_complex) )
def build_notifications(self): '''Implements EmailJobBase build_notifications method Returns: list of :py:class:`~purchasing.notifications.Notification` objects, one for each new Opportunity. For each Opportunity, the ``to_email`` field is the union of all followers of the opportunity and any followers of any categories that the Opportunity has ''' notifications = [] for opportunity in self.get_opportunities(): opp_categories = [i.id for i in opportunity.categories] category_vendors = Vendor.query.filter( Vendor.categories.any(Category.id.in_(opp_categories))).all() notifications.append( Notification( to_email=set([i.email for i in category_vendors] + [i.email for i in opportunity.vendors]), cc_email=list(), from_email=current_app.config['BEACON_SENDER'], subject='A new City of Pittsburgh opportunity from Beacon!', html_template='opportunities/emails/newopp.html', txt_template='opportunities/emails/newopp.txt', opportunity=opportunity)) opportunity.raw_update(publish_notification_sent=True) return notifications
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: opp_categories = [i.id for i in self.categories] vendors = Vendor.query.filter( Vendor.categories.any(Category.id.in_(opp_categories))).all() Notification( to_email=[i.email for i in vendors], subject='A new City of Pittsburgh opportunity from Beacon!', html_template='opportunities/emails/newopp.html', txt_template='opportunities/emails/newopp.txt', opportunity=self).send(multi=True) self.publish_notification_sent = True self.published_at = datetime.datetime.utcnow() current_app.logger.info( u'''BEACON PUBLISHED: ID: {} | Title: {} | Publish Date: {} | Submission Start Date: {} | Submission End Date: {} '''.format(self.id, self.title, str(self.planned_publish), str(self.planned_submission_start), str(self.planned_submission_end))) return True return False
def notify_approvals(self, user): '''Send the approval notifications to everyone with approval rights Arguments: user: A :py:class:`~purchasing.users.models.User` object ''' Notification( to_email=[user.email], subject='Your post has been sent to OMB for approval', html_template='opportunities/emails/staff_postsubmitted.html', txt_template='opportunities/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='opportunities/emails/admin_postforapproval.html', txt_template='opportunities/emails/admin_postforapproval.txt', opportunity=self).send(multi=True)
def test_notification_initialization(self, render_template): '''Test notifications properly initialize ''' notification = Notification( from_email='*****@*****.**', to_email='*****@*****.**', cc_email=[('*****@*****.**',), ('*****@*****.**',)] ) self.assertEquals(notification.to_email, ['*****@*****.**']) self.assertEquals(notification.from_email, '*****@*****.**') self.assertEquals(notification.cc_email, ['*****@*****.**', '*****@*****.**']) self.assertEquals(notification.subject, '') self.assertEquals(notification.html_body, 'a test') self.assertEquals(notification.txt_body, '') self.assertEquals(notification.attachments, [])
def feedback_handler(contract, search_for=None): '''Allow user to send feedback on the data present in a specific contract Arguments: contract: :py:class:`~purchasing.data.contracts.ContractBase` object search_for: search term or None. Returns: Redirects to or renders the appropriate feedback handling template ''' form = FeedbackForm() search_form = SearchForm() if not current_user.is_anonymous(): form.sender.data = current_user.email if form.validate_on_submit(): current_app.logger.info( 'WEXFEEDBACK - Feedback from {email} about {contract}'.format( email=form.sender.data, contract=contract.description)) feedback_sent = Notification( to_email=db.session.query(User.email).join( Role, User.role_id == Role.id).filter( Role.name.in_(['admin', 'superadmin'])).all(), subject= 'Scout contract feedback - ID: {id}, Description: {description}'. format(id=contract.id if contract.id else 'N/A', description=contract.description), html_template='scout/feedback_email.html', contract=contract, sender=form.data.get('sender'), body=form.data.get('body')).send() if feedback_sent: flash('Thank you for your feedback!', 'alert-success') else: flash('Oh no! Something went wrong. We are looking into it.', 'alert-danger') if contract.id: return redirect(url_for('scout.contract', contract_id=contract.id)) return redirect(url_for('scout.explore')) return render_template('scout/feedback.html', search_form=search_form, contract=contract, choices=Department.choices(), feedback_form=form, search_for=search_for)
def build_notifications(self): ''' ''' notifications = [] for contract in self.get_expiring_contracts(): notifications.append( Notification( to_email=[i.email for i in contract.followers], subject=self.notification_props['subject'], html_template=self.notification_props['html_template'], contract=contract ) ) return notifications
def post_validate_action(self, action, contract, current_stage): '''Send the email updates Arguments: action: A :py:class:`~purchasing.data.contract_stages.ContractStageActionItem` that needs to be updated with details for the action log contract: A :py:class:`~purchasing.data.contracts.ContractBase` object current_stage: The current :py:class:`~purchasing.data.contract_stages.ContractStage` Returns: The modified :py:class:`~purchasing.data.contract_stages.ContractStageActionItem` with the action detail updated to include the form's data ''' current_app.logger.info( 'CONDUCTOR EMAIL UPDATE | New update on stage "{}" from contract "{}" (ID: {})' .format(current_stage.name, contract.description, contract.id)) action.action_detail = { 'sent_to': self.data.get('send_to', ''), 'body': self.data.get('body'), 'subject': self.data.get('subject'), 'stage_name': current_stage.name, 'attachments': self.get_attachment_filenames() } Notification(to_email=[ i.strip() for i in self.data.get('send_to').split(';') if i != '' ], from_email=current_app.config['CONDUCTOR_SENDER'], reply_to=current_user.email, cc_email=[ i.strip() for i in self.data.get('send_to_cc').split(';') if i != '' ], subject=self.data.get('subject'), html_template='conductor/emails/email_update.html', body=self.data.get('body'), attachments=[ i.upload.data for i in self.attachments.entries ]).send(multi=False) return action
def publish(opportunity_id): '''Publish an opportunity If an :py:class:`~purchasing.opportunities.models.Opportunity` has been created by a non-admin, it will be stuck in a "pending" state until it has been approved by an admin. This view function handles the publication event for a specific :py:class:`~purchasing.opportunities.models.Opportunity` :status 200: Publish the relevant opportunity and send the relevant publication emails :status 404: :py:class:`~purchasing.opportunities.models.Opportunity` not found ''' opportunity = Opportunity.query.get(opportunity_id) if opportunity: opportunity.is_public = True db.session.commit() flash('Opportunity successfully published!', 'alert-success') Notification( to_email=[opportunity.created_by.email], subject='OMB approved your opportunity post!', html_template='opportunities/emails/staff_postapproved.html', txt_template='opportunities/emails/staff_postapproved.txt', opportunity=opportunity).send(multi=True) current_app.logger.info( '''BEACON APPROVED: ID: {} | Title: {} | Publish Date: {} | Submission Start Date: {} | Submission End Date: {} ''' .format(opportunity.id, opportunity.title.encode('ascii', 'ignore'), str(opportunity.planned_publish), str(opportunity.planned_submission_start), str(opportunity.planned_submission_end))) opportunity.send_publish_email() db.session.commit() return redirect(url_for('opportunities_admin.pending')) abort(404)
def build_notifications(self): '''Implements EmailJobBase build_notifications method Returns: list of :py:class:`~purchasing.notifications.Notification` objects, one for each non-expired opportunity that has been published since the last Beacon newsletter was sent out ''' notifications = [] opportunities = self.get_opportunities() notifications.append( Notification( to_email=set( [i.email for i in Vendor.newsletter_subscribers()]), from_email=current_app.config['BEACON_SENDER'], subject='Your biweekly Beacon opportunity summary', html_template='opportunities/emails/biweeklydigest.html', txt_template='opportunities/emails/biweeklydigest.txt', opportunities=opportunities)) return notifications
def edit_company_contacts(contract_id): '''Update information about company contacts, and save all information New :py:class:`~purchasing.data.contracts.ContractBase` objects are created for each unique controller number. Notifications are also sent to all of the original contract's followers to say that the contract information has been replaced/updated with new info. :param contract_id: Primary key ID for a :py:class:`~purchasing.data.contracts.ContractBase` .. seealso:: * :py:class:`~purchasing.conductor.forms.CompanyContactListForm` * :py:meth:`~purchasing.data.contracts.ContractBase.create` * :py:meth:`~purchasing.data.contracts.ContractBase.complete` * :py:class:`~purchasing.notifications.Notification` :status 200: Render the CompanyContactListForm form :status 302: Post the data and redirect back to the success view, or redirect back to contract or company views if those haven't been completed yet. :status 404: Contract not found ''' contract = ContractBase.query.get(contract_id) if contract and session.get( 'contract-{}'.format(contract_id)) is not None and session.get( 'companies-{}'.format(contract_id)) is not None: form = CompanyContactListForm() # pull out companies from session, order them by financial id # so that they can be grouped appropriately companies = sorted(json.loads( session['companies-{}'.format(contract_id)]), key=lambda x: x.get('financial_id')) if form.validate_on_submit(): main_contract = contract for ix, _company in enumerate(companies): contract_data = json.loads( session['contract-{}'.format(contract_id)]) # because multiple companies can have the same name, don't use # get_or_create because it can create multiples if _company.get('company_id') > 0: company = Company.query.get(_company.get('company_id')) else: company = Company.create( company_name=_company.get('company_name')) # contacts should be unique to companies, though try: for _contact in form.data.get('companies')[ix].get( 'contacts'): _contact['company_id'] = company.id contact, _ = get_or_create(db.session, CompanyContact, **_contact) # if there are no contacts, an index error will be thrown for this company # so we catch it and just pass except IndexError: pass contract_data['financial_id'] = _company['financial_id'] if contract.financial_id is None or contract.financial_id == _company[ 'financial_id']: contract.update_with_spec_number(contract_data, company=company) else: contract = ContractBase.clone(contract, parent_id=contract.parent_id, strip=False) contract.update_with_spec_number(contract_data, company=company) contract.is_visible = True db.session.commit() Notification(to_email=[i.email for i in contract.followers], from_email=current_app.config['CONDUCTOR_SENDER'], reply_to=current_user.email, subject='A contract you follow has been updated!', html_template='conductor/emails/new_contract.html', contract=main_contract).send(multi=True) session.pop('contract-{}'.format(contract_id)) session.pop('companies-{}'.format(contract_id)) session['success-{}'.format(contract_id)] = True current_app.logger.info(''' CONDUCTOR CONTRACT COMPLETE - company contacts for contract "{}" assigned. |New contract(s) successfully created''' .format(contract.description)) if contract.parent: contract.parent.complete() return redirect( url_for('conductor.success', contract_id=main_contract.id)) if len(form.companies.entries) == 0: for company in companies: form.companies.append_entry() return render_template('conductor/edit/edit_company_contacts.html', form=form, contract=contract, companies=companies) elif session.get('contract-{}'.format(contract_id)) is None: return redirect(url_for('conductor.edit', contract_id=contract_id)) elif session.get('companies-{}'.format(contract_id)) is None: return redirect( url_for('conductor.edit_company', contract_id=contract_id)) abort(404)
def signup_for_opp(form, opportunity, multi=False): '''Sign a vendor up for an opportunity Generic helper method to handle subscriptions from both the list view (signing up form multiple opportunities) and the detail view (signing up for a single opportunity). Responsible for creation of new Vendor objects if necessary, and sending emails based on the opportunities selected to receive updates about. Arguments: form: The relevant subscription form opportunity: Either an opportunity model or a list of opportunity ids multi: A boolean to flag if there are multiple opportunities that should to subscribe to or a single opportunity Returns: True if email sent successfully, false otherwise ''' send_email = True email_opportunities = [] if opportunity is None or (isinstance(opportunity, list) and len(opportunity) == 0): form.errors['opportunities'] = [ 'You must select at least one opportunity!' ] return False # add the email/business name to the session session['email'] = form.data.get('email') session['business_name'] = form.data.get('business_name') # subscribe the vendor to the opportunity vendor = Vendor.query.filter( Vendor.email == form.data.get('email')).first() if vendor is None: vendor = Vendor(email=form.data.get('email'), business_name=form.data.get('business_name')) db.session.add(vendor) db.session.commit() else: vendor.update(business_name=form.data.get('business_name')) if multi: for opp in opportunity: _opp = Opportunity.query.get(int(opp)) if not _opp.is_public: db.session.rollback() form.errors['opportunities'] = ['That\'s not a valid choice.'] return False if _opp in vendor.opportunities: send_email = False else: vendor.opportunities.add(_opp) email_opportunities.append(_opp) else: if opportunity in vendor.opportunities: send_email = False else: vendor.opportunities.add(opportunity) email_opportunities.append(opportunity) if form.data.get('also_categories'): # TODO -- add support for categories pass db.session.commit() current_app.logger.info( u'OPPSIGNUP - Vendor has signed up for opportunities: EMAIL: {email} at BUSINESS: {bis_name} signed up for:\n' + u'OPPORTUNITY: {opportunities}'.format( email=form.data.get('email'), business_name=form.data.get('business_name'), opportunities=', '.join([i.title for i in email_opportunities]))) if send_email: Notification(to_email=vendor.email, from_email=current_app.config['BEACON_SENDER'], subject='Subscription confirmation from Beacon', html_template='opportunities/emails/oppselected.html', txt_template='opportunities/emails/oppselected.txt', opportunities=email_opportunities).send() return True