Skip to content
Snippets Groups Projects
messaging.py 11.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • from enum import Enum
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    
    import requests
    
    from django.conf import settings
    
    from django.contrib import messages
    
    from django.template.loader import render_to_string
    
    from .tasks import send_mail
    
    def link_to(target, request):
        return request.scheme + '://' + request.get_host() + target.get_absolute_url()
    
    
    
    class MESSAGES(Enum):
    
        UPDATE_LEAD = 'Update Lead'
    
        APPLICANT_EDIT = "Applicant Edit"
    
        NEW_SUBMISSION = 'New Submission'
        TRANSITION = 'Transition'
        DETERMINATION_OUTCOME = 'Determination Outcome'
        INVITED_TO_PROPOSAL = 'Invited To Proposal'
        REVIEWERS_UPDATED = 'Reviewers Updated'
        READY_FOR_REVIEW = 'Ready For Review'
        NEW_REVIEW = 'New Review'
        COMMENT = 'Comment'
        PROPOSAL_SUBMITTED = 'Proposal Submitted'
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        OPENED_SEALED = 'Opened Sealed Submission'
    
    
        @classmethod
        def choices(cls):
            return [
                (choice.name, choice.value)
                for choice in cls
            ]
    
    class AdapterBase:
        messages = {}
    
        always_send = False
    
    
        def message(self, message_type, **kwargs):
    
            try:
                message = self.messages[message_type]
            except KeyError:
                # We don't know how to handle that message type
                return
    
    
            try:
                # see if its a method on the adapter
                method = getattr(self, message)
            except AttributeError:
    
                return self.render_message(message, **kwargs)
    
                # Delegate all responsibility to the custom method
    
                return method(**kwargs)
    
    
        def render_message(self, message, **kwargs):
            return message.format(**kwargs)
    
    
        def extra_kwargs(self, message_type, **kwargs):
            return {}
    
        def recipients(self, message_type, **kwargs):
            raise NotImplementedError()
    
    
        def process(self, message_type, event, **kwargs):
    
            kwargs.update(self.extra_kwargs(message_type, **kwargs))
    
    
            message = self.message(message_type, **kwargs)
            if not message:
    
            for recipient in self.recipients(message_type, **kwargs):
    
                message_log = self.create_log(message, recipient, event)
    
                if settings.SEND_MESSAGES or self.always_send:
    
                    status = self.send_message(message, recipient=recipient, message_log=message_log, **kwargs)
    
                else:
                    status = 'Message not sent as SEND_MESSAGES==FALSE'
    
    
                message_log.update_status(status)
    
                if not settings.SEND_MESSAGES:
    
                    if recipient:
                        message = '{} [to: {}]: {}'.format(self.adapter_type, recipient, message)
                    else:
                        message = '{}: {}'.format(self.adapter_type, message)
    
                    messages.add_message(kwargs['request'], messages.INFO, message)
    
        def create_log(self, message, recipient, event):
            from .models import Message
            return Message.objects.create(
    
                type=self.adapter_type,
                content=message,
    
                recipient=recipient or '',
    
                event=event,
            )
    
    
        def send_message(self, message, **kwargs):
    
            # Process the message, should return the result of the send
            # Returning None will not record this action
    
            raise NotImplementedError()
    
    
    class ActivityAdapter(AdapterBase):
    
        adapter_type = "Activity Feed"
    
        always_send = True
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            MESSAGES.TRANSITION: 'Progressed from {old_phase.display_name} to {submission.phase}',
    
            MESSAGES.NEW_SUBMISSION: 'Submitted {submission.title} for {submission.page.title}',
    
            MESSAGES.APPLICANT_EDIT: 'Edited',
    
            MESSAGES.UPDATE_LEAD: 'Lead changed from {old.lead} to {submission.lead}',
    
            MESSAGES.DETERMINATION_OUTCOME: 'Sent a determination. Outcome: {determination.clean_outcome}',
    
            MESSAGES.INVITED_TO_PROPOSAL: 'Invited to submit a proposal',
    
            MESSAGES.REVIEWERS_UPDATED: 'reviewers_updated',
    
            MESSAGES.NEW_REVIEW: 'Submitted a review',
            MESSAGES.OPENED_SEALED: 'Opened the submission while still sealed',
    
        def recipients(self, message_type, **kwargs):
            return [None]
    
    
        def extra_kwargs(self, message_type, **kwargs):
            if message_type == MESSAGES.OPENED_SEALED:
                from .models import INTERNAL
                return {'visibility': INTERNAL}
            return {}
    
    
        def reviewers_updated(self, added, removed, **kwargs):
            message = ['Reviewers updated.']
            if added:
                message.append('Added:')
                message.append(', '.join([str(user) for user in added]) + '.')
    
            if removed:
                message.append('Removed:')
                message.append(', '.join([str(user) for user in removed]) + '.')
    
            return ' '.join(message)
    
        def send_message(self, message, user, submission, **kwargs):
    
            from .models import Activity, PUBLIC
            visibility = kwargs.get('visibility', PUBLIC)
    
            Activity.actions.create(
    
                user=user,
                submission=submission,
    
                visibility=visibility,
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    class SlackAdapter(AdapterBase):
    
        adapter_type = "Slack"
    
        always_send = True
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        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.COMMENT: 'A new comment has been posted on <{link}|{submission.title}>',
    
            MESSAGES.EDIT: '{user} has edited <{link}|{submission.title}>',
    
            MESSAGES.APPLICANT_EDIT: '{user} has edited <{link}|{submission.title}>',
    
            MESSAGES.REVIEWERS_UPDATED: '{user} has updated the reviewers on <{link}|{submission.title}>',
    
            MESSAGES.TRANSITION: '{user} has updated the status of <{link}|{submission.title}>: {old_phase.display_name} → {submission.phase}',
    
            MESSAGES.DETERMINATION_OUTCOME: 'A determination for <{link}|{submission.title}> was sent by email. Outcome: {determination.clean_outcome}',
    
            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.score}',
    
            MESSAGES.READY_FOR_REVIEW: 'notify_reviewers',
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            MESSAGES.OPENED_SEALED: '{user} has opened the sealed submission: <{link}|{submission.title}>'
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        }
    
        def __init__(self):
            super().__init__()
    
            self.destination = settings.SLACK_DESTINATION_URL
            self.target_room = settings.SLACK_DESTINATION_ROOM
    
        def extra_kwargs(self, message_type, **kwargs):
            submission = kwargs['submission']
            request = kwargs['request']
            link = link_to(submission, request)
            return {'link': link}
    
    
        def recipients(self, message_type, submission, **kwargs):
    
            return [self.slack_id(submission.lead)]
    
    
        def notify_reviewers(self, submission, **kwargs):
            reviewers_to_notify = []
            for reviewer in submission.reviewers.all():
                if submission.phase.permissions.can_review(reviewer):
                    reviewers_to_notify.append(reviewer)
    
            reviewers = ', '.join(
                self.slack_id(reviewer) or str(reviewer) for reviewer in reviewers_to_notify
            )
    
            return (
                '<{link}|{submission.title}> is ready for review. The following are assigned as reviewers: {reviewers}'.format(
                    reviewers=reviewers,
                    submission=submission,
                    **kwargs
                )
            )
    
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        def slack_id(self, user):
    
            if user.slack:
                return f'<{user.slack}>'
            return ''
    
        def send_message(self, message, recipient, **kwargs):
    
            if not self.destination or not self.target_room:
    
                errors = list()
                if not self.destination:
                    errors.append('Destination URL')
                if not self.target_room:
                    errors.append('Room ID')
                return 'Missing configuration: {}'.format(', '.join(errors))
    
            message = ' '.join([recipient, message]).strip()
    
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            data = {
    
                "room": self.target_room,
    
    Todd Dembrey's avatar
    Todd Dembrey committed
                "message": message,
            }
    
            response = requests.post(self.destination, json=data)
    
            return str(response.status_code) + ': ' + response.content.decode()
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    class EmailAdapter(AdapterBase):
        adapter_type = 'Email'
        messages = {
    
            MESSAGES.NEW_SUBMISSION: 'funds/email/confirmation.html',
    
            MESSAGES.COMMENT: 'notify_comment',
    
            MESSAGES.EDIT: 'messages/email/edit.html',
    
            MESSAGES.TRANSITION: 'messages/email/transition.html',
    
            MESSAGES.DETERMINATION_OUTCOME: 'messages/email/determination.html',
    
            MESSAGES.INVITED_TO_PROPOSAL: 'messages/email/invited_to_proposal.html',
    
            MESSAGES.READY_FOR_REVIEW: 'messages/email/ready_to_review.html',
        }
    
    
        def extra_kwargs(self, message_type, submission, **kwargs):
            if message_type == MESSAGES.READY_FOR_REVIEW:
                subject = 'Application ready to review: {submission.title}'.format(submission=submission)
            else:
                subject = submission.page.specific.subject or 'Your application to Open Technology Fund: {submission.title}'.format(submission=submission)
            return {
                'subject': subject,
            }
    
        def notify_comment(self, **kwargs):
            comment = kwargs['comment']
    
            submission = kwargs['submission']
    
            if not comment.priviledged and not comment.user == submission.user:
    
                return self.render_message('messages/email/comment.html', **kwargs)
    
        def recipients(self, message_type, submission, **kwargs):
            if message_type == MESSAGES.READY_FOR_REVIEW:
                return self.reviewers(submission)
            return [submission.user.email]
    
        def reviewers(self, submission):
    
                reviewer.email
    
                for reviewer in submission.missing_reviewers.all()
    
                if submission.phase.permissions.can_review(reviewer)
            ]
    
    
        def render_message(self, template, **kwargs):
            return render_to_string(template, kwargs)
    
        def send_message(self, message, submission, subject, recipient, **kwargs):
    
                    subject,
                    message,
                    submission.page.specific.from_address,
                    [recipient],
    
                    log=kwargs['message_log']
    
                )
            except Exception as e:
                return 'Error: ' + str(e)
    
    
    class MessengerBackend:
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        def __init__(self, *adpaters):
            self.adapters = adpaters
    
        def __call__(self, message_type, request, user, submission, **kwargs):
            return self.send(message_type, request=request, user=user, submission=submission, **kwargs)
    
        def send(self, message_type, user, submission, **kwargs):
    
            from .models import Event
    
            event = Event.objects.create(type=message_type.name, by=user, submission=submission)
    
            for adapter in self.adapters:
    
                adapter.process(message_type, event, user=user, submission=submission, **kwargs)
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    adapters = [
        ActivityAdapter(),
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        SlackAdapter(),
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        EmailAdapter(),
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    ]
    
    
    messenger = MessengerBackend(*adapters)