def test_storm_object_modified_no_edited_fields(self): # A longpoll event is not emitted unless edited_fields is populated. storm_object = FakeStormClass() storm_object.id = 1234 with capture_longpoll_emissions() as log: notify(ObjectModifiedEvent(storm_object, storm_object, None)) self.assertEqual([], log) with capture_longpoll_emissions() as log: notify(ObjectModifiedEvent(storm_object, storm_object, ())) self.assertEqual([], log)
def execute(self, context, current_event): """See IEmailCommand.""" if isinstance(context, CreateBugParams): # No one intentially reports a duplicate bug. Bug email commands # support CreateBugParams, so in this case, just return. return context, current_event self._ensureNumberOfArguments() [bug_id] = self.string_args if bug_id != 'no': try: bug = getUtility(IBugSet).getByNameOrID(bug_id) except NotFoundError: raise EmailProcessingError( get_error_message('no-such-bug.txt', error_templates=error_templates, bug_id=bug_id)) else: # 'no' is a special value for unmarking a bug as a duplicate. bug = None duplicate_field = IBug['duplicateof'].bind(context) try: duplicate_field.validate(bug) except ValidationError as error: raise EmailProcessingError(error.doc()) context_snapshot = Snapshot(context, providing=providedBy(context)) context.markAsDuplicate(bug) current_event = ObjectModifiedEvent(context, context_snapshot, 'duplicateof') notify(current_event) return bug, current_event
def request_action(self, action, data): changed = False recipe_before_modification = Snapshot(self.context, providing=providedBy( self.context)) recipe_text = data.pop('recipe_text') parser = RecipeParser(recipe_text) recipe = parser.parse() if self.context.builder_recipe != recipe: try: self.error_handler(self.context.setRecipeText, recipe_text) changed = True except ErrorHandled: return distros = data.pop('distroseries') if distros != self.context.distroseries: self.context.distroseries.clear() for distroseries_item in distros: self.context.distroseries.add(distroseries_item) changed = True if self.updateContextFromData(data, notify_modified=False): changed = True if changed: field_names = [ form_field.__name__ for form_field in self.form_fields ] notify( ObjectModifiedEvent(self.context, recipe_before_modification, field_names)) self.next_url = canonical_url(self.context)
def updateContextFromData(self, data, context=None, notify_modified=True): """Update the context object based on form data. If no context is given, the view's context is used. If any changes were made, ObjectModifiedEvent will be emitted. This method should be called by an action method of the form. Returns True if there were any changes to apply. """ if context is None: context = self.context if notify_modified: context_before_modification = Snapshot( context, providing=providedBy(context)) was_changed = form.applyChanges(context, self.form_fields, data, self.adapters) if was_changed and notify_modified: field_names = [ form_field.__name__ for form_field in self.form_fields ] notify( ObjectModifiedEvent(context, context_before_modification, field_names)) return was_changed
def test_workitems_added_notification_message(self): """ Test that we get a notification for setting work items on a new specification.""" stub.test_emails = [] spec = self.factory.makeSpecification() old_spec = Snapshot(spec, providing=providedBy(spec)) new_work_item = { 'title': u'A work item', 'status': SpecificationWorkItemStatus.TODO, 'assignee': None, 'milestone': None, 'sequence': 0 } login_person(spec.owner) spec.updateWorkItems([new_work_item]) # For API requests, lazr.restful does the notify() call, for this test # we need to call ourselves. transaction.commit() notify(ObjectModifiedEvent( spec, old_spec, edited_fields=['workitems_text'])) transaction.commit() self.assertEqual(1, len(stub.test_emails)) rationale = 'Work items set to:\nWork items:\n%s: %s' % ( new_work_item['title'], new_work_item['status'].name) [email] = stub.test_emails # Actual message is part 2 of the e-mail. msg = email[2] self.assertIn(rationale, msg)
def linkBug(self, action, data): """Link to the requested bug. Publish an ObjectModifiedEvent and display a notification. """ response = self.request.response target_unmodified = Snapshot(self.context, providing=providedBy(self.context)) bug = data['bug'] try: self.context.linkBug(bug, user=self.user) except Unauthorized: # XXX flacoste 2006-08-23 bug=57470: This should use proper _(). self.setFieldError( 'bug', 'You are not allowed to link to private bug #%d.' % bug.id) return bug_props = {'bugid': bug.id, 'title': bug.title} response.addNotification( _( u'Added link to bug #$bugid: ' u'\N{left double quotation mark}$title' u'\N{right double quotation mark}.', mapping=bug_props)) notify(ObjectModifiedEvent(self.context, target_unmodified, ['bugs'])) self.next_url = canonical_url(self.context)
def remove_action(self, action, data): """Update the bug.""" bug = self.context.bug bug_before_modification = Snapshot(bug, providing=providedBy(bug)) bug.markAsDuplicate(None) notify( ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof')) return self._duplicate_action_result()
def test_no_job_created_if_no_delta(self): """Ensure None is returned if no change has been made.""" merge_proposal, person = self.makeProposalWithSubscriber() old_merge_proposal = Snapshot( merge_proposal, providing=providedBy(merge_proposal)) event = ObjectModifiedEvent( merge_proposal, old_merge_proposal, [], merge_proposal.registrant) merge_proposal_modified(merge_proposal, event) self.assertIs(None, self.getProposalUpdatedEmailJob(merge_proposal))
def test_no_job_created_if_work_in_progress(self): """Ensure None is returned if no change has been made.""" merge_proposal, person = self.makeProposalWithSubscriber( needs_review=False) old_merge_proposal = Snapshot( merge_proposal, providing=providedBy(merge_proposal)) merge_proposal.commit_message = 'new commit message' merge_proposal.description = 'change description' event = ObjectModifiedEvent( merge_proposal, old_merge_proposal, [], merge_proposal.registrant) merge_proposal_modified(merge_proposal, event) self.assertIs(None, self.getProposalUpdatedEmailJob(merge_proposal))
def test_job_created_if_work_in_progress_merged(self): # If work in progress is merged, then that is email worthy. merge_proposal, person = self.makeProposalWithSubscriber( needs_review=False) old_merge_proposal = Snapshot( merge_proposal, providing=providedBy(merge_proposal)) merge_proposal.setStatus(BranchMergeProposalStatus.MERGED) event = ObjectModifiedEvent( merge_proposal, old_merge_proposal, [], merge_proposal.registrant) merge_proposal_modified(merge_proposal, event) job = self.getProposalUpdatedEmailJob(merge_proposal) self.assertIsNot(None, job, 'Job was not created.')
def test_what_changed_works_with_fieldnames(self): # When what_changed is passed an ObjectModifiedEvent with a list # of fieldnames in its edited_fields property, it will deal with # those fields appropriately. bug = self.factory.makeBug() bug_before_modification = Snapshot(bug, providing=IBug) with person_logged_in(bug.owner): bug.setPrivate(True, bug.owner) event = ObjectModifiedEvent(bug, bug_before_modification, ['private']) expected_changes = {'private': ['False', 'True']} changes = what_changed(event) self.assertEqual(expected_changes, changes)
def test_duplicate_edit_notifications(self): # Bug edits for a duplicate are sent to duplicate subscribers only. bug_before_modification = Snapshot(self.dupe_bug, providing=providedBy(self.dupe_bug)) self.dupe_bug.description = 'A changed description' notify( ObjectModifiedEvent(self.dupe_bug, bug_before_modification, ['description'], user=self.dupe_bug.owner)) latest_notification = BugNotification.selectFirst(orderBy='-id') recipients = set(recipient.person for recipient in latest_notification.recipients) self.assertEqual(self.dupe_subscribers, recipients)
def test_what_changed_works_with_field_instances(self): # Sometimes something will pass what_changed an # ObjectModifiedEvent where the edited_fields list contains # field instances. what_changed handles that correctly, too. bug = self.factory.makeBug() bug_before_modification = Snapshot(bug, providing=IBug) with person_logged_in(bug.owner): bug.setPrivate(True, bug.owner) event = ObjectModifiedEvent(bug, bug_before_modification, [IBug['private']]) expected_changes = {'private': ['False', 'True']} changes = what_changed(event) self.assertEqual(expected_changes, changes)
def test_modifyPOTemplate_makes_job(self): """Creating a Packaging should make a TranslationMergeJob.""" potemplate = self.factory.makePOTemplate() finder = JobFinder(None, None, TranslationTemplateChangeJob, potemplate) self.assertEqual([], finder.find()) with person_logged_in(potemplate.owner): snapshot = Snapshot(potemplate, providing=IPOTemplate) potemplate.name = self.factory.getUniqueString() notify(ObjectModifiedEvent(potemplate, snapshot, ["name"])) (job, ) = finder.find() self.assertIsInstance(job, TranslationTemplateChangeJob)
def test_no_job_created_if_only_preview_diff_changed(self): """Ensure None is returned if only the preview diff has changed.""" merge_proposal, person = self.makeProposalWithSubscriber() old_merge_proposal = Snapshot( merge_proposal, providing=providedBy(merge_proposal)) merge_proposal.updatePreviewDiff( ''.join(unified_diff('', 'Fake diff')), unicode(self.factory.getUniqueString('revid')), unicode(self.factory.getUniqueString('revid'))) event = ObjectModifiedEvent( merge_proposal, old_merge_proposal, [], merge_proposal.registrant) merge_proposal_modified(merge_proposal, event) self.assertIs(None, self.getProposalUpdatedEmailJob(merge_proposal))
def expireBugTasks(self, transaction_manager): """Expire old, unassigned, Incomplete BugTasks. Only BugTasks for projects that use Malone are updated. This method will login as the bug_watch_updater celebrity and logout after the expiration is done. """ message_template = ('[Expired for %s because there has been no ' 'activity for %d days.]') self.log.info('Expiring unattended, INCOMPLETE bugtasks older than ' '%d days for projects that use Launchpad Bugs.' % self.days_before_expiration) self._login() try: expired_count = 0 bugtask_set = getUtility(IBugTaskSet) incomplete_bugtasks = bugtask_set.findExpirableBugTasks( self.days_before_expiration, user=self.janitor, target=self.target, limit=self.limit) self.log.info('Found %d bugtasks to expire.' % incomplete_bugtasks.count()) for bugtask in incomplete_bugtasks: # We don't expire bugtasks with conjoined masters. if bugtask.conjoined_master: continue bugtask_before_modification = Snapshot( bugtask, providing=providedBy(bugtask)) bugtask.transitionToStatus(BugTaskStatus.EXPIRED, self.janitor) content = message_template % (bugtask.bugtargetdisplayname, self.days_before_expiration) bugtask.bug.newMessage(owner=self.janitor, subject=bugtask.bug.followup_subject(), content=content) notify( ObjectModifiedEvent(bugtask, bugtask_before_modification, ['status'], user=self.janitor)) # We commit after each expiration because emails are sent # immediately in zopeless. This minimize the risk of # duplicate expiration emails being sent in case an error # occurs later on. transaction_manager.commit() expired_count += 1 self.log.info('Expired %d bugtasks.' % expired_count) finally: self._logout() self.log.info('Finished expiration run.')
def test_assignee_not_a_subscriber(self): """Test that a new recipient being assigned a bug task does send a NEW message.""" self.assertEqual(len(stub.test_emails), 0, 'emails in queue') self.bug_task.transitionToAssignee(self.person_assigned) notify(ObjectModifiedEvent( self.bug_task, self.bug_task_before_modification, ['assignee'], user=self.user)) transaction.commit() self.assertEqual(len(stub.test_emails), 1, 'email not sent') new_message = '[NEW]' msg = stub.test_emails[-1][2] self.assertTrue(new_message in msg, '%s not in \n%s\n' % (new_message, msg))
def notify_modified(proposal, func, *args, **kwargs): """Call func, then notify about the changes it made. :param proposal: the merge proposal to notify about. :param func: The callable that will modify the merge proposal. :param args: Additional arguments for the method. :param kwargs: Keyword arguments for the method. :return: The return value of the method. """ from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta snapshot = BranchMergeProposalNoPreviewDiffDelta.snapshot(proposal) result = func(*args, **kwargs) notify(ObjectModifiedEvent(proposal, snapshot, [])) return result
def change_action(self, action, data): """Update the bug.""" data = dict(data) # We handle duplicate changes by hand instead of leaving it to # the usual machinery because we must use bug.markAsDuplicate(). bug = self.context.bug bug_before_modification = Snapshot(bug, providing=providedBy(bug)) duplicateof = data.pop('duplicateof') bug.markAsDuplicate(duplicateof) notify( ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof')) # Apply other changes. self.updateBugFromData(data) return self._duplicate_action_result()
def test_notifications_for_question_subscribers(self): # Ensure that notifications are sent to subscribers of a # question linked to the expired bug. bugtask = self.bug.default_bugtask bugtask_before_modification = Snapshot(bugtask, providing=IBugTask) bugtask.transitionToStatus(BugTaskStatus.EXPIRED, self.product.owner) bug_modified = ObjectModifiedEvent(bugtask, bugtask_before_modification, ["status"]) notify(bug_modified) recipients = [ job.metadata['recipient_set'] for job in pop_questionemailjobs() ] self.assertContentEqual(['ASKER_SUBSCRIBER'], recipients)
def makeProposalUpdatedEmailJob(self): """Fixture method providing a mailer for a modified merge proposal""" merge_proposal, subscriber = self.makeProposalWithSubscriber() old_merge_proposal = Snapshot( merge_proposal, providing=providedBy(merge_proposal)) merge_proposal.requestReview() merge_proposal.commit_message = 'new commit message' merge_proposal.description = 'change description' event = ObjectModifiedEvent( merge_proposal, old_merge_proposal, [], merge_proposal.registrant) merge_proposal_modified(merge_proposal, event) job = self.getProposalUpdatedEmailJob(merge_proposal) self.assertIsNot(None, job, 'Job was not created.') return job, subscriber
def test_iterReady_supports_updated_emails(self): # iterReady will also return pending MergeProposalUpdatedEmailJob. bmp = self.makeBranchMergeProposal( set_state=BranchMergeProposalStatus.NEEDS_REVIEW) self.completePendingJobs() old_merge_proposal = ( BranchMergeProposalNoPreviewDiffDelta.snapshot(bmp)) bmp.commit_message = 'new commit message' event = ObjectModifiedEvent( bmp, old_merge_proposal, [], bmp.registrant) merge_proposal_modified(bmp, event) [job] = self.job_source.iterReady() self.assertEqual(job.branch_merge_proposal, bmp) self.assertIsInstance(job, MergeProposalUpdatedEmailJob)
def execute(self, context, current_event): """See `IEmailCommand`. Much of this method was lifted from `EditEmailCommand.execute`. """ # Parse args. self._ensureNumberOfArguments() [security_flag] = self.string_args if security_flag == 'yes': security_related = True elif security_flag == 'no': security_related = False else: raise EmailProcessingError(get_error_message( 'security-parameter-mismatch.txt', error_templates=error_templates), stop_processing=True) if isinstance(context, CreateBugParams): if security_related: context.information_type = InformationType.PRIVATESECURITY return context, current_event # Take a snapshot. edited = False edited_fields = set() if IObjectModifiedEvent.providedBy(current_event): context_snapshot = current_event.object_before_modification edited_fields.update(current_event.edited_fields) else: context_snapshot = Snapshot(context, providing=providedBy(context)) # Apply requested changes. user = getUtility(ILaunchBag).user if security_related: if context.setPrivate(True, user): edited = True edited_fields.add('private') if context.security_related != security_related: context.setSecurityRelated(security_related, user) edited = True edited_fields.add('security_related') # Update the current event. if edited and not IObjectCreatedEvent.providedBy(current_event): current_event = ObjectModifiedEvent(context, context_snapshot, list(edited_fields)) return context, current_event
def test_for_bug_modifier_header(self): """Test X-Launchpad-Bug-Modifier appears when a bug is modified.""" self.bug_task.transitionToStatus(BugTaskStatus.CONFIRMED, self.user) notify(ObjectModifiedEvent( self.bug_task, self.bug_task_before_modification, ['status'], user=self.user)) transaction.commit() latest_notification = BugNotification.selectFirst(orderBy='-id') notifications, omitted, messages = construct_email_notifications( [latest_notification]) self.assertEqual(len(notifications), 1, 'email notification not created') headers = [msg['X-Launchpad-Bug-Modifier'] for msg in messages] self.assertEqual(len(headers), len(messages))
def change_action(self, action, data): """Update the bug.""" data = dict(data) bug = self.context.bug information_type = data.pop('information_type') changed_fields = ['information_type'] # When the user first submits the form, validate change is True and # so we check that the bug does not become invisible. If the user # confirms they really want to make the change, validate change is # False and we process the change as normal. if self.request.is_ajax: validate_change = data.get('validate_change', False) if (validate_change and information_type in PRIVATE_INFORMATION_TYPES and self._bug_will_be_invisible(information_type)): self.request.response.setStatus(400, "Bug Visibility") return '' user_will_be_subscribed = ( information_type in PRIVATE_INFORMATION_TYPES and bug.getSubscribersForPerson(self.user).is_empty()) bug_before_modification = Snapshot(bug, providing=providedBy(bug)) changed = bug.transitionToInformationType( information_type, self.user) if changed: self._handlePrivacyChanged(user_will_be_subscribed) notify( ObjectModifiedEvent( bug, bug_before_modification, changed_fields, user=self.user)) if self.request.is_ajax: # Avoid circular imports from lp.bugs.browser.bugtask import ( can_add_package_task_to_bug, can_add_project_task_to_bug, ) if changed: result_data = self._getSubscriptionDetails() result_data['can_add_project_task'] = ( can_add_project_task_to_bug(bug)) result_data['can_add_package_task'] = ( can_add_package_task_to_bug(bug)) self.request.response.setHeader( 'content-type', 'application/json') return dumps( result_data, cls=ResourceJSONEncoder, media_type=EntryResource.JSON_TYPE) else: return ''
def notify_modified(obj, edited_fields, snapshot_names=None, user=None): """A context manager that notifies about modifications to an object. Use this as follows:: with notify_modified(obj, ["attr"]): obj.attr = value Or:: edited_fields = set() with notify_modified(obj, edited_fields): if obj.attr != new_attr: obj.attr = new_attr edited_fields.add("attr") Or even:: edited_fields = set() with notify_modified(obj, edited_fields) as previous_obj: do_something() if obj.attr != previous_obj.attr: edited_fields.add("attr") :param obj: The object being modified. :param edited_fields: An iterable of fields being modified. This is not used until after the block wrapped by the context manager has finished executing, so you can safely pass a mutable object and add items to it from inside the block as you determine which fields are being modified. A notification will only be sent if `edited_fields` is non-empty. :param snapshot_names: If not None, only snapshot these names. This may be used if snapshotting some of the object's attributes is expensive in some contexts (and they can't be wrapped by `doNotSnapshot` for some reason). :param user: If not None, the user making these changes. If None, defaults to the principal registered in the current interaction. """ obj_before_modification = Snapshot(obj, names=snapshot_names, providing=providedBy(obj)) yield obj_before_modification edited_fields = list(edited_fields) if edited_fields: notify( ObjectModifiedEvent(obj, obj_before_modification, edited_fields, user=user))
def monitor(klass, merge_proposal): """Context manager to monitor for changes in a merge proposal. If the merge proposal has changed, an `ObjectModifiedEvent` is issued via `zope.event.notify`. """ merge_proposal_snapshot = klass.snapshot(merge_proposal) yield merge_proposal_delta = klass.construct(merge_proposal_snapshot, merge_proposal) if merge_proposal_delta is not None: merge_proposal_event = ObjectModifiedEvent( merge_proposal, merge_proposal_snapshot, vars(merge_proposal_delta).keys()) notify(merge_proposal_event)
def test_assignee_new_subscriber(self): """Build a list of people who will receive e-mails about the bug task changes and ensure the assignee is not one.""" self.bug_task.transitionToAssignee(self.person_assigned) notify(ObjectModifiedEvent( self.bug_task, self.bug_task_before_modification, ['assignee'], user=self.user)) latest_notification = BugNotification.selectFirst(orderBy='-id') notifications, omitted, messages = construct_email_notifications( [latest_notification]) self.assertEqual(len(notifications), 1, 'email notication not created') receivers = [message['To'] for message in messages] self.assertFalse(self.person_assigned_email in receivers, 'Assignee was emailed about the bug task change')
def test_assignee_notification_message(self): """Test notification string when a person is assigned a task by someone else.""" self.assertEqual(len(stub.test_emails), 0, 'emails in queue') self.bug_task.transitionToAssignee(self.person_assigned) notify(ObjectModifiedEvent( self.bug_task, self.bug_task_before_modification, ['assignee'], user=self.user)) transaction.commit() self.assertEqual(len(stub.test_emails), 1, 'email not sent') rationale = ( 'Sample Person (name12) has assigned this bug to you for Rebirth') msg = stub.test_emails[-1][2] self.assertTrue(rationale in msg, '%s not in\n%s\n' % (rationale, msg))
def test_storm_object_modified(self): storm_object = FakeStormClass() storm_object.id = 1234 with capture_longpoll_emissions() as log: object_event = ObjectModifiedEvent(storm_object, storm_object, ("itchy", "scratchy")) notify(object_event) expected = LongPollEventRecord( "longpoll.event.faketable.1234", { "event_key": "longpoll.event.faketable.1234", "what": "modified", "edited_fields": ["itchy", "scratchy"], "id": 1234, }) self.assertEqual([expected], log)