From b848140e1df7fa4f605aa481aa4cdd1011e7bf36 Mon Sep 17 00:00:00 2001 From: Todd Dembrey <todd.dembrey@torchbox.com> Date: Tue, 6 Aug 2019 15:26:10 +0100 Subject: [PATCH] Feature/gh1329 link project to activity (#1366) * Update the activity model to work with generic foreign key * Update the messaging text to work with the change to sources ref #1329 --- opentech/apply/activity/admin.py | 4 +- opentech/apply/activity/messaging.py | 263 +++++++++--------- .../0028_add_new_generic_relation.py | 48 ++++ .../0029_migrate_old_submission_relation.py | 33 +++ .../migrations/0030_remove_old_relation.py | 17 ++ .../0031_add_generic_fk_to_event.py | 30 ++ ...032_migrate_submission_to_generic_event.py | 33 +++ .../0033_remove_old_submission_fk_event.py | 17 ++ opentech/apply/activity/models.py | 16 +- .../activity/include/listing_base.html | 2 +- .../messages/email/applicant_base.html | 6 +- .../messages/email/batch_ready_to_review.html | 2 +- .../templates/messages/email/comment.html | 2 +- .../messages/email/ready_to_review.html | 4 +- .../templates/messages/email/transition.html | 2 +- .../activity/templatetags/activity_tags.py | 2 +- opentech/apply/activity/tests/factories.py | 4 +- .../apply/activity/tests/test_messaging.py | 56 ++-- opentech/apply/activity/views.py | 8 +- opentech/apply/determinations/views.py | 6 +- opentech/apply/funds/api_views.py | 6 +- opentech/apply/funds/models/submissions.py | 17 +- opentech/apply/funds/models/utils.py | 2 +- opentech/apply/funds/serializers.py | 2 +- .../templates/funds/email/confirmation.html | 8 +- .../funds/tests/views/test_batch_progress.py | 6 +- opentech/apply/funds/views.py | 30 +- opentech/apply/projects/models.py | 7 + opentech/apply/projects/views.py | 2 +- opentech/apply/review/views.py | 8 +- 30 files changed, 431 insertions(+), 212 deletions(-) create mode 100644 opentech/apply/activity/migrations/0028_add_new_generic_relation.py create mode 100644 opentech/apply/activity/migrations/0029_migrate_old_submission_relation.py create mode 100644 opentech/apply/activity/migrations/0030_remove_old_relation.py create mode 100644 opentech/apply/activity/migrations/0031_add_generic_fk_to_event.py create mode 100644 opentech/apply/activity/migrations/0032_migrate_submission_to_generic_event.py create mode 100644 opentech/apply/activity/migrations/0033_remove_old_submission_fk_event.py diff --git a/opentech/apply/activity/admin.py b/opentech/apply/activity/admin.py index eacc4bfbd..11d7238c6 100644 --- a/opentech/apply/activity/admin.py +++ b/opentech/apply/activity/admin.py @@ -12,9 +12,9 @@ class MessageInline(admin.TabularInline): class EventAdmin(admin.ModelAdmin): - list_display = ('type', 'by', 'when', 'submission') + list_display = ('type', 'by', 'when', 'source') list_filter = ('type', 'when') - readonly_fields = ('type', 'submission', 'when', 'by') + readonly_fields = ('type', 'source', 'when', 'by') inlines = (MessageInline,) def has_add_permission(self, request): diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py index b63b1a36e..d071c11dc 100644 --- a/opentech/apply/activity/messaging.py +++ b/opentech/apply/activity/messaging.py @@ -111,38 +111,38 @@ class AdapterBase: def recipients(self, message_type, **kwargs): raise NotImplementedError() - def batch_recipients(self, message_type, submissions, **kwargs): + def batch_recipients(self, message_type, sources, **kwargs): # Default batch recipients is to send a message to each of the recipients that would # receive a message under normal conditions return [ { - 'recipients': self.recipients(message_type, submission=submission, **kwargs), - 'submissions': [submission] + 'recipients': self.recipients(message_type, source=source, **kwargs), + 'sources': [source] } - for submission in submissions + for source in sources ] - def process_batch(self, message_type, events, request, user, submissions, related=None, **kwargs): - events_by_submission = { - event.submission.id: event + def process_batch(self, message_type, events, request, user, sources, related=None, **kwargs): + events_by_source = { + event.source.id: event for event in events } - for recipient in self.batch_recipients(message_type, submissions, **kwargs): + for recipient in self.batch_recipients(message_type, sources, **kwargs): recipients = recipient['recipients'] - submissions = recipient['submissions'] - events = [events_by_submission[submission.id] for submission in submissions] - self.process_send(message_type, recipients, events, request, user, submissions=submissions, submission=None, related=related, **kwargs) + sources = recipient['sources'] + events = [events_by_source[source.id] for source in sources] + self.process_send(message_type, recipients, events, request, user, sources=sources, source=None, related=related, **kwargs) - def process(self, message_type, event, request, user, submission, related=None, **kwargs): - recipients = self.recipients(message_type, submission=submission, related=related, **kwargs) - self.process_send(message_type, recipients, [event], request, user, submission, related=related, **kwargs) + def process(self, message_type, event, request, user, source, related=None, **kwargs): + recipients = self.recipients(message_type, source=source, related=related, **kwargs) + self.process_send(message_type, recipients, [event], request, user, source, related=related, **kwargs) - def process_send(self, message_type, recipients, events, request, user, submission, submissions=list(), related=None, **kwargs): + def process_send(self, message_type, recipients, events, request, user, source, sources=list(), related=None, **kwargs): kwargs = { 'request': request, 'user': user, - 'submission': submission, - 'submissions': submissions, + 'source': source, + 'sources': sources, 'related': related, **kwargs, } @@ -200,10 +200,10 @@ class ActivityAdapter(AdapterBase): messages = { MESSAGES.TRANSITION: 'handle_transition', MESSAGES.BATCH_TRANSITION: 'handle_batch_transition', - MESSAGES.NEW_SUBMISSION: 'Submitted {submission.title} for {submission.page.title}', + MESSAGES.NEW_SUBMISSION: 'Submitted {source.title} for {source.page.title}', MESSAGES.EDIT: 'Edited', MESSAGES.APPLICANT_EDIT: 'Edited', - MESSAGES.UPDATE_LEAD: 'Lead changed from {old_lead} to {submission.lead}', + MESSAGES.UPDATE_LEAD: 'Lead changed from {old_lead} to {source.lead}', MESSAGES.BATCH_UPDATE_LEAD: 'Batch Lead changed to {new_lead}', MESSAGES.DETERMINATION_OUTCOME: 'Sent a determination. Outcome: {determination.clean_outcome}', MESSAGES.BATCH_DETERMINATION_OUTCOME: 'batch_determination', @@ -213,14 +213,14 @@ class ActivityAdapter(AdapterBase): MESSAGES.PARTNERS_UPDATED: 'partners_updated', MESSAGES.NEW_REVIEW: 'Submitted a review', MESSAGES.OPENED_SEALED: 'Opened the submission while still sealed', - MESSAGES.SCREENING: 'Screening status from {old_status} to {submission.screening_status}', - MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {submission}' + MESSAGES.SCREENING: 'Screening status from {old_status} to {source.screening_status}', + MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {source}' } def recipients(self, message_type, **kwargs): return [None] - def extra_kwargs(self, message_type, submission, submissions, **kwargs): + def extra_kwargs(self, message_type, source, sources, **kwargs): from .models import INTERNAL if message_type in [ MESSAGES.OPENED_SEALED, @@ -232,8 +232,8 @@ class ActivityAdapter(AdapterBase): ]: return {'visibility': INTERNAL} - submission = submission or submissions[0] - if is_transition(message_type) and not submission.phase.permissions.can_view(submission.user): + source = source or sources[0] + if is_transition(message_type) and not source.phase.permissions.can_view(source.user): # User's shouldn't see status activity changes for stages that aren't visible to the them return {'visibility': INTERNAL} return {} @@ -259,15 +259,15 @@ class ActivityAdapter(AdapterBase): ]) return ' '.join(base) - def batch_determination(self, submissions, determinations, **kwargs): - submission = submissions[0] + def batch_determination(self, sources, determinations, **kwargs): + submission = sources[0] determination = determinations[submission.id] return self.messages[MESSAGES.DETERMINATION_OUTCOME].format( determination=determination, - submission=submission, ) - def handle_transition(self, old_phase, submission, **kwargs): + def handle_transition(self, old_phase, source, **kwargs): + submission = source base_message = 'Progressed from {old_display} to {new_display}' new_phase = submission.phase @@ -294,11 +294,12 @@ class ActivityAdapter(AdapterBase): return staff_message - def handle_batch_transition(self, transitions, submissions, **kwargs): - kwargs.pop('submission') + def handle_batch_transition(self, transitions, sources, **kwargs): + submissions = sources + kwargs.pop('source') for submission in submissions: old_phase = transitions[submission.id] - return self.handle_transition(old_phase=old_phase, submission=submission, **kwargs) + return self.handle_transition(old_phase=old_phase, source=submission, **kwargs) def partners_updated(self, added, removed, **kwargs): message = ['Partners updated.'] @@ -312,23 +313,24 @@ class ActivityAdapter(AdapterBase): return ' '.join(message) - def send_message(self, message, user, submission, submissions, **kwargs): + def send_message(self, message, user, source, sources, **kwargs): from .models import Activity, PUBLIC visibility = kwargs.get('visibility', PUBLIC) try: # If this was a batch action we want to pull out the submission - submission = submissions[0] + source = sources[0] except IndexError: pass related = kwargs['related'] if isinstance(related, dict): try: - related = related[submission.id] + related = related[source.id] except KeyError: pass + # TODO resolve how related objects work with submission/project has_correct_fields = all(hasattr(related, attr) for attr in ['author', 'submission', 'get_absolute_url']) if has_correct_fields and isinstance(related, models.Model): related_object = related @@ -337,7 +339,7 @@ class ActivityAdapter(AdapterBase): Activity.actions.create( user=user, - submission=submission, + source=source, timestamp=timezone.now(), message=message, visibility=visibility, @@ -349,31 +351,31 @@ class SlackAdapter(AdapterBase): adapter_type = "Slack" always_send = True messages = { - MESSAGES.NEW_SUBMISSION: 'A new submission has been submitted for {submission.page.title}: <{link}|{submission.title}>', - MESSAGES.UPDATE_LEAD: 'The lead of <{link}|{submission.title}> has been updated from {old_lead} to {submission.lead} by {user}', + MESSAGES.NEW_SUBMISSION: 'A new submission has been submitted for {source.page.title}: <{link}|{source.title}>', + MESSAGES.UPDATE_LEAD: 'The lead of <{link}|{source.title}> has been updated from {old_lead} to {source.lead} by {user}', MESSAGES.BATCH_UPDATE_LEAD: 'handle_batch_lead', - MESSAGES.COMMENT: 'A new {comment.visibility} comment has been posted on <{link}|{submission.title}> by {user}', - MESSAGES.EDIT: '{user} has edited <{link}|{submission.title}>', - MESSAGES.APPLICANT_EDIT: '{user} has edited <{link}|{submission.title}>', + MESSAGES.COMMENT: 'A new {comment.visibility} comment has been posted on <{link}|{source.title}> by {user}', + MESSAGES.EDIT: '{user} has edited <{link}|{source.title}>', + MESSAGES.APPLICANT_EDIT: '{user} has edited <{link}|{source.title}>', MESSAGES.REVIEWERS_UPDATED: 'reviewers_updated', MESSAGES.BATCH_REVIEWERS_UPDATED: 'handle_batch_reviewers', - MESSAGES.PARTNERS_UPDATED: '{user} has updated the partners on <{link}|{submission.title}>', - MESSAGES.TRANSITION: '{user} has updated the status of <{link}|{submission.title}>: {old_phase.display_name} → {submission.phase}', + MESSAGES.PARTNERS_UPDATED: '{user} has updated the partners on <{link}|{source.title}>', + MESSAGES.TRANSITION: '{user} has updated the status of <{link}|{source.title}>: {old_phase.display_name} → {source.phase}', MESSAGES.BATCH_TRANSITION: 'handle_batch_transition', - MESSAGES.DETERMINATION_OUTCOME: 'A determination for <{link}|{submission.title}> was sent by email. Outcome: {determination.clean_outcome}', + MESSAGES.DETERMINATION_OUTCOME: 'A determination for <{link}|{source.title}> was sent by email. Outcome: {determination.clean_outcome}', MESSAGES.BATCH_DETERMINATION_OUTCOME: 'handle_batch_determination', - MESSAGES.PROPOSAL_SUBMITTED: 'A proposal has been submitted for review: <{link}|{submission.title}>', - MESSAGES.INVITED_TO_PROPOSAL: '<{link}|{submission.title}> by {submission.user} has been invited to submit a proposal', - MESSAGES.NEW_REVIEW: '{user} has submitted a review for <{link}|{submission.title}>. Outcome: {review.outcome}, Score: {review.get_score_display}', + MESSAGES.PROPOSAL_SUBMITTED: 'A proposal has been submitted for review: <{link}|{source.title}>', + MESSAGES.INVITED_TO_PROPOSAL: '<{link}|{source.title}> by {source.user} has been invited to submit a proposal', + MESSAGES.NEW_REVIEW: '{user} has submitted a review for <{link}|{source.title}>. Outcome: {review.outcome}, Score: {review.get_score_display}', MESSAGES.READY_FOR_REVIEW: 'notify_reviewers', - MESSAGES.OPENED_SEALED: '{user} has opened the sealed submission: <{link}|{submission.title}>', - MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {submission}', + MESSAGES.OPENED_SEALED: '{user} has opened the sealed submission: <{link}|{source.title}>', + MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {source.title}', MESSAGES.BATCH_READY_FOR_REVIEW: 'batch_notify_reviewers', - MESSAGES.DELETE_SUBMISSION: '{user} has deleted {submission.title}', - MESSAGES.DELETE_REVIEW: '{user} has deleted {review.author} review for <{link}|{submission.title}>.', + MESSAGES.DELETE_SUBMISSION: '{user} has deleted {source.title}', + MESSAGES.DELETE_REVIEW: '{user} has deleted {review.author} review for <{link}|{source.title}>.', MESSAGES.CREATED_PROJECT: '{user} has created a Project: <{link}|{project.name}>.', MESSAGES.UPDATE_PROJECT_LEAD: 'The lead of project <{link}|{project.name}> has been updated from {old_lead} to {project.lead} by {user}', - MESSAGES.EDIT_REVIEW: '{user} has edited {review.author} review for <{link}|{submission.title}>.', + MESSAGES.EDIT_REVIEW: '{user} has edited {review.author} review for <{link}|{source.title}>.', } def __init__(self): @@ -381,47 +383,49 @@ class SlackAdapter(AdapterBase): self.destination = settings.SLACK_DESTINATION_URL self.target_room = settings.SLACK_DESTINATION_ROOM - def slack_links(self, links, submissions): + def slack_links(self, links, sources): return ', '.join( - f'<{links[submission.id]}|{submission.title}>' - for submission in submissions + f'<{links[source.id]}|{source.title}>' + for source in sources ) def extra_kwargs(self, message_type, **kwargs): - submission = kwargs['submission'] - submissions = kwargs['submissions'] + source = kwargs['source'] + sources = kwargs['sources'] request = kwargs['request'] - link = link_to(submission, request) + link = link_to(source, request) links = { - submission.id: link_to(submission, request) - for submission in submissions + source.id: link_to(source, request) + for source in sources } return { 'link': link, 'links': links, } - def recipients(self, message_type, submission, related, **kwargs): - recipients = [self.slack_id(submission.lead)] + def recipients(self, message_type, source, related, **kwargs): + recipients = [self.slack_id(source.lead)] # Notify second reviewer when first reviewer is done. if message_type == MESSAGES.NEW_REVIEW and related: + submission = source if submission.assigned.with_roles().count() == 2 and related.author.reviewer == submission.assigned.with_roles().first().reviewer: recipients.append(self.slack_id(submission.assigned.with_roles().last().reviewer)) return recipients - def batch_recipients(self, message_type, submissions, **kwargs): + def batch_recipients(self, message_type, sources, **kwargs): # We group the messages by lead - leads = User.objects.filter(id__in=submissions.values('lead')) + leads = User.objects.filter(id__in=sources.values('lead')) return [ { 'recipients': [self.slack_id(lead)], - 'submissions': submissions.filter(lead=lead), + 'sources': sources.filter(lead=lead), } for lead in leads ] - def reviewers_updated(self, submission, link, user, added=list(), removed=list(), **kwargs): + def reviewers_updated(self, source, link, user, added=list(), removed=list(), **kwargs): + submission = source message = [f'{user} has updated the reviewers on <{link}|{submission.title}>.'] if added: @@ -434,7 +438,8 @@ class SlackAdapter(AdapterBase): return ' '.join(message) - def handle_batch_lead(self, submissions, links, user, new_lead, **kwargs): + def handle_batch_lead(self, sources, links, user, new_lead, **kwargs): + submissions = sources submissions_text = self.slack_links(links, submissions) return ( '{user} has batch changed lead to {new_lead} on: {submissions_text}'.format( @@ -444,7 +449,8 @@ class SlackAdapter(AdapterBase): ) ) - def handle_batch_reviewers(self, submissions, links, user, added, **kwargs): + def handle_batch_reviewers(self, sources, links, user, added, **kwargs): + submissions = sources submissions_text = self.slack_links(links, submissions) reviewers_text = ' '.join([ f'{str(user)} as {role.name},' @@ -459,7 +465,8 @@ class SlackAdapter(AdapterBase): ) ) - def handle_batch_transition(self, user, links, submissions, transitions, **kwargs): + def handle_batch_transition(self, user, links, sources, transitions, **kwargs): + submissions = sources submissions_text = [ ': '.join([ self.slack_links(links, [submission]), @@ -475,7 +482,8 @@ class SlackAdapter(AdapterBase): ) ) - def handle_batch_determination(self, submissions, links, determinations, **kwargs): + def handle_batch_determination(self, sources, links, determinations, **kwargs): + submissions = sources submissions_links = ','.join([ self.slack_links(links, [submission]) for submission in submissions @@ -490,7 +498,8 @@ class SlackAdapter(AdapterBase): ) ) - def notify_reviewers(self, submission, link, **kwargs): + def notify_reviewers(self, source, link, **kwargs): + submission = source reviewers_to_notify = [] for reviewer in submission.reviewers.all(): if submission.phase.permissions.can_review(reviewer): @@ -508,12 +517,12 @@ class SlackAdapter(AdapterBase): ) ) - def batch_notify_reviewers(self, submissions, links, **kwargs): - kwargs.pop('submission') + def batch_notify_reviewers(self, sources, links, **kwargs): + kwargs.pop('source') kwargs.pop('link') return '. '.join( - self.notify_reviewers(submission, link=links[submission.id], **kwargs) - for submission in submissions + self.notify_reviewers(source, link=links[source.id], **kwargs) + for source in sources ) def slack_id(self, user): @@ -521,10 +530,10 @@ class SlackAdapter(AdapterBase): return f'<{user.slack}>' return '' - def slack_channels(self, submission): + def slack_channels(self, source): target_rooms = [self.target_room] try: - extra_rooms = submission.get_from_parent('slack_channel').split(',') + extra_rooms = source.get_from_parent('slack_channel').split(',') except AttributeError: # Not a submission object, no extra rooms. pass @@ -540,8 +549,8 @@ class SlackAdapter(AdapterBase): return target_rooms - def send_message(self, message, recipient, submission, **kwargs): - target_rooms = self.slack_channels(submission) + def send_message(self, message, recipient, source, **kwargs): + target_rooms = self.slack_channels(source) if not self.destination or not any(target_rooms): errors = list() @@ -579,20 +588,21 @@ class EmailAdapter(AdapterBase): MESSAGES.PARTNERS_UPDATED_PARTNER: 'partners_updated_partner', } - def get_subject(self, message_type, submission): - if submission: + def get_subject(self, message_type, source): + if source: if is_ready_for_review(message_type): - subject = 'Application ready to review: {submission.title}'.format(submission=submission) + subject = 'Application ready to review: {submission.title}'.format(submission=source) else: - subject = submission.page.specific.subject or 'Your application to Open Technology Fund: {submission.title}'.format(submission=submission) + subject = source.page.specific.subject or 'Your application to Open Technology Fund: {source.title}'.format(source=source) return subject - def extra_kwargs(self, message_type, submission, submissions, **kwargs): + def extra_kwargs(self, message_type, source, sources, **kwargs): return { - 'subject': self.get_subject(message_type, submission), + 'subject': self.get_subject(message_type, source), } - def handle_transition(self, old_phase, submission, **kwargs): + def handle_transition(self, old_phase, source, **kwargs): + submission = source # Retrive status index to see if we are going forward or backward. old_index = list(dict(PHASES).keys()).index(old_phase.name) target_index = list(dict(PHASES).keys()).index(submission.status) @@ -601,71 +611,73 @@ class EmailAdapter(AdapterBase): if is_forward: return self.render_message( 'messages/email/transition.html', - submission=submission, + source=submission, old_phase=old_phase, **kwargs ) - def handle_batch_transition(self, transitions, submissions, **kwargs): - kwargs.pop('submission') + def handle_batch_transition(self, transitions, sources, **kwargs): + submissions = sources + kwargs.pop('source') for submission in submissions: old_phase = transitions[submission.id] - return self.handle_transition(old_phase=old_phase, submission=submission, **kwargs) + return self.handle_transition(old_phase=old_phase, source=submission, **kwargs) - def batch_determination(self, determinations, submissions, **kwargs): - kwargs.pop('submission') + def batch_determination(self, determinations, sources, **kwargs): + submissions = sources + kwargs.pop('source') for submission in submissions: determination = determinations[submission.id] return self.render_message( 'messages/email/determination.html', - submission=submission, + source=submission, determination=determination, **kwargs ) def notify_comment(self, **kwargs): comment = kwargs['comment'] - submission = kwargs['submission'] - if not comment.priviledged and not comment.user == submission.user: + source = kwargs['source'] + if not comment.priviledged and not comment.user == source.user: return self.render_message('messages/email/comment.html', **kwargs) - def recipients(self, message_type, submission, **kwargs): + def recipients(self, message_type, source, **kwargs): if is_ready_for_review(message_type): - return self.reviewers(submission) + return self.reviewers(source) if is_transition(message_type): # Only notify the applicant if the new phase can be seen within the workflow - if not submission.phase.permissions.can_view(submission.user): + if not source.phase.permissions.can_view(source.user): return [] if message_type == MESSAGES.PARTNERS_UPDATED_PARTNER: partners = kwargs['added'] return [partner.email for partner in partners] - return [submission.user.email] + return [source.user.email] - def batch_recipients(self, message_type, submissions, **kwargs): + def batch_recipients(self, message_type, sources, **kwargs): if not is_ready_for_review(message_type): - return super().batch_recipients(message_type, submissions, **kwargs) + return super().batch_recipients(message_type, sources, **kwargs) reviewers_to_message = defaultdict(list) - for submission in submissions: - reviewers = self.reviewers(submission) + for source in sources: + reviewers = self.reviewers(source) for reviewer in reviewers: - reviewers_to_message[reviewer].append(submission) + reviewers_to_message[reviewer].append(source) return [ { 'recipients': [reviewer], - 'submissions': submissions, - } for reviewer, submissions in reviewers_to_message.items() + 'sources': sources, + } for reviewer, sources in reviewers_to_message.items() ] - def reviewers(self, submission): + def reviewers(self, source): return [ reviewer.email - for reviewer in submission.missing_reviewers.all() - if submission.phase.permissions.can_review(reviewer) and not reviewer.is_apply_staff + for reviewer in source.missing_reviewers.all() + if source.phase.permissions.can_review(reviewer) and not reviewer.is_apply_staff ] def partners_updated_applicant(self, added, removed, **kwargs): @@ -683,12 +695,12 @@ class EmailAdapter(AdapterBase): def render_message(self, template, **kwargs): return render_to_string(template, kwargs) - def send_message(self, message, submission, subject, recipient, logs, **kwargs): + def send_message(self, message, source, subject, recipient, logs, **kwargs): try: send_mail( subject, message, - submission.page.specific.from_address, + source.page.specific.from_address, [recipient], logs=logs ) @@ -706,7 +718,7 @@ class DjangoMessagesAdapter(AdapterBase): MESSAGES.BATCH_DETERMINATION_OUTCOME: 'batch_determinations', } - def batch_reviewers_updated(self, added, submissions, **kwargs): + def batch_reviewers_updated(self, added, sources, **kwargs): reviewers_text = ' '.join([ f'{str(user)} as {role.name},' for role, user in added @@ -717,10 +729,10 @@ class DjangoMessagesAdapter(AdapterBase): 'Batch reviewers added: ' + reviewers_text + ' to ' + - ', '.join(['"{}"'.format(submission.title) for submission in submissions]) + ', '.join(['"{}"'.format(source.title) for source in sources]) ) - def batch_transition(self, submissions, transitions, **kwargs): + def batch_transition(self, sources, transitions, **kwargs): base_message = 'Successfully updated:' transition = '{submission} [{old_display} → {new_display}].' transition_messages = [ @@ -728,12 +740,13 @@ class DjangoMessagesAdapter(AdapterBase): submission=submission.title, old_display=transitions[submission.id], new_display=submission.phase, - ) for submission in submissions + ) for submission in sources ] messages = [base_message, *transition_messages] return ' '.join(messages) - def batch_determinations(self, submissions, determinations, **kwargs): + def batch_determinations(self, sources, determinations, **kwargs): + submissions = sources outcome = determinations[submissions[0].id].clean_outcome base_message = f'Successfully determined as {outcome}: ' @@ -745,10 +758,10 @@ class DjangoMessagesAdapter(AdapterBase): def recipients(self, *args, **kwargs): return [None] - def batch_recipients(self, message_type, submissions, *args, **kwargs): + def batch_recipients(self, message_type, sources, *args, **kwargs): return [{ 'recipients': [None], - 'submissions': submissions, + 'sources': sources, }] def send_message(self, message, request, **kwargs): @@ -762,20 +775,20 @@ class MessengerBackend: def __call__(self, *args, related=None, **kwargs): return self.send(*args, related=related, **kwargs) - def send(self, message_type, request, user, related, submission=None, submissions=list(), **kwargs): + def send(self, message_type, request, user, related, source=None, sources=list(), **kwargs): from .models import Event - if submission: - event = Event.objects.create(type=message_type.name, by=user, submission=submission) + if source: + event = Event.objects.create(type=message_type.name, by=user, source=source) for adapter in self.adapters: - adapter.process(message_type, event, request=request, user=user, submission=submission, related=related, **kwargs) + adapter.process(message_type, event, request=request, user=user, source=source, related=related, **kwargs) - elif submissions: + elif sources: events = Event.objects.bulk_create( - Event(type=message_type.name, by=user, submission=submission) - for submission in submissions + Event(type=message_type.name, by=user, source=source) + for source in sources ) for adapter in self.adapters: - adapter.process_batch(message_type, events, request=request, user=user, submissions=submissions, related=related, **kwargs) + adapter.process_batch(message_type, events, request=request, user=user, sources=sources, related=related, **kwargs) adapters = [ diff --git a/opentech/apply/activity/migrations/0028_add_new_generic_relation.py b/opentech/apply/activity/migrations/0028_add_new_generic_relation.py new file mode 100644 index 000000000..545a19f6e --- /dev/null +++ b/opentech/apply/activity/migrations/0028_add_new_generic_relation.py @@ -0,0 +1,48 @@ +# Generated by Django 2.0.13 on 2019-07-10 17:07 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('activity', '0027_add_update_project_lead'), + ] + + operations = [ + # Updates to the existing GenericForeignKey to related objects + migrations.RenameField( + model_name='activity', + old_name='object_id', + new_name='related_object_id', + ), + migrations.RenameField( + model_name='activity', + old_name='content_type', + new_name='related_content_type', + ), + migrations.AlterField( + model_name='activity', + name='related_content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activity_related', to='contenttypes.ContentType'), + ), + # Add the new generic foreign key + migrations.AddField( + model_name='activity', + name='source_content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activity_source', to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='activity', + name='source_object_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + # Make the submission field nullable + migrations.AlterField( + model_name='activity', + name='submission', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='funds.ApplicationSubmission', related_name='activities', null=True) + ), + ] diff --git a/opentech/apply/activity/migrations/0029_migrate_old_submission_relation.py b/opentech/apply/activity/migrations/0029_migrate_old_submission_relation.py new file mode 100644 index 000000000..1c487b0f8 --- /dev/null +++ b/opentech/apply/activity/migrations/0029_migrate_old_submission_relation.py @@ -0,0 +1,33 @@ +# Generated by Django 2.0.13 on 2019-07-10 17:33 + +from django.db import migrations +from django.db.models import F + + +def submission_to_source(apps, schema_editor): + Activity = apps.get_model('activity', 'Activity') + if Activity.objects.exists(): + ContentType = apps.get_model('contenttypes', 'ContentType') + content_type = ContentType.objects.get(model='applicationsubmission', app_label='funds') + Activity.objects.update( + source_object_id=F('submission_id'), + source_content_type=content_type, + ) + + +def source_to_submission(apps, schema_editor): + Activity = apps.get_model('activity', 'Activity') + Activity.objects.update(submission_id=F('source_object_id')) + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0028_add_new_generic_relation'), + ('funds', '0065_applicationsubmission_meta_categories'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.RunPython(submission_to_source, source_to_submission) + ] diff --git a/opentech/apply/activity/migrations/0030_remove_old_relation.py b/opentech/apply/activity/migrations/0030_remove_old_relation.py new file mode 100644 index 000000000..b52423157 --- /dev/null +++ b/opentech/apply/activity/migrations/0030_remove_old_relation.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.13 on 2019-07-10 17:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0029_migrate_old_submission_relation'), + ] + + operations = [ + migrations.RemoveField( + model_name='activity', + name='submission', + ), + ] diff --git a/opentech/apply/activity/migrations/0031_add_generic_fk_to_event.py b/opentech/apply/activity/migrations/0031_add_generic_fk_to_event.py new file mode 100644 index 000000000..f67b8a245 --- /dev/null +++ b/opentech/apply/activity/migrations/0031_add_generic_fk_to_event.py @@ -0,0 +1,30 @@ +# Generated by Django 2.0.13 on 2019-07-10 22:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('activity', '0030_remove_old_relation'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='submission', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='funds.ApplicationSubmission'), + ), + migrations.AddField( + model_name='event', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='event', + name='object_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/opentech/apply/activity/migrations/0032_migrate_submission_to_generic_event.py b/opentech/apply/activity/migrations/0032_migrate_submission_to_generic_event.py new file mode 100644 index 000000000..137d80d9c --- /dev/null +++ b/opentech/apply/activity/migrations/0032_migrate_submission_to_generic_event.py @@ -0,0 +1,33 @@ +# Generated by Django 2.0.13 on 2019-07-10 22:36 + +from django.db import migrations +from django.db.models import F + + +def submission_to_source(apps, schema_editor): + Event = apps.get_model('activity', 'Event') + if Event.objects.exists(): + ContentType = apps.get_model('contenttypes', 'ContentType') + content_type = ContentType.objects.get(model='applicationsubmission', app_label='funds') + Event.objects.update( + object_id=F('submission_id'), + content_type=content_type, + ) + + +def source_to_submission(apps, schema_editor): + Event = apps.get_model('activity', 'Event') + Event.objects.update(submission_id=F('object_id')) + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0031_add_generic_fk_to_event'), + ('funds', '0065_applicationsubmission_meta_categories'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.RunPython(submission_to_source, source_to_submission) + ] diff --git a/opentech/apply/activity/migrations/0033_remove_old_submission_fk_event.py b/opentech/apply/activity/migrations/0033_remove_old_submission_fk_event.py new file mode 100644 index 000000000..6e76833da --- /dev/null +++ b/opentech/apply/activity/migrations/0033_remove_old_submission_fk_event.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.13 on 2019-07-10 22:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0032_migrate_submission_to_generic_event'), + ] + + operations = [ + migrations.RemoveField( + model_name='event', + name='submission', + ), + ] diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py index 089686efe..c981a73bd 100644 --- a/opentech/apply/activity/models.py +++ b/opentech/apply/activity/models.py @@ -85,7 +85,11 @@ class Activity(models.Model): timestamp = models.DateTimeField() type = models.CharField(choices=ACTIVITY_TYPES.items(), max_length=30) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) - submission = models.ForeignKey('funds.ApplicationSubmission', related_name='activities', on_delete=models.CASCADE) + + source_content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE, related_name='activity_source') + source_object_id = models.PositiveIntegerField(blank=True, null=True) + source = GenericForeignKey('source_content_type', 'source_object_id') + message = models.TextField() visibility = models.CharField(choices=list(VISIBILITY.items()), default=PUBLIC, max_length=10) @@ -95,9 +99,9 @@ class Activity(models.Model): previous = models.ForeignKey("self", on_delete=models.CASCADE, null=True) # Fields for generic relations to other objects. related_object should implement `get_absolute_url` - content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField(blank=True, null=True) - related_object = GenericForeignKey('content_type', 'object_id') + related_content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE, related_name='activity_related') + related_object_id = models.PositiveIntegerField(blank=True, null=True) + related_object = GenericForeignKey('related_content_type', 'related_object_id') objects = models.Manager.from_queryset(ActivityQuerySet)() comments = CommentManger.from_queryset(CommentQueryset)() @@ -139,7 +143,9 @@ class Event(models.Model): when = models.DateTimeField(auto_now_add=True) type = models.CharField(choices=MESSAGES.choices(), max_length=50) by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) - submission = models.ForeignKey('funds.ApplicationSubmission', related_name='+', on_delete=models.CASCADE) + content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField(blank=True, null=True) + source = GenericForeignKey('content_type', 'object_id') def __str__(self): return ' '.join([self.get_type_display(), 'by:', str(self.by), 'on:', self.submission.title]) diff --git a/opentech/apply/activity/templates/activity/include/listing_base.html b/opentech/apply/activity/templates/activity/include/listing_base.html index 95352d4d9..bdc55edf7 100644 --- a/opentech/apply/activity/templates/activity/include/listing_base.html +++ b/opentech/apply/activity/templates/activity/include/listing_base.html @@ -32,7 +32,7 @@ <p class="feed__heading"> {% if submission_title %} - updated <a href="{{ activity.submission.get_absolute_url }}">{{ activity.submission.title }}</a> + updated <a href="{{ activity.source.get_absolute_url }}">{{ activity.source.title }}</a> {% endif %} {% if editable %} diff --git a/opentech/apply/activity/templates/messages/email/applicant_base.html b/opentech/apply/activity/templates/messages/email/applicant_base.html index bc70586b3..d88936a30 100644 --- a/opentech/apply/activity/templates/messages/email/applicant_base.html +++ b/opentech/apply/activity/templates/messages/email/applicant_base.html @@ -1,8 +1,8 @@ {% extends "messages/email/base.html" %} -{% block salutation %}Dear {{ submission.user.get_full_name|default:"applicant" }},{% endblock %} +{% block salutation %}Dear {{ source.user.get_full_name|default:"applicant" }},{% endblock %} -{% block more_info %}You can access your application here: {{ request.scheme }}://{{ request.get_host }}{{ submission.get_absolute_url }} -If you have any questions, please submit them here: {{ request.scheme }}://{{ request.get_host }}{{ submission.get_absolute_url }}#communications +{% block more_info %}You can access your application here: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }} +If you have any questions, please submit them here: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}#communications If you have any issues accessing the submission system or other general inquiries, please email us at hello@opentech.fund {% endblock %} diff --git a/opentech/apply/activity/templates/messages/email/batch_ready_to_review.html b/opentech/apply/activity/templates/messages/email/batch_ready_to_review.html index 25d2aa7a6..19ae54272 100644 --- a/opentech/apply/activity/templates/messages/email/batch_ready_to_review.html +++ b/opentech/apply/activity/templates/messages/email/batch_ready_to_review.html @@ -3,7 +3,7 @@ {% block content %} New proposals have been added to your review list. -{% for submission in submissions %} +{% for submission in sources %} Title: {{ submission.title }} Link: {{ request.scheme }}://{{ request.get_host }}{{ submission.get_absolute_url }} diff --git a/opentech/apply/activity/templates/messages/email/comment.html b/opentech/apply/activity/templates/messages/email/comment.html index c092483e3..79c0cf525 100644 --- a/opentech/apply/activity/templates/messages/email/comment.html +++ b/opentech/apply/activity/templates/messages/email/comment.html @@ -1,5 +1,5 @@ {% extends "messages/email/applicant_base.html" %} -{% block content %}There has been a new comment on your application: {{ submission.title }} +{% block content %}There has been a new comment on your application: {{ source.title }} {{ comment.user }}: {{ comment.message }}{% endblock %} diff --git a/opentech/apply/activity/templates/messages/email/ready_to_review.html b/opentech/apply/activity/templates/messages/email/ready_to_review.html index 1e9b3e67c..98dabcef3 100644 --- a/opentech/apply/activity/templates/messages/email/ready_to_review.html +++ b/opentech/apply/activity/templates/messages/email/ready_to_review.html @@ -4,6 +4,6 @@ {% block content %} A new proposal has been added to your review list. -Title: {{ submission.title }} -Link: {{ request.scheme }}://{{ request.get_host }}{{ submission.get_absolute_url }} +Title: {{ source.title }} +Link: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }} {% endblock %} diff --git a/opentech/apply/activity/templates/messages/email/transition.html b/opentech/apply/activity/templates/messages/email/transition.html index 522c08cf8..4ac07ed0f 100644 --- a/opentech/apply/activity/templates/messages/email/transition.html +++ b/opentech/apply/activity/templates/messages/email/transition.html @@ -1,3 +1,3 @@ {% extends "messages/email/applicant_base.html" %} -{% block content %}Your application has been progressed from {{ old_phase.public_name }} to {{ submission.phase.public_name }}.{% endblock %} +{% block content %}Your application has been progressed from {{ old_phase.public_name }} to {{ source.phase.public_name }}.{% endblock %} diff --git a/opentech/apply/activity/templatetags/activity_tags.py b/opentech/apply/activity/templatetags/activity_tags.py index 627b45bbe..635d5754f 100644 --- a/opentech/apply/activity/templatetags/activity_tags.py +++ b/opentech/apply/activity/templatetags/activity_tags.py @@ -12,7 +12,7 @@ register = template.Library() @register.filter def display_author(activity, user): - if activity.submission.user == user and isinstance(activity.related_object, Review): + if activity.source.user == user and isinstance(activity.related_object, Review): return 'Reviewer' return activity.user diff --git a/opentech/apply/activity/tests/factories.py b/opentech/apply/activity/tests/factories.py index fb312efc0..17bcf29eb 100644 --- a/opentech/apply/activity/tests/factories.py +++ b/opentech/apply/activity/tests/factories.py @@ -16,7 +16,7 @@ class CommentFactory(factory.DjangoModelFactory): internal = factory.Trait(visibility=INTERNAL) reviewers = factory.Trait(visibility=REVIEWER) - submission = factory.SubFactory(ApplicationSubmissionFactory) + source = factory.SubFactory(ApplicationSubmissionFactory) user = factory.SubFactory(UserFactory) message = factory.Faker('sentence') timestamp = factory.LazyFunction(timezone.now) @@ -32,7 +32,7 @@ class EventFactory(factory.DjangoModelFactory): type = factory.Iterator([choice[0] for choice in MESSAGES.choices()]) by = factory.SubFactory(UserFactory) - submission = factory.SubFactory(ApplicationSubmissionFactory) + source = factory.SubFactory(ApplicationSubmissionFactory) class MessageFactory(factory.DjangoModelFactory): diff --git a/opentech/apply/activity/tests/test_messaging.py b/opentech/apply/activity/tests/test_messaging.py index 78c1d19b6..5ecbfe9e4 100644 --- a/opentech/apply/activity/tests/test_messaging.py +++ b/opentech/apply/activity/tests/test_messaging.py @@ -17,6 +17,7 @@ from opentech.apply.funds.tests.factories import ( ) from opentech.apply.review.tests.factories import ReviewFactory from opentech.apply.users.tests.factories import ReviewerFactory, UserFactory +from opentech.apply.projects.tests.factories import ProjectFactory from ..models import Activity, Event, Message, INTERNAL, PUBLIC from ..messaging import ( @@ -56,8 +57,8 @@ class AdapterMixin(TestCase): def process_kwargs(self, message_type, **kwargs): if 'user' not in kwargs: kwargs['user'] = UserFactory() - if 'submission' not in kwargs: - kwargs['submission'] = ApplicationSubmissionFactory() + if 'source' not in kwargs: + kwargs['source'] = ApplicationSubmissionFactory() if 'request' not in kwargs: kwargs['request'] = make_request() if message_type in neat_related: @@ -69,7 +70,8 @@ class AdapterMixin(TestCase): def adapter_process(self, message_type, **kwargs): kwargs = self.process_kwargs(message_type, **kwargs) - self.adapter.process(message_type, event=EventFactory(submission=kwargs['submission']), **kwargs) + event = EventFactory(source=kwargs['source']) + self.adapter.process(message_type, event=event, **kwargs) @override_settings(SEND_MESSAGES=True) @@ -145,7 +147,9 @@ class TestBaseAdapter(AdapterMixin, TestCase): self.assertTrue(self.adapter.adapter_type in messages[0].message) -class TestMessageBackend(TestCase): +class TestMessageBackendApplication(TestCase): + source_factory = ApplicationSubmissionFactory + def setUp(self): self.mocked_adapter = Mock(AdapterBase) self.backend = MessengerBackend @@ -153,7 +157,7 @@ class TestMessageBackend(TestCase): 'related': None, 'request': None, 'user': UserFactory(), - 'submission': ApplicationSubmissionFactory(), + 'source': self.source_factory(), } def test_message_sent_to_adapter(self): @@ -187,6 +191,10 @@ class TestMessageBackend(TestCase): self.assertEqual(Event.objects.first().by, user) +class TestMessageBackendProject(TestMessageBackendApplication): + source_factory = ProjectFactory + + @override_settings(SEND_MESSAGES=True) class TestActivityAdapter(TestCase): def setUp(self): @@ -197,13 +205,13 @@ class TestActivityAdapter(TestCase): user = UserFactory() submission = ApplicationSubmissionFactory() - self.adapter.send_message(message, user=user, submission=submission, submissions=[], related=None) + self.adapter.send_message(message, user=user, source=submission, sources=[], related=None) self.assertEqual(Activity.objects.count(), 1) activity = Activity.objects.first() self.assertEqual(activity.user, user) self.assertEqual(activity.message, message) - self.assertEqual(activity.submission, submission) + self.assertEqual(activity.source, submission) def test_reviewers_message_no_removed(self): assigned_reviewer = AssignedReviewersFactory() @@ -247,13 +255,13 @@ class TestActivityAdapter(TestCase): def test_internal_transition_kwarg_for_invisible_transition(self): submission = ApplicationSubmissionFactory(status='post_review_discussion') - kwargs = self.adapter.extra_kwargs(MESSAGES.TRANSITION, submission=submission, submissions=None) + kwargs = self.adapter.extra_kwargs(MESSAGES.TRANSITION, source=submission, sources=None) self.assertEqual(kwargs['visibility'], INTERNAL) def test_public_transition_kwargs(self): submission = ApplicationSubmissionFactory() - kwargs = self.adapter.extra_kwargs(MESSAGES.TRANSITION, submission=submission, submissions=None) + kwargs = self.adapter.extra_kwargs(MESSAGES.TRANSITION, source=submission, sources=None) self.assertNotIn('visibility', kwargs) @@ -293,7 +301,7 @@ class TestActivityAdapter(TestCase): def test_lead_not_saved_on_activity(self): submission = ApplicationSubmissionFactory() user = UserFactory() - self.adapter.send_message('a message', user=user, submission=submission, submissions=[], related=user) + self.adapter.send_message('a message', user=user, source=submission, sources=[], related=user) activity = Activity.objects.first() self.assertEqual(activity.related_object, None) @@ -302,8 +310,8 @@ class TestActivityAdapter(TestCase): self.adapter.send_message( 'a message', user=review.author.reviewer, - submission=review.submission, - submissions=[], + source=review.submission, + sources=[], related=review, ) activity = Activity.objects.first() @@ -322,7 +330,7 @@ class TestSlackAdapter(AdapterMixin, TestCase): def test_cant_send_with_no_room(self): adapter = SlackAdapter() submission = ApplicationSubmissionFactory() - adapter.send_message('my message', '', submission) + adapter.send_message('my message', '', source=submission) self.assertEqual(len(responses.calls), 0) @override_settings( @@ -333,7 +341,7 @@ class TestSlackAdapter(AdapterMixin, TestCase): def test_cant_send_with_no_url(self): adapter = SlackAdapter() submission = ApplicationSubmissionFactory() - adapter.send_message('my message', '', submission) + adapter.send_message('my message', '', source=submission) self.assertEqual(len(responses.calls), 0) @override_settings( @@ -346,7 +354,7 @@ class TestSlackAdapter(AdapterMixin, TestCase): submission = ApplicationSubmissionFactory() adapter = SlackAdapter() message = 'my message' - adapter.send_message(message, '', submission) + adapter.send_message(message, '', source=submission) self.assertEqual(len(responses.calls), 1) self.assertDictEqual( json.loads(responses.calls[0].request.body), @@ -366,7 +374,7 @@ class TestSlackAdapter(AdapterMixin, TestCase): submission = ApplicationSubmissionFactory(page__slack_channel='dummy') adapter = SlackAdapter() message = 'my message' - adapter.send_message(message, '', submission) + adapter.send_message(message, '', source=submission) self.assertEqual(len(responses.calls), 1) self.assertDictEqual( json.loads(responses.calls[0].request.body), @@ -380,14 +388,14 @@ class TestSlackAdapter(AdapterMixin, TestCase): def test_gets_lead_if_slack_set(self): adapter = SlackAdapter() submission = ApplicationSubmissionFactory() - recipients = adapter.recipients(MESSAGES.COMMENT, submission, related=None) + recipients = adapter.recipients(MESSAGES.COMMENT, source=submission, related=None) self.assertTrue(submission.lead.slack in recipients[0]) @responses.activate def test_gets_blank_if_slack_not_set(self): adapter = SlackAdapter() submission = ApplicationSubmissionFactory(lead__slack='') - recipients = adapter.recipients(MESSAGES.COMMENT, submission, related=None) + recipients = adapter.recipients(MESSAGES.COMMENT, source=submission, related=None) self.assertTrue(submission.lead.slack in recipients[0]) @override_settings( @@ -427,7 +435,7 @@ class TestEmailAdapter(AdapterMixin, TestCase): def test_email_new_submission(self): submission = ApplicationSubmissionFactory() - self.adapter_process(MESSAGES.NEW_SUBMISSION, submission=submission) + self.adapter_process(MESSAGES.NEW_SUBMISSION, source=submission) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to, [submission.user.email]) @@ -435,20 +443,20 @@ class TestEmailAdapter(AdapterMixin, TestCase): def test_no_email_private_comment(self): comment = CommentFactory(internal=True) - self.adapter_process(MESSAGES.COMMENT, related=comment, submission=comment.submission) + self.adapter_process(MESSAGES.COMMENT, related=comment, source=comment.source) self.assertEqual(len(mail.outbox), 0) def test_no_email_own_comment(self): application = ApplicationSubmissionFactory() - comment = CommentFactory(user=application.user, submission=application) + comment = CommentFactory(user=application.user, source=application) - self.adapter_process(MESSAGES.COMMENT, related=comment, user=comment.user, submission=comment.submission) + self.adapter_process(MESSAGES.COMMENT, related=comment, user=comment.user, source=comment.source) self.assertEqual(len(mail.outbox), 0) def test_reviewers_email(self): reviewers = ReviewerFactory.create_batch(4) submission = ApplicationSubmissionFactory(status='external_review', reviewers=reviewers, workflow_stages=2) - self.adapter_process(MESSAGES.READY_FOR_REVIEW, submission=submission) + self.adapter_process(MESSAGES.READY_FOR_REVIEW, source=submission) self.assertEqual(len(mail.outbox), 4) self.assertTrue(mail.outbox[0].subject, 'ready to review') @@ -492,7 +500,7 @@ class TestAnyMailBehaviour(AdapterMixin, TestCase): def test_email_new_submission(self): submission = ApplicationSubmissionFactory() - self.adapter_process(MESSAGES.NEW_SUBMISSION, submission=submission) + self.adapter_process(MESSAGES.NEW_SUBMISSION, source=submission) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to, [submission.user.email]) diff --git a/opentech/apply/activity/views.py b/opentech/apply/activity/views.py index 25b216107..bde2632b4 100644 --- a/opentech/apply/activity/views.py +++ b/opentech/apply/activity/views.py @@ -15,15 +15,15 @@ class AllActivityContextMixin: def get_context_data(self, **kwargs): extra = { 'actions': Activity.actions.filter(submission__in=self.object_list).select_related( - 'submission', + 'source_content_type', 'user', )[:ACTIVITY_LIMIT], 'comments': Activity.comments.filter(submission__in=self.object_list).select_related( - 'submission', + 'source_content_type', 'user', )[:ACTIVITY_LIMIT], 'all_activity': Activity.objects.filter(submission__in=self.object_list).select_related( - 'submission', + 'source_content_type', 'user', )[:ACTIVITY_LIMIT], } @@ -63,7 +63,7 @@ class CommentFormView(DelegatedViewMixin, CreateView): MESSAGES.COMMENT, request=self.request, user=self.request.user, - submission=self.object.submission, + source=self.object.submission, related=self.object, ) return response diff --git a/opentech/apply/determinations/views.py b/opentech/apply/determinations/views.py index 45bfa5290..7b580123c 100644 --- a/opentech/apply/determinations/views.py +++ b/opentech/apply/determinations/views.py @@ -119,7 +119,7 @@ class BatchDeterminationCreateView(CreateView): MESSAGES.BATCH_DETERMINATION_OUTCOME, request=self.request, user=self.request.user, - submissions=submissions.filter(id__in=list(determinations)), + sources=submissions.filter(id__in=list(determinations)), related=determinations, ) @@ -254,7 +254,7 @@ class DeterminationCreateOrUpdateView(CreateOrUpdateView): message=self.object.stripped_message, timestamp=timezone.now(), user=self.request.user, - submission=self.submission, + source=self.submission, related_object=self.object, ) @@ -276,7 +276,7 @@ class DeterminationCreateOrUpdateView(CreateOrUpdateView): MESSAGES.DETERMINATION_OUTCOME, request=self.request, user=self.object.author, - submission=self.object.submission, + source=self.object.submission, related=self.object, ) diff --git a/opentech/apply/funds/api_views.py b/opentech/apply/funds/api_views.py index 1c2c1f3dd..09379c7a7 100644 --- a/opentech/apply/funds/api_views.py +++ b/opentech/apply/funds/api_views.py @@ -163,7 +163,7 @@ class CommentFilter(filters.FilterSet): class AllCommentFilter(CommentFilter): class Meta(CommentFilter.Meta): - fields = CommentFilter.Meta.fields + ['submission'] + fields = CommentFilter.Meta.fields + ['source_object_id'] class CommentList(generics.ListAPIView): @@ -200,13 +200,13 @@ class CommentListCreate(generics.ListCreateAPIView): timestamp=timezone.now(), type=COMMENT, user=self.request.user, - submission_id=self.kwargs['pk'] + source=ApplicationSubmission.objects.get(pk=self.kwargs['pk']) ) messenger( MESSAGES.COMMENT, request=self.request, user=self.request.user, - submission=obj.submission, + source=obj.submission, related=obj, ) diff --git a/opentech/apply/funds/models/submissions.py b/opentech/apply/funds/models/submissions.py index ed3bd7b27..fb164d788 100644 --- a/opentech/apply/funds/models/submissions.py +++ b/opentech/apply/funds/models/submissions.py @@ -2,6 +2,7 @@ import os from functools import partialmethod from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.contrib.postgres.fields import JSONField @@ -140,7 +141,7 @@ class ApplicationSubmissionQueryset(JSONOrderable): return self.exclude(next__isnull=False) def with_latest_update(self): - activities = self.model.activities.field.model + activities = self.model.activities.rel.model latest_activity = activities.objects.filter(submission=OuterRef('id')).select_related('user') return self.annotate( last_user_update=Subquery(latest_activity[:1].values('user__full_name')), @@ -148,7 +149,7 @@ class ApplicationSubmissionQueryset(JSONOrderable): ) def for_table(self, user): - activities = self.model.activities.field.model + activities = self.model.activities.rel.model comments = activities.comments.filter(submission=OuterRef('id')).visible_to(user) roles_for_review = self.model.assigned.field.model.objects.with_roles().filter( submission=OuterRef('id'), reviewer=user) @@ -396,6 +397,12 @@ class ApplicationSubmission( related_name='submissions', blank=True, ) + activities = GenericRelation( + 'activity.Activity', + content_type_field='source_content_type', + object_id_field='source_object_id', + related_query_name='submission', + ) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True) search_data = models.TextField() @@ -745,7 +752,7 @@ def log_status_update(sender, **kwargs): MESSAGES.TRANSITION, user=by, request=request, - submission=instance, + source=instance, related=old_phase, ) @@ -754,7 +761,7 @@ def log_status_update(sender, **kwargs): MESSAGES.READY_FOR_REVIEW, user=by, request=request, - submission=instance, + source=instance, ) if instance.status in STAGE_CHANGE_ACTIONS: @@ -762,7 +769,7 @@ def log_status_update(sender, **kwargs): MESSAGES.INVITED_TO_PROPOSAL, request=request, user=by, - submission=instance, + source=instance, ) diff --git a/opentech/apply/funds/models/utils.py b/opentech/apply/funds/models/utils.py index f6661614f..bc8ef6a91 100644 --- a/opentech/apply/funds/models/utils.py +++ b/opentech/apply/funds/models/utils.py @@ -97,7 +97,7 @@ class WorkflowStreamForm(WorkflowHelpers, AbstractStreamForm): # type: ignore MESSAGES.NEW_SUBMISSION, request=request, user=form_submission.user, - submission=form_submission, + source=form_submission, ) return super().render_landing_page(request, form_submission=None, *args, **kwargs) diff --git a/opentech/apply/funds/serializers.py b/opentech/apply/funds/serializers.py index 54478823b..cd828247a 100644 --- a/opentech/apply/funds/serializers.py +++ b/opentech/apply/funds/serializers.py @@ -208,7 +208,7 @@ class CommentSerializer(serializers.ModelSerializer): class Meta: model = Activity - fields = ('id', 'timestamp', 'user', 'submission', 'message', 'visibility', 'edited', 'edit_url', 'editable') + fields = ('id', 'timestamp', 'user', 'source', 'message', 'visibility', 'edited', 'edit_url', 'editable') def get_message(self, obj): return bleach_value(markdown(obj.message)) diff --git a/opentech/apply/funds/templates/funds/email/confirmation.html b/opentech/apply/funds/templates/funds/email/confirmation.html index 8ce3bbd4c..2defd74c2 100644 --- a/opentech/apply/funds/templates/funds/email/confirmation.html +++ b/opentech/apply/funds/templates/funds/email/confirmation.html @@ -1,11 +1,11 @@ {% extends "messages/email/base.html" %} -{% block content %}{% with email_context=submission.page.specific %}We appreciate your {{ email_context.title }} application submission to the Open Technology Fund. We will review and reply to your submission as quickly as possible. +{% block content %}{% with email_context=source.page.specific %}We appreciate your {{ email_context.title }} application submission to the Open Technology Fund. We will review and reply to your submission as quickly as possible. Our reply will have the next steps for your {{ email_context.title }} application. You can find more information about our support options, review process and selection criteria on our website. {{ email_context.confirmation_text_text }}{% endwith %} -Project name: {{ submission.title }} -Contact name: {{ submission.user.get_full_name }} -Contact email: {{ submission.user.email }}{% endblock %} +Project name: {{ source.title }} +Contact name: {{ source.user.get_full_name }} +Contact email: {{ source.user.email }}{% endblock %} diff --git a/opentech/apply/funds/tests/views/test_batch_progress.py b/opentech/apply/funds/tests/views/test_batch_progress.py index b7784f49f..ac56fbdf7 100644 --- a/opentech/apply/funds/tests/views/test_batch_progress.py +++ b/opentech/apply/funds/tests/views/test_batch_progress.py @@ -98,7 +98,7 @@ class StaffTestCase(BaseBatchProgressViewTestCase): self.post_page(data=self.data(action, [submission])) patched.assert_called_once() _, _, kwargs = patched.mock_calls[0] - self.assertQuerysetEqual(kwargs['submissions'], ApplicationSubmission.objects.none()) + self.assertQuerysetEqual(kwargs['sources'], ApplicationSubmission.objects.none()) @mock.patch('opentech.apply.funds.views.messenger') def test_messenger_with_submission_in_review(self, patched): @@ -107,9 +107,9 @@ class StaffTestCase(BaseBatchProgressViewTestCase): self.post_page(data=self.data(action, [submission])) self.assertEqual(patched.call_count, 2) _, _, kwargs = patched.mock_calls[0] - self.assertCountEqual(kwargs['submissions'], [submission]) + self.assertCountEqual(kwargs['sources'], [submission]) _, _, kwargs = patched.mock_calls[1] - self.assertCountEqual(kwargs['submissions'], [submission]) + self.assertCountEqual(kwargs['sources'], [submission]) class ReivewersTestCase(BaseBatchProgressViewTestCase): diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py index 1a6ce3fd6..81342d12f 100644 --- a/opentech/apply/funds/views.py +++ b/opentech/apply/funds/views.py @@ -120,7 +120,7 @@ class BatchUpdateLeadView(DelegatedViewMixin, FormView): MESSAGES.BATCH_UPDATE_LEAD, request=self.request, user=self.request.user, - submissions=submissions, + sources=submissions, new_lead=new_lead, ) return super().form_valid(form) @@ -147,7 +147,7 @@ class BatchUpdateReviewersView(DelegatedViewMixin, FormView): MESSAGES.BATCH_REVIEWERS_UPDATED, request=self.request, user=self.request.user, - submissions=submissions, + sources=submissions, added=reviewers, ) return super().form_valid(form) @@ -205,7 +205,7 @@ class BatchProgressSubmissionView(DelegatedViewMixin, FormView): MESSAGES.BATCH_TRANSITION, user=self.request.user, request=self.request, - submissions=succeeded_submissions, + sources=succeeded_submissions, related=phase_changes, ) @@ -218,7 +218,7 @@ class BatchProgressSubmissionView(DelegatedViewMixin, FormView): MESSAGES.BATCH_READY_FOR_REVIEW, user=self.request.user, request=self.request, - submissions=succeeded_submissions.filter(status__in=ready_for_review), + sources=succeeded_submissions.filter(status__in=ready_for_review), ) return super().form_valid(form) @@ -389,7 +389,7 @@ class CreateProjectView(DelegatedViewMixin, CreateView): MESSAGES.CREATED_PROJECT, request=self.request, user=self.request.user, - submission=project.submission, + source=project.submission, project=project, ) @@ -410,7 +410,7 @@ class ScreeningSubmissionView(DelegatedViewMixin, UpdateView): MESSAGES.SCREENING, request=self.request, user=self.request.user, - submission=self.object, + source=self.object, related=str(old.screening_status), ) return response @@ -430,7 +430,7 @@ class UpdateLeadView(DelegatedViewMixin, UpdateView): MESSAGES.UPDATE_LEAD, request=self.request, user=self.request.user, - submission=form.instance, + source=form.instance, related=old.lead, ) return response @@ -457,7 +457,7 @@ class UpdateReviewersView(DelegatedViewMixin, UpdateView): MESSAGES.REVIEWERS_UPDATED, request=self.request, user=self.request.user, - submission=self.kwargs['submission'], + source=self.kwargs['submission'], added=added, removed=removed, ) @@ -505,7 +505,7 @@ class UpdatePartnersView(DelegatedViewMixin, UpdateView): MESSAGES.PARTNERS_UPDATED, request=self.request, user=self.request.user, - submission=self.kwargs['submission'], + source=self.kwargs['submission'], added=added, removed=removed, ) @@ -514,7 +514,7 @@ class UpdatePartnersView(DelegatedViewMixin, UpdateView): MESSAGES.PARTNERS_UPDATED_PARTNER, request=self.request, user=self.request.user, - submission=self.kwargs['submission'], + source=self.kwargs['submission'], added=added, removed=removed, ) @@ -659,7 +659,7 @@ class SubmissionSealedView(DetailView): MESSAGES.OPENED_SEALED, request=self.request, user=self.request.user, - submission=submission, + source=submission, ) self.request.session.setdefault('peeked', {})[str(submission.id)] = True # Dictionary updates do not trigger session saves. Force update @@ -737,7 +737,7 @@ class AdminSubmissionEditView(BaseSubmissionEditView): MESSAGES.EDIT, request=self.request, user=self.request.user, - submission=self.object, + source=self.object, related=revision, ) @@ -776,14 +776,14 @@ class ApplicantSubmissionEditView(BaseSubmissionEditView): MESSAGES.PROPOSAL_SUBMITTED, request=self.request, user=self.request.user, - submission=self.object, + source=self.object, ) elif revision: messenger( MESSAGES.APPLICANT_EDIT, request=self.request, user=self.request.user, - submission=self.object, + source=self.object, related=revision, ) @@ -908,7 +908,7 @@ class SubmissionDeleteView(DeleteView): MESSAGES.DELETE_SUBMISSION, user=request.user, request=request, - submission=submission, + source=submission, ) response = super().delete(request, *args, **kwargs) return response diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py index b70627676..ed6ca9530 100644 --- a/opentech/apply/projects/models.py +++ b/opentech/apply/projects/models.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.urls import reverse @@ -14,6 +15,12 @@ class Project(models.Model): contact_address = models.TextField(default='') value = models.DecimalField(default=0, max_digits=10, decimal_places=2) + activities = GenericRelation( + 'activity.Activity', + content_type_field='source_content_type', + object_id_field='source_object_id', + ) + def __str__(self): return self.name diff --git a/opentech/apply/projects/views.py b/opentech/apply/projects/views.py index d77ade136..2c45853e5 100644 --- a/opentech/apply/projects/views.py +++ b/opentech/apply/projects/views.py @@ -28,7 +28,7 @@ class UpdateLeadView(DelegatedViewMixin, UpdateView): MESSAGES.UPDATE_PROJECT_LEAD, request=self.request, user=self.request.user, - submission=form.instance.submission, + source=form.instance.submission, related=old.lead, project=form.instance, ) diff --git a/opentech/apply/review/views.py b/opentech/apply/review/views.py index 15fe39ba7..ffadaa556 100644 --- a/opentech/apply/review/views.py +++ b/opentech/apply/review/views.py @@ -92,7 +92,7 @@ class ReviewEditView(UserPassesTestMixin, BaseStreamForm, UpdateView): MESSAGES.EDIT_REVIEW, user=self.request.user, request=self.request, - submission=review.submission, + source=review.submission, related=review, ) response = super().form_valid(form) @@ -159,7 +159,7 @@ class ReviewCreateOrUpdateView(BaseStreamForm, CreateOrUpdateView): MESSAGES.NEW_REVIEW, request=self.request, user=self.request.user, - submission=self.submission, + source=self.submission, related=self.object, ) @@ -299,7 +299,7 @@ class ReviewOpinionFormView(UserPassesTestMixin, CreateView): MESSAGES.REVIEW_OPINION, request=self.request, user=self.request.user, - submission=self.review.submission, + source=self.review.submission, related=opinion, ) @@ -399,7 +399,7 @@ class ReviewDeleteView(UserPassesTestMixin, DeleteView): MESSAGES.DELETE_REVIEW, user=request.user, request=request, - submission=review.submission, + source=review.submission, related=review, ) response = super().delete(request, *args, **kwargs) -- GitLab