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