Skip to content

Index

hypha.apply.activity.adapters

ActivityAdapter

Bases: AdapterBase

adapter_type class-attribute instance-attribute

adapter_type = 'Activity Feed'

always_send class-attribute instance-attribute

always_send = True

messages class-attribute instance-attribute

messages = {TRANSITION: 'handle_transition', BATCH_TRANSITION: 'handle_batch_transition', NEW_SUBMISSION: gettext('Submitted {source.title} for {source.page.title}'), EDIT_SUBMISSION: gettext('Edited'), APPLICANT_EDIT: gettext('Edited'), UPDATE_LEAD: gettext('Lead changed from {old_lead} to {source.lead}'), BATCH_UPDATE_LEAD: gettext('Batch Lead changed to {new_lead}'), DETERMINATION_OUTCOME: gettext('Sent a determination. Outcome: {determination.clean_outcome}'), BATCH_DETERMINATION_OUTCOME: 'batch_determination', INVITED_TO_PROPOSAL: gettext('Invited to submit a proposal'), REVIEWERS_UPDATED: 'reviewers_updated', BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated', PARTNERS_UPDATED: 'partners_updated', NEW_REVIEW: gettext('Submitted a review'), OPENED_SEALED: gettext('Opened the submission while still sealed'), SCREENING: 'handle_screening_statuses', REVIEW_OPINION: gettext('{user} {opinion.opinion_display}s with {opinion.review.author}s review of {source}'), DELETE_REVIEW_OPINION: gettext('{user} deleted the opinion for review: {review_opinion.review}'), CREATED_PROJECT: gettext('Created project'), PROJECT_TRANSITION: 'handle_project_transition', UPDATE_PROJECT_LEAD: gettext('Lead changed from {old_lead} to {source.lead}'), SEND_FOR_APPROVAL: gettext('Requested approval'), APPROVE_PAF: 'handle_paf_assignment', APPROVE_PROJECT: gettext('Approved'), REQUEST_PROJECT_CHANGE: gettext('Requested changes for acceptance: "{comment}"'), SUBMIT_CONTRACT_DOCUMENTS: gettext('Submitted Contract Documents'), UPLOAD_CONTRACT: gettext('Uploaded a {contract.state} contract'), APPROVE_CONTRACT: gettext('Approved contract'), UPDATE_INVOICE_STATUS: 'handle_update_invoice_status', CREATE_INVOICE: gettext('Invoice added'), SUBMIT_REPORT: gettext('Submitted a report'), SKIPPED_REPORT: 'handle_skipped_report', REPORT_FREQUENCY_CHANGED: 'handle_report_frequency', DISABLED_REPORTING: gettext('Reporting disabled'), BATCH_DELETE_SUBMISSION: 'handle_batch_delete_submission', BATCH_ARCHIVE_SUBMISSION: 'handle_batch_archive_submission', BATCH_UPDATE_INVOICE_STATUS: 'handle_batch_update_invoice_status', ARCHIVE_SUBMISSION: gettext('{user} has archived the submission: {source.title}'), UNARCHIVE_SUBMISSION: gettext('{user} has unarchived the submission: {source.title}'), DELETE_INVOICE: gettext('Deleted an invoice')}

message

message(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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)
    else:
        # Delegate all responsibility to the custom method
        return method(**kwargs)

render_message

render_message(message, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def render_message(self, message, **kwargs):
    return message.format(**kwargs)
get_neat_related(message_type, related)
Source code in hypha/apply/activity/adapters/base.py
def get_neat_related(self, message_type, related):
    # We translate the related kwarg into something we can understand
    try:
        neat_name = neat_related[message_type]
    except KeyError:
        # Message type doesn't expect a related object
        if related:
            raise ValueError(
                f"Unexpected 'related' kwarg provided for {message_type}"
            ) from None
        return {}
    else:
        if not related:
            raise ValueError(f"{message_type} expects a 'related' kwarg")
        return {neat_name: related}

batch_recipients

batch_recipients(message_type, sources, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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, source=source, **kwargs),
            "sources": [source],
        }
        for source in sources
    ]

process_batch

process_batch(message_type, events, request, user, sources, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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, sources, user=user, **kwargs
    ):
        recipients = recipient["recipients"]
        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,
        )

process

process(message_type, event, request, user, source, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process(
    self, message_type, event, request, user, source, related=None, **kwargs
):
    recipients = self.recipients(
        message_type,
        source=source,
        related=related,
        user=user,
        request=request,
        **kwargs,
    )
    self.process_send(
        message_type,
        recipients,
        [event],
        request,
        user,
        source,
        related=related,
        **kwargs,
    )

process_send

process_send(message_type, recipients, events, request, user, source, sources=None, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process_send(
    self,
    message_type,
    recipients,
    events,
    request,
    user,
    source,
    sources=None,
    related=None,
    **kwargs,
):
    if sources is None:
        sources = []
    try:
        # If this was a batch action we want to pull out the submission
        source = sources[0]
    except IndexError:
        pass

    kwargs = {
        "request": request,
        "user": user,
        "source": source,
        "sources": sources,
        "related": related,
        **kwargs,
    }
    kwargs.update(self.get_neat_related(message_type, related))
    kwargs.update(self.extra_kwargs(message_type, **kwargs))

    message = self.message(message_type, **kwargs)
    if not message:
        return

    for recipient in recipients:
        message_logs = self.create_logs(message, recipient, *events)

        if settings.SEND_MESSAGES or self.always_send:
            status = self.send_message(
                message, recipient=recipient, logs=message_logs, **kwargs
            )
        else:
            status = "Message not sent as SEND_MESSAGES==FALSE"

        message_logs.update_status(status)

        if not settings.SEND_MESSAGES:
            if recipient:
                debug_message = "{} [to: {}]: {}".format(
                    self.adapter_type, recipient, message
                )
            else:
                debug_message = "{}: {}".format(self.adapter_type, message)
            messages.add_message(request, messages.DEBUG, debug_message)

create_logs

create_logs(message, recipient, *events)
Source code in hypha/apply/activity/adapters/base.py
def create_logs(self, message, recipient, *events):
    from ..models import Message

    messages = Message.objects.bulk_create(
        Message(**self.log_kwargs(message, recipient, event)) for event in events
    )
    return Message.objects.filter(id__in=[message.id for message in messages])

log_kwargs

log_kwargs(message, recipient, event)
Source code in hypha/apply/activity/adapters/base.py
def log_kwargs(self, message, recipient, event):
    return {
        "type": self.adapter_type,
        "content": message,
        "recipient": recipient or "",
        "event": event,
    }

recipients

recipients(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def recipients(self, message_type, **kwargs):
    return [None]

extra_kwargs

extra_kwargs(message_type, source, sources, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def extra_kwargs(self, message_type, source, sources, **kwargs):
    if message_type in [
        MESSAGES.OPENED_SEALED,
        MESSAGES.REVIEWERS_UPDATED,
        MESSAGES.SCREENING,
        MESSAGES.REVIEW_OPINION,
        MESSAGES.DELETE_REVIEW_OPINION,
        MESSAGES.BATCH_REVIEWERS_UPDATED,
        MESSAGES.PARTNERS_UPDATED,
        MESSAGES.APPROVE_PROJECT,
        MESSAGES.REQUEST_PROJECT_CHANGE,
        MESSAGES.SEND_FOR_APPROVAL,
        MESSAGES.APPROVE_PAF,
        MESSAGES.NEW_REVIEW,
        MESSAGES.UPDATE_PROJECT_LEAD,
    ]:
        return {"visibility": TEAM}

    if message_type in [
        MESSAGES.CREATED_PROJECT,
        MESSAGES.APPROVE_CONTRACT,
        MESSAGES.UPLOAD_CONTRACT,
        MESSAGES.SUBMIT_CONTRACT_DOCUMENTS,
        MESSAGES.DELETE_INVOICE,
        MESSAGES.CREATE_INVOICE,
    ]:
        return {"visibility": APPLICANT}

    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": TEAM}

    if message_type == MESSAGES.UPDATE_INVOICE_STATUS:
        invoice = kwargs.get("invoice", None)
        if invoice and not is_invoice_public_transition(invoice):
            return {"visibility": TEAM}
        return {"visibility": APPLICANT}
    return {}

reviewers_updated

reviewers_updated(added=None, removed=None, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def reviewers_updated(self, added=None, removed=None, **kwargs):
    message = [_("Reviewers updated.")]
    if added:
        message.append(_("Added:"))
        message.extend(reviewers_message(added))

    if removed:
        message.append(_("Removed:"))
        message.extend(reviewers_message(removed))

    return " ".join(message)

batch_reviewers_updated

batch_reviewers_updated(added, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def batch_reviewers_updated(self, added, **kwargs):
    base = [_("Batch Reviewers Updated.")]
    base.extend(
        [
            _("{user} as {name}.").format(user=str(user), name=role.name)
            for role, user in added
            if user
        ]
    )
    return " ".join(base)

batch_determination

batch_determination(sources, determinations, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def batch_determination(self, sources, determinations, **kwargs):
    submission = sources[0]
    determination = determinations[submission.id]
    return self.messages[MESSAGES.DETERMINATION_OUTCOME].format(
        determination=determination,
    )

handle_batch_delete_submission

handle_batch_delete_submission(sources, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_batch_delete_submission(self, sources, **kwargs):
    submissions = sources
    submissions_text = ", ".join([submission.title for submission in submissions])
    return _("Successfully deleted submissions: {title}").format(
        title=submissions_text
    )

handle_batch_archive_submission

handle_batch_archive_submission(sources, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_batch_archive_submission(self, sources, **kwargs):
    submissions = sources
    submissions_text = ", ".join([submission.title for submission in submissions])
    return _("Successfully archived submissions: {title}").format(
        title=submissions_text
    )

handle_batch_update_invoice_status

handle_batch_update_invoice_status(sources, invoices, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_batch_update_invoice_status(self, sources, invoices, **kwargs):
    invoice_numbers = ", ".join(
        [
            invoice.invoice_number if invoice.invoice_number else ""
            for invoice in invoices
        ]
    )
    invoice_status = invoices[0].status if invoices else ""
    return _(
        "Successfully updated status to {invoice_status} for invoices: {invoice_numbers}"
    ).format(
        invoice_status=get_invoice_status_display_value(invoice_status),
        invoice_numbers=invoice_numbers,
    )

handle_paf_assignment

handle_paf_assignment(source, paf_approvals, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_paf_assignment(self, source, paf_approvals, **kwargs):
    if hasattr(paf_approvals, "__iter__"):  # paf_approvals has to be iterable
        users = ", ".join(
            [
                paf_approval.user.full_name
                if paf_approval.user.full_name
                else paf_approval.user.username
                for paf_approval in paf_approvals
            ]
        )
        users_sentence = " and".join(users.rsplit(",", 1))
        return _("PAF assigned to {}").format(users_sentence)
    return None

handle_transition

handle_transition(old_phase, source, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_transition(self, old_phase, source, **kwargs):
    submission = source
    base_message = _("Progressed from {old_display} to {new_display}")

    new_phase = submission.phase

    staff_message = base_message.format(
        old_display=old_phase.display_name,
        new_display=new_phase.display_name,
    )

    if new_phase.permissions.can_view(submission.user):
        # we need to provide a different message to the applicant
        if not old_phase.permissions.can_view(submission.user):
            old_phase = submission.workflow.previous_visible(
                old_phase, submission.user
            )

        applicant_message = base_message.format(
            old_display=old_phase.public_name,
            new_display=new_phase.public_name,
        )

        return json.dumps(
            {
                TEAM: staff_message,
                ALL: applicant_message,
            }
        )

    return staff_message

handle_project_transition

handle_project_transition(old_stage, source, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_project_transition(self, old_stage, source, **kwargs):
    project = source
    base_message = _("Progressed from {old_display} to {new_display}")

    staff_message = base_message.format(
        old_display=get_project_status_display_value(old_stage),
        new_display=project.status_display,
    )

    applicant_message = base_message.format(
        old_display=get_project_public_status(project_status=old_stage),
        new_display=get_project_public_status(project_status=project.status),
    )

    return json.dumps(
        {
            TEAM: staff_message,
            ALL: applicant_message,
        }
    )

handle_batch_transition

handle_batch_transition(transitions, sources, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
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, source=submission, **kwargs
        )

partners_updated

partners_updated(added, removed, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def partners_updated(self, added, removed, **kwargs):
    message = [_("Partners 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)

handle_report_frequency

handle_report_frequency(config, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_report_frequency(self, config, **kwargs):
    new_schedule = config.get_frequency_display()
    return _(
        "Updated reporting frequency. New schedule is: {new_schedule} starting on {schedule_start}"
    ).format(new_schedule=new_schedule, schedule_start=config.schedule_start)

handle_skipped_report

handle_skipped_report(report, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_skipped_report(self, report, **kwargs):
    if report.skipped:
        return "Skipped a Report"
    else:
        return "Marked a Report as required"

handle_update_invoice_status

handle_update_invoice_status(invoice, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_update_invoice_status(self, invoice, **kwargs):
    base_message = _("Updated Invoice status to: {invoice_status}.")
    staff_message = base_message.format(invoice_status=invoice.get_status_display())

    if is_invoice_public_transition(invoice):
        public_status = get_invoice_public_status(invoice_status=invoice.status)
        applicant_message = base_message.format(invoice_status=public_status)
        return json.dumps({TEAM: staff_message, ALL: applicant_message})

    return staff_message

send_message

send_message(message, user, source, sources, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def send_message(self, message, user, source, sources, **kwargs):
    from ..models import Activity

    visibility = kwargs.get("visibility", ALL)

    related = kwargs["related"]
    if isinstance(related, dict):
        try:
            related = related[source.id]
        except KeyError:
            pass

    has_correct_fields = all(
        hasattr(related, attr) for attr in ["get_absolute_url"]
    )
    isnt_source = source != related
    is_model = isinstance(related, DjangoModel)
    if has_correct_fields and isnt_source and is_model:
        related_object = related
    else:
        related_object = None

    Activity.actions.create(
        user=user,
        source=source,
        timestamp=timezone.now(),
        message=message,
        visibility=visibility,
        related_object=related_object,
    )

handle_screening_statuses

handle_screening_statuses(source, old_status, **kwargs)
Source code in hypha/apply/activity/adapters/activity_feed.py
def handle_screening_statuses(self, source, old_status, **kwargs):
    new_status = ", ".join([s.title for s in source.screening_statuses.all()])
    return _("Screening decision from {old_status} to {new_status}").format(
        old_status=old_status, new_status=new_status
    )

AdapterBase

messages class-attribute instance-attribute

messages = {}

always_send class-attribute instance-attribute

always_send = False

message

message(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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)
    else:
        # Delegate all responsibility to the custom method
        return method(**kwargs)

render_message

render_message(message, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def render_message(self, message, **kwargs):
    return message.format(**kwargs)

extra_kwargs

extra_kwargs(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def extra_kwargs(self, message_type, **kwargs):
    return {}
get_neat_related(message_type, related)
Source code in hypha/apply/activity/adapters/base.py
def get_neat_related(self, message_type, related):
    # We translate the related kwarg into something we can understand
    try:
        neat_name = neat_related[message_type]
    except KeyError:
        # Message type doesn't expect a related object
        if related:
            raise ValueError(
                f"Unexpected 'related' kwarg provided for {message_type}"
            ) from None
        return {}
    else:
        if not related:
            raise ValueError(f"{message_type} expects a 'related' kwarg")
        return {neat_name: related}

recipients

recipients(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def recipients(self, message_type, **kwargs):
    raise NotImplementedError()

batch_recipients

batch_recipients(message_type, sources, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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, source=source, **kwargs),
            "sources": [source],
        }
        for source in sources
    ]

process_batch

process_batch(message_type, events, request, user, sources, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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, sources, user=user, **kwargs
    ):
        recipients = recipient["recipients"]
        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,
        )

process

process(message_type, event, request, user, source, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process(
    self, message_type, event, request, user, source, related=None, **kwargs
):
    recipients = self.recipients(
        message_type,
        source=source,
        related=related,
        user=user,
        request=request,
        **kwargs,
    )
    self.process_send(
        message_type,
        recipients,
        [event],
        request,
        user,
        source,
        related=related,
        **kwargs,
    )

process_send

process_send(message_type, recipients, events, request, user, source, sources=None, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process_send(
    self,
    message_type,
    recipients,
    events,
    request,
    user,
    source,
    sources=None,
    related=None,
    **kwargs,
):
    if sources is None:
        sources = []
    try:
        # If this was a batch action we want to pull out the submission
        source = sources[0]
    except IndexError:
        pass

    kwargs = {
        "request": request,
        "user": user,
        "source": source,
        "sources": sources,
        "related": related,
        **kwargs,
    }
    kwargs.update(self.get_neat_related(message_type, related))
    kwargs.update(self.extra_kwargs(message_type, **kwargs))

    message = self.message(message_type, **kwargs)
    if not message:
        return

    for recipient in recipients:
        message_logs = self.create_logs(message, recipient, *events)

        if settings.SEND_MESSAGES or self.always_send:
            status = self.send_message(
                message, recipient=recipient, logs=message_logs, **kwargs
            )
        else:
            status = "Message not sent as SEND_MESSAGES==FALSE"

        message_logs.update_status(status)

        if not settings.SEND_MESSAGES:
            if recipient:
                debug_message = "{} [to: {}]: {}".format(
                    self.adapter_type, recipient, message
                )
            else:
                debug_message = "{}: {}".format(self.adapter_type, message)
            messages.add_message(request, messages.DEBUG, debug_message)

create_logs

create_logs(message, recipient, *events)
Source code in hypha/apply/activity/adapters/base.py
def create_logs(self, message, recipient, *events):
    from ..models import Message

    messages = Message.objects.bulk_create(
        Message(**self.log_kwargs(message, recipient, event)) for event in events
    )
    return Message.objects.filter(id__in=[message.id for message in messages])

log_kwargs

log_kwargs(message, recipient, event)
Source code in hypha/apply/activity/adapters/base.py
def log_kwargs(self, message, recipient, event):
    return {
        "type": self.adapter_type,
        "content": message,
        "recipient": recipient or "",
        "event": event,
    }

send_message

send_message(message, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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()

DjangoMessagesAdapter

Bases: AdapterBase

adapter_type class-attribute instance-attribute

adapter_type = 'Django'

always_send class-attribute instance-attribute

always_send = True

messages class-attribute instance-attribute

messages = {BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated', BATCH_TRANSITION: 'batch_transition', BATCH_DETERMINATION_OUTCOME: 'batch_determinations', REMOVE_DOCUMENT: gettext('Successfully removed document'), SKIPPED_REPORT: 'handle_skipped_report', REPORT_FREQUENCY_CHANGED: 'handle_report_frequency', DISABLED_REPORTING: gettext('Reporting disabled'), CREATE_REMINDER: gettext('Reminder created'), DELETE_REMINDER: gettext('Reminder deleted')}

message

message(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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)
    else:
        # Delegate all responsibility to the custom method
        return method(**kwargs)

render_message

render_message(message, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def render_message(self, message, **kwargs):
    return message.format(**kwargs)

extra_kwargs

extra_kwargs(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def extra_kwargs(self, message_type, **kwargs):
    return {}
get_neat_related(message_type, related)
Source code in hypha/apply/activity/adapters/base.py
def get_neat_related(self, message_type, related):
    # We translate the related kwarg into something we can understand
    try:
        neat_name = neat_related[message_type]
    except KeyError:
        # Message type doesn't expect a related object
        if related:
            raise ValueError(
                f"Unexpected 'related' kwarg provided for {message_type}"
            ) from None
        return {}
    else:
        if not related:
            raise ValueError(f"{message_type} expects a 'related' kwarg")
        return {neat_name: related}

process_batch

process_batch(message_type, events, request, user, sources, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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, sources, user=user, **kwargs
    ):
        recipients = recipient["recipients"]
        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,
        )

process

process(message_type, event, request, user, source, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process(
    self, message_type, event, request, user, source, related=None, **kwargs
):
    recipients = self.recipients(
        message_type,
        source=source,
        related=related,
        user=user,
        request=request,
        **kwargs,
    )
    self.process_send(
        message_type,
        recipients,
        [event],
        request,
        user,
        source,
        related=related,
        **kwargs,
    )

process_send

process_send(message_type, recipients, events, request, user, source, sources=None, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process_send(
    self,
    message_type,
    recipients,
    events,
    request,
    user,
    source,
    sources=None,
    related=None,
    **kwargs,
):
    if sources is None:
        sources = []
    try:
        # If this was a batch action we want to pull out the submission
        source = sources[0]
    except IndexError:
        pass

    kwargs = {
        "request": request,
        "user": user,
        "source": source,
        "sources": sources,
        "related": related,
        **kwargs,
    }
    kwargs.update(self.get_neat_related(message_type, related))
    kwargs.update(self.extra_kwargs(message_type, **kwargs))

    message = self.message(message_type, **kwargs)
    if not message:
        return

    for recipient in recipients:
        message_logs = self.create_logs(message, recipient, *events)

        if settings.SEND_MESSAGES or self.always_send:
            status = self.send_message(
                message, recipient=recipient, logs=message_logs, **kwargs
            )
        else:
            status = "Message not sent as SEND_MESSAGES==FALSE"

        message_logs.update_status(status)

        if not settings.SEND_MESSAGES:
            if recipient:
                debug_message = "{} [to: {}]: {}".format(
                    self.adapter_type, recipient, message
                )
            else:
                debug_message = "{}: {}".format(self.adapter_type, message)
            messages.add_message(request, messages.DEBUG, debug_message)

create_logs

create_logs(message, recipient, *events)
Source code in hypha/apply/activity/adapters/base.py
def create_logs(self, message, recipient, *events):
    from ..models import Message

    messages = Message.objects.bulk_create(
        Message(**self.log_kwargs(message, recipient, event)) for event in events
    )
    return Message.objects.filter(id__in=[message.id for message in messages])

log_kwargs

log_kwargs(message, recipient, event)
Source code in hypha/apply/activity/adapters/base.py
def log_kwargs(self, message, recipient, event):
    return {
        "type": self.adapter_type,
        "content": message,
        "recipient": recipient or "",
        "event": event,
    }

batch_reviewers_updated

batch_reviewers_updated(added, sources, **kwargs)
Source code in hypha/apply/activity/adapters/django_messages.py
def batch_reviewers_updated(self, added, sources, **kwargs):
    reviewers_text = " ".join(
        [
            _("{user} as {name},").format(user=str(user), name=role.name)
            for role, user in added
            if user
        ]
    )

    return _("Batch reviewers added: {reviewers_text} to ").format(
        reviewers_text=reviewers_text
    ) + ", ".join(['"{title}"'.format(title=source.title) for source in sources])

handle_report_frequency

handle_report_frequency(config, **kwargs)
Source code in hypha/apply/activity/adapters/django_messages.py
def handle_report_frequency(self, config, **kwargs):
    new_schedule = config.get_frequency_display()
    return _(
        "Successfully updated reporting frequency. They will now report {new_schedule} starting on {schedule_start}"
    ).format(new_schedule=new_schedule, schedule_start=config.schedule_start)

handle_skipped_report

handle_skipped_report(report, **kwargs)
Source code in hypha/apply/activity/adapters/django_messages.py
def handle_skipped_report(self, report, **kwargs):
    if report.skipped:
        return _(
            "Successfully skipped a Report for {start_date} to {end_date}"
        ).format(start_date=report.start_date, end_date=report.end_date)
    else:
        return _(
            "Successfully unskipped a Report for {start_date} to {end_date}"
        ).format(start_date=report.start_date, end_date=report.end_date)

batch_transition

batch_transition(sources, transitions, **kwargs)
Source code in hypha/apply/activity/adapters/django_messages.py
def batch_transition(self, sources, transitions, **kwargs):
    base_message = "Successfully updated:"
    transition = "{submission} [{old_display} → {new_display}]."
    transition_messages = [
        transition.format(
            submission=submission.title,
            old_display=transitions[submission.id],
            new_display=submission.phase,
        )
        for submission in sources
    ]
    messages = [base_message, *transition_messages]
    return " ".join(messages)

batch_determinations

batch_determinations(sources, determinations, **kwargs)
Source code in hypha/apply/activity/adapters/django_messages.py
def batch_determinations(self, sources, determinations, **kwargs):
    submissions = sources
    outcome = determinations[submissions[0].id].clean_outcome

    base_message = _("Successfully determined as {outcome}: ").format(
        outcome=outcome
    )
    submissions_text = [str(submission.title) for submission in submissions]
    return base_message + ", ".join(submissions_text)

recipients

recipients(*args, **kwargs)
Source code in hypha/apply/activity/adapters/django_messages.py
def recipients(self, *args, **kwargs):
    return [None]

batch_recipients

batch_recipients(message_type, sources, *args, **kwargs)
Source code in hypha/apply/activity/adapters/django_messages.py
def batch_recipients(self, message_type, sources, *args, **kwargs):
    return [
        {
            "recipients": [None],
            "sources": sources,
        }
    ]

send_message

send_message(message, request, **kwargs)
Source code in hypha/apply/activity/adapters/django_messages.py
def send_message(self, message, request, **kwargs):
    messages.add_message(request, messages.INFO, message)

EmailAdapter

Bases: AdapterBase

always_send class-attribute instance-attribute

always_send = False

adapter_type class-attribute instance-attribute

adapter_type = 'Email'

messages class-attribute instance-attribute

messages = {NEW_SUBMISSION: 'messages/email/submission_confirmation.html', DRAFT_SUBMISSION: 'messages/email/submission_confirmation.html', COMMENT: 'notify_comment', EDIT_SUBMISSION: 'messages/email/submission_edit.html', TRANSITION: 'handle_transition', BATCH_TRANSITION: 'handle_batch_transition', DETERMINATION_OUTCOME: 'handle_determination', BATCH_DETERMINATION_OUTCOME: 'handle_batch_determination', INVITED_TO_PROPOSAL: 'messages/email/invited_to_proposal.html', BATCH_READY_FOR_REVIEW: 'handle_batch_ready_for_review', READY_FOR_REVIEW: 'handle_ready_for_review', REVIEWERS_UPDATED: 'handle_ready_for_review', BATCH_REVIEWERS_UPDATED: 'handle_batch_ready_for_review', PARTNERS_UPDATED: 'partners_updated_applicant', PARTNERS_UPDATED_PARTNER: 'partners_updated_partner', UPLOAD_CONTRACT: 'messages/email/contract_uploaded.html', SUBMIT_CONTRACT_DOCUMENTS: 'messages/email/submit_contract_documents.html', CREATED_PROJECT: 'handle_project_created', UPDATED_VENDOR: 'handle_vendor_updated', SENT_TO_COMPLIANCE: 'messages/email/sent_to_compliance.html', REQUEST_PROJECT_CHANGE: 'messages/email/project_request_change.html', ASSIGN_PAF_APPROVER: 'messages/email/assign_paf_approvers.html', APPROVE_PAF: 'messages/email/paf_for_approval.html', UPDATE_INVOICE: 'handle_invoice_updated', UPDATE_INVOICE_STATUS: 'handle_invoice_status_updated', APPROVE_INVOICE: 'messages/email/invoice_approved.html', SUBMIT_REPORT: 'messages/email/report_submitted.html', SKIPPED_REPORT: 'messages/email/report_skipped.html', REPORT_FREQUENCY_CHANGED: 'messages/email/report_frequency.html', REPORT_NOTIFY: 'messages/email/report_notify.html', REVIEW_REMINDER: 'messages/email/ready_to_review.html', PROJECT_TRANSITION: 'handle_project_transition'}

message

message(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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)
    else:
        # Delegate all responsibility to the custom method
        return method(**kwargs)
get_neat_related(message_type, related)
Source code in hypha/apply/activity/adapters/base.py
def get_neat_related(self, message_type, related):
    # We translate the related kwarg into something we can understand
    try:
        neat_name = neat_related[message_type]
    except KeyError:
        # Message type doesn't expect a related object
        if related:
            raise ValueError(
                f"Unexpected 'related' kwarg provided for {message_type}"
            ) from None
        return {}
    else:
        if not related:
            raise ValueError(f"{message_type} expects a 'related' kwarg")
        return {neat_name: related}

process_batch

process_batch(message_type, events, request, user, sources, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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, sources, user=user, **kwargs
    ):
        recipients = recipient["recipients"]
        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,
        )

process

process(message_type, event, request, user, source, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process(
    self, message_type, event, request, user, source, related=None, **kwargs
):
    recipients = self.recipients(
        message_type,
        source=source,
        related=related,
        user=user,
        request=request,
        **kwargs,
    )
    self.process_send(
        message_type,
        recipients,
        [event],
        request,
        user,
        source,
        related=related,
        **kwargs,
    )

process_send

process_send(message_type, recipients, events, request, user, source, sources=None, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process_send(
    self,
    message_type,
    recipients,
    events,
    request,
    user,
    source,
    sources=None,
    related=None,
    **kwargs,
):
    if sources is None:
        sources = []
    try:
        # If this was a batch action we want to pull out the submission
        source = sources[0]
    except IndexError:
        pass

    kwargs = {
        "request": request,
        "user": user,
        "source": source,
        "sources": sources,
        "related": related,
        **kwargs,
    }
    kwargs.update(self.get_neat_related(message_type, related))
    kwargs.update(self.extra_kwargs(message_type, **kwargs))

    message = self.message(message_type, **kwargs)
    if not message:
        return

    for recipient in recipients:
        message_logs = self.create_logs(message, recipient, *events)

        if settings.SEND_MESSAGES or self.always_send:
            status = self.send_message(
                message, recipient=recipient, logs=message_logs, **kwargs
            )
        else:
            status = "Message not sent as SEND_MESSAGES==FALSE"

        message_logs.update_status(status)

        if not settings.SEND_MESSAGES:
            if recipient:
                debug_message = "{} [to: {}]: {}".format(
                    self.adapter_type, recipient, message
                )
            else:
                debug_message = "{}: {}".format(self.adapter_type, message)
            messages.add_message(request, messages.DEBUG, debug_message)

create_logs

create_logs(message, recipient, *events)
Source code in hypha/apply/activity/adapters/base.py
def create_logs(self, message, recipient, *events):
    from ..models import Message

    messages = Message.objects.bulk_create(
        Message(**self.log_kwargs(message, recipient, event)) for event in events
    )
    return Message.objects.filter(id__in=[message.id for message in messages])

log_kwargs

log_kwargs(message, recipient, event)
Source code in hypha/apply/activity/adapters/base.py
def log_kwargs(self, message, recipient, event):
    return {
        "type": self.adapter_type,
        "content": message,
        "recipient": recipient or "",
        "event": event,
    }

get_subject

get_subject(message_type, source)
Source code in hypha/apply/activity/adapters/emails.py
def get_subject(self, message_type, source):
    if source and hasattr(source, "title"):
        if is_ready_for_review(message_type) or is_reviewer_update(message_type):
            subject = _("Application ready to review: {source.title}").format(
                source=source
            )
            if message_type in {
                MESSAGES.BATCH_READY_FOR_REVIEW,
                MESSAGES.BATCH_REVIEWERS_UPDATED,
            }:
                subject = _("Multiple applications are now ready for your review")
        elif message_type in {MESSAGES.REVIEW_REMINDER}:
            subject = _(
                "Reminder: Application ready to review: {source.title}"
            ).format(source=source)
        elif message_type in [
            MESSAGES.SENT_TO_COMPLIANCE,
            MESSAGES.APPROVE_PAF,
        ]:
            subject = _("Project is waiting for approval: {source.title}").format(
                source=source
            )
        elif message_type == MESSAGES.UPLOAD_CONTRACT:
            subject = _("Contract uploaded for the project: {source.title}").format(
                source=source
            )
        elif message_type == MESSAGES.SUBMIT_CONTRACT_DOCUMENTS:
            subject = _(
                "Contract Documents required approval for the project: {source.title}"
            ).format(source=source)
        elif message_type == MESSAGES.PROJECT_TRANSITION:
            from hypha.apply.projects.models.project import (
                CONTRACTING,
                INVOICING_AND_REPORTING,
            )

            if source.status == CONTRACTING:
                subject = _(
                    "Project is waiting for the contract: {source.title}"
                ).format(source=source)
            elif source.status == INVOICING_AND_REPORTING:
                subject = _(
                    "Project is ready for invoicing: {source.title}"
                ).format(source=source)
            else:
                subject = _(
                    "Project status has changed to {project_status}: {source.title}"
                ).format(
                    project_status=display_project_status(source, source.user),
                    source=source,
                )
        elif message_type == MESSAGES.REQUEST_PROJECT_CHANGE:
            subject = _("Project has been rejected, please update and resubmit")
        elif message_type == MESSAGES.ASSIGN_PAF_APPROVER:
            subject = _(
                "Project documents are ready to be assigned for approval: {source.title}".format(
                    source=source
                )
            )
        else:
            try:
                subject = source.page.specific.subject or _(
                    "Your application to {org_long_name}: {source.title}"
                ).format(org_long_name=settings.ORG_LONG_NAME, source=source)
            except AttributeError:
                subject = _("Your {org_long_name} Project: {source.title}").format(
                    org_long_name=settings.ORG_LONG_NAME, source=source
                )
        return subject

extra_kwargs

extra_kwargs(message_type, source, sources, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def extra_kwargs(self, message_type, source, sources, **kwargs):
    return {
        "subject": self.get_subject(message_type, source),
    }

handle_transition

handle_transition(old_phase, source, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_transition(self, old_phase, source, **kwargs):
    from hypha.apply.funds.workflow import PHASES

    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)
    is_forward = old_index < target_index

    if is_forward:
        return self.render_message(
            "messages/email/transition.html",
            source=submission,
            old_phase=old_phase,
            **kwargs,
        )

handle_batch_transition

handle_batch_transition(transitions, sources, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
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, source=submission, **kwargs
        )

handle_project_transition

handle_project_transition(source, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_project_transition(self, source, **kwargs):
    from hypha.apply.projects.models.project import (
        CONTRACTING,
        INVOICING_AND_REPORTING,
    )

    if source.status == CONTRACTING:
        return self.render_message(
            "messages/email/ready_for_contracting.html",
            source=source,
            **kwargs,
        )

    if source.status == INVOICING_AND_REPORTING:
        return self.render_message(
            "messages/email/ready_for_invoicing.html",
            source=source,
            **kwargs,
        )

handle_invoice_status_updated

handle_invoice_status_updated(related, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_invoice_status_updated(self, related, **kwargs):
    return self.render_message(
        "messages/email/invoice_status_updated.html",
        has_changes_requested=related.has_changes_requested,
        **kwargs,
    )

handle_invoice_updated

handle_invoice_updated(**kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_invoice_updated(self, **kwargs):
    return self.render_message(
        "messages/email/invoice_updated.html",
        **kwargs,
    )

handle_project_created

handle_project_created(source, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_project_created(self, source, **kwargs):
    from hypha.apply.projects.models import ProjectSettings

    request = kwargs.get("request")
    project_settings = ProjectSettings.for_request(request)
    if project_settings.vendor_setup_required:
        return self.render_message(
            "messages/email/vendor_setup_needed.html", source=source, **kwargs
        )

handle_vendor_updated

handle_vendor_updated(source, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_vendor_updated(self, source, **kwargs):
    return self.render_message(
        "messages/email/vendor_updated.html",
        source=source,
        **kwargs,
    )

handle_determination

handle_determination(determination, source, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_determination(self, determination, source, **kwargs):
    submission = source
    if determination.send_notice:
        return self.render_message(
            "messages/email/determination.html",
            source=submission,
            determination=determination,
            **kwargs,
        )

handle_batch_determination

handle_batch_determination(determinations, sources, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_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",
            source=submission,
            determination=determination,
            **kwargs,
        )

handle_ready_for_review

handle_ready_for_review(request, source, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_ready_for_review(self, request, source, **kwargs):
    if settings.SEND_READY_FOR_REVIEW:
        return self.render_message(
            "messages/email/ready_to_review.html",
            source=source,
            request=request,
            **kwargs,
        )

handle_batch_ready_for_review

handle_batch_ready_for_review(request, sources, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def handle_batch_ready_for_review(self, request, sources, **kwargs):
    if settings.SEND_READY_FOR_REVIEW:
        return self.render_message(
            "messages/email/batch_ready_to_review.html",
            sources=sources,
            request=request,
            **kwargs,
        )

notify_comment

notify_comment(**kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def notify_comment(self, **kwargs):
    comment = kwargs["comment"]
    source = kwargs["source"]
    if not comment.priviledged and not comment.user == source.user:
        return self.render_message("messages/email/comment.html", **kwargs)

recipients

recipients(message_type, source, user, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def recipients(self, message_type, source, user, **kwargs):
    if is_ready_for_review(message_type):
        return self.reviewers(source)

    if is_reviewer_update(message_type):
        # Notify newly added reviewers only if they can review in the current phase
        reviewers = self.reviewers(source)
        added = kwargs.get("added", [])
        return [
            assigned_reviewer.reviewer.email
            for assigned_reviewer in added
            if assigned_reviewer.reviewer.email in reviewers
        ]

    if is_transition(message_type):
        # Only notify the applicant if the new phase can be seen within the workflow
        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]

    if message_type == MESSAGES.APPROVE_PAF:
        from hypha.apply.projects.models.project import ProjectSettings

        # notify the assigned approvers
        request = kwargs.get("request")
        project_settings = ProjectSettings.for_request(request)
        if project_settings.paf_approval_sequential:
            next_paf_approval = source.paf_approvals.filter(approved=False).first()
            if next_paf_approval and next_paf_approval.user:
                return [next_paf_approval.user.email]
        return list(
            filter(
                lambda approver: approver is not None,
                source.paf_approvals.filter(approved=False).values_list(
                    "user__email", flat=True
                ),
            )
        )

    if message_type == MESSAGES.ASSIGN_PAF_APPROVER:
        from hypha.apply.projects.models.project import ProjectSettings

        # notify PAFReviewerRole's groups' users to assign approvers
        request = kwargs.get("request")
        project_settings = ProjectSettings.for_request(request)
        if project_settings.paf_approval_sequential:
            next_paf_approval = source.paf_approvals.filter(approved=False).first()
            if next_paf_approval and not next_paf_approval.user:
                assigners = get_users_for_groups(
                    list(next_paf_approval.paf_reviewer_role.user_roles.all()),
                    exact_match=True,
                )
                return [assigner.email for assigner in assigners]

        assigners_emails = []
        if user == source.lead:
            for approval in source.paf_approvals.filter(
                approved=False, user__isnull=True
            ):
                assigners_emails.extend(
                    [
                        assigner.email
                        for assigner in get_users_for_groups(
                            list(approval.paf_reviewer_role.user_roles.all()),
                            exact_match=True,
                        )
                    ]
                )
        else:
            assigners_emails.extend(
                [
                    assigner.email
                    for assigner in get_users_for_groups(
                        list(user.groups.all()), exact_match=True
                    )
                ]
            )
        return set(assigners_emails)

    if message_type == MESSAGES.REQUEST_PROJECT_CHANGE:
        return [source.lead.email]

    if message_type == MESSAGES.SENT_TO_COMPLIANCE:
        return get_compliance_email(
            target_user_gps=[
                CONTRACTING_GROUP_NAME,
                FINANCE_GROUP_NAME,
                STAFF_GROUP_NAME,
            ]
        )

    if message_type == MESSAGES.SUBMIT_CONTRACT_DOCUMENTS:
        return get_compliance_email(target_user_gps=[STAFF_GROUP_NAME])

    if message_type in {MESSAGES.SUBMIT_REPORT, MESSAGES.UPDATE_INVOICE}:
        # Don't tell the user if they did these activities
        if user.is_applicant:
            return []

    if message_type in {MESSAGES.REVIEW_REMINDER}:
        return self.reviewers(source)

    if message_type == MESSAGES.UPDATE_INVOICE_STATUS:
        related = kwargs.get("related", None)
        if related:
            if related.status in {CHANGES_REQUESTED_BY_STAFF, DECLINED}:
                return [source.user.email]
        return []

    if message_type == MESSAGES.PROJECT_TRANSITION:
        from hypha.apply.projects.models.project import (
            CONTRACTING,
            INVOICING_AND_REPORTING,
        )

        if source.status == CONTRACTING:
            if settings.STAFF_UPLOAD_CONTRACT:
                return get_compliance_email(
                    target_user_gps=[CONTRACTING_GROUP_NAME, STAFF_GROUP_NAME]
                )
            return get_compliance_email(target_user_gps=[CONTRACTING_GROUP_NAME])
        if source.status == INVOICING_AND_REPORTING:
            return [source.user.email]

    if message_type == MESSAGES.APPROVE_INVOICE:
        if user.is_apply_staff:
            return get_compliance_email(target_user_gps=[FINANCE_GROUP_NAME])
        if settings.INVOICE_EXTENDED_WORKFLOW and user.is_finance_level_1:
            finance_2_users_email = (
                User.objects.active()
                .filter(groups__name=FINANCE_GROUP_NAME)
                .filter(groups__name=APPROVER_GROUP_NAME)
                .values_list("email", flat=True)
            )
            return finance_2_users_email
        return []

    if isinstance(source, get_user_model()):
        return user.email
    return [source.user.email]

batch_recipients

batch_recipients(message_type, sources, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def batch_recipients(self, message_type, sources, **kwargs):
    if not (is_ready_for_review(message_type) or is_reviewer_update(message_type)):
        return super().batch_recipients(message_type, sources, **kwargs)

    added = [reviewer.email for _, reviewer in kwargs.get("added", []) if reviewer]

    reviewers_to_message = defaultdict(list)
    for source in sources:
        reviewers = self.reviewers(source)
        for reviewer in reviewers:
            if not is_reviewer_update(message_type) or reviewer in added:
                reviewers_to_message[reviewer].append(source)

    return [
        {
            "recipients": [reviewer],
            "sources": sources,
        }
        for reviewer, sources in reviewers_to_message.items()
    ]

reviewers

reviewers(source)
Source code in hypha/apply/activity/adapters/emails.py
def reviewers(self, source):
    return [
        reviewer.email
        for reviewer in source.missing_reviewers.all()
        if source.phase.permissions.can_review(reviewer)
        and not reviewer.is_apply_staff
    ]

partners_updated_applicant

partners_updated_applicant(added, removed, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def partners_updated_applicant(self, added, removed, **kwargs):
    if added:
        return self.render_message(
            "messages/email/partners_update_applicant.html", added=added, **kwargs
        )

partners_updated_partner

partners_updated_partner(added, removed, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def partners_updated_partner(self, added, removed, **kwargs):
    for _partner in added:
        return self.render_message(
            "messages/email/partners_update_partner.html", **kwargs
        )

render_message

render_message(template, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def render_message(self, template, **kwargs):
    with language(settings.LANGUAGE_CODE):
        text = render_to_string(template, kwargs, kwargs["request"])

    return remove_extra_empty_lines(text)

send_message

send_message(message, source, subject, recipient, logs, **kwargs)
Source code in hypha/apply/activity/adapters/emails.py
def send_message(self, message, source, subject, recipient, logs, **kwargs):
    try:
        from_email = source.page.specific.from_address
    except AttributeError:  # we're dealing with a project
        from_email = source.submission.page.specific.from_address
    except Exception as e:
        from_email = None
        logger.exception(e)

    try:
        send_mail(subject, message, from_email, [recipient], logs=logs)
    except Exception as e:
        return "Error: " + str(e)

SlackAdapter

SlackAdapter()

Bases: AdapterBase

Notification Adaptor for internal staff on the configured slack channels.

Source code in hypha/apply/activity/adapters/slack.py
def __init__(self):
    super().__init__()
    self.destination = settings.SLACK_ENDPOINT_URL
    self.target_room = settings.SLACK_DESTINATION_ROOM
    self.comments_room = settings.SLACK_DESTINATION_ROOM_COMMENTS
    self.comments_type = settings.SLACK_TYPE_COMMENTS

adapter_type class-attribute instance-attribute

adapter_type = 'Slack'

always_send class-attribute instance-attribute

always_send = True

messages class-attribute instance-attribute

messages = {NEW_SUBMISSION: gettext('A new submission has been submitted for {source.page.title}: <{link}|{source.title}> by {user}'), UPDATE_LEAD: gettext('The lead of <{link}|{source.title}> has been updated from {old_lead} to {source.lead} by {user}'), BATCH_UPDATE_LEAD: 'handle_batch_lead', COMMENT: gettext('A new {comment.visibility} comment has been posted on <{link}|{source.title}> by {user}'), EDIT_SUBMISSION: gettext('{user} has edited <{link}|{source.title}>'), APPLICANT_EDIT: gettext('{user} has edited <{link}|{source.title}>'), REVIEWERS_UPDATED: 'reviewers_updated', BATCH_REVIEWERS_UPDATED: 'handle_batch_reviewers', PARTNERS_UPDATED: gettext('{user} has updated the partners on <{link}|{source.title}>'), TRANSITION: gettext('{user} has updated the status of <{link}|{source.title}>: {old_phase.display_name} → {source.phase}'), BATCH_TRANSITION: 'handle_batch_transition', DETERMINATION_OUTCOME: 'handle_determination', BATCH_DETERMINATION_OUTCOME: 'handle_batch_determination', PROPOSAL_SUBMITTED: gettext('A proposal has been submitted for review: <{link}|{source.title}>'), INVITED_TO_PROPOSAL: gettext('<{link}|{source.title}> by {source.user} has been invited to submit a proposal'), NEW_REVIEW: gettext('{user} has submitted a review for <{link}|{source.title}>. Outcome: {review.outcome},  Score: {review.get_score_display}'), READY_FOR_REVIEW: 'notify_reviewers', OPENED_SEALED: gettext('{user} has opened the sealed submission: <{link}|{source.title}>'), REVIEW_OPINION: gettext('{user} {opinion.opinion_display}s with {opinion.review.author}s review of <{link}|{source.title}>'), BATCH_READY_FOR_REVIEW: 'batch_notify_reviewers', DELETE_SUBMISSION: gettext('{user} has deleted {source.title}'), DELETE_REVIEW: gettext('{user} has deleted {review.author} review for <{link}|{source.title}>'), DELETE_REVIEW_OPINION: gettext('{user} has deleted {review_opinion.author} review opinion for <{link}|{source.title}>'), CREATED_PROJECT: gettext('{user} has created a Project: <{link}|{source.title}>'), UPDATE_PROJECT_LEAD: gettext('The lead of project <{link}|{source.title}> has been updated from {old_lead} to {source.lead} by {user}'), EDIT_REVIEW: gettext('{user} has edited {review.author} review for <{link}|{source.title}>'), SEND_FOR_APPROVAL: gettext('{user} has requested approval on project <{link}|{source.title}>'), APPROVE_PROJECT: gettext('{user} has approved project <{link}|{source.title}>'), REQUEST_PROJECT_CHANGE: gettext('{user} has requested changes for project acceptance on <{link}|{source.title}>'), UPLOAD_CONTRACT: gettext('{user} has uploaded a contract for <{link}|{source.title}>'), SUBMIT_CONTRACT_DOCUMENTS: gettext('{user} has submitted the contracting document for project <{link}|{source.title}>'), APPROVE_CONTRACT: gettext('{user} has approved contract for <{link}|{source.title}>'), CREATE_INVOICE: gettext('{user} has created invoice for <{link}|{source.title}>'), UPDATE_INVOICE_STATUS: gettext('{user} has changed the status of <{link_related}|invoice> on <{link}|{source.title}> to {invoice.status_display}'), DELETE_INVOICE: gettext('{user} has deleted invoice from <{link}|{source.title}>'), UPDATE_INVOICE: gettext('{user} has updated invoice for <{link}|{source.title}>'), SUBMIT_REPORT: gettext('{user} has submitted a report for <{link}|{source.title}>'), BATCH_DELETE_SUBMISSION: 'handle_batch_delete_submission', STAFF_ACCOUNT_CREATED: gettext('{user} has created a new account for <{link}|{source}>'), STAFF_ACCOUNT_EDITED: gettext('{user} has edited account for <{link}|{source}> that now has following roles: {roles}'), BATCH_ARCHIVE_SUBMISSION: 'handle_batch_archive_submission', ARCHIVE_SUBMISSION: gettext('{user} has archived the submission: {source.title}'), UNARCHIVE_SUBMISSION: gettext('{user} has unarchived the submission: {source.title}')}

destination instance-attribute

destination = SLACK_ENDPOINT_URL

target_room instance-attribute

target_room = SLACK_DESTINATION_ROOM

comments_room instance-attribute

comments_room = SLACK_DESTINATION_ROOM_COMMENTS

comments_type instance-attribute

comments_type = SLACK_TYPE_COMMENTS

message

message(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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)
    else:
        # Delegate all responsibility to the custom method
        return method(**kwargs)

render_message

render_message(message, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def render_message(self, message, **kwargs):
    return message.format(**kwargs)
get_neat_related(message_type, related)
Source code in hypha/apply/activity/adapters/base.py
def get_neat_related(self, message_type, related):
    # We translate the related kwarg into something we can understand
    try:
        neat_name = neat_related[message_type]
    except KeyError:
        # Message type doesn't expect a related object
        if related:
            raise ValueError(
                f"Unexpected 'related' kwarg provided for {message_type}"
            ) from None
        return {}
    else:
        if not related:
            raise ValueError(f"{message_type} expects a 'related' kwarg")
        return {neat_name: related}

process_batch

process_batch(message_type, events, request, user, sources, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
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, sources, user=user, **kwargs
    ):
        recipients = recipient["recipients"]
        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,
        )

process

process(message_type, event, request, user, source, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process(
    self, message_type, event, request, user, source, related=None, **kwargs
):
    recipients = self.recipients(
        message_type,
        source=source,
        related=related,
        user=user,
        request=request,
        **kwargs,
    )
    self.process_send(
        message_type,
        recipients,
        [event],
        request,
        user,
        source,
        related=related,
        **kwargs,
    )

process_send

process_send(message_type, recipients, events, request, user, source, sources=None, related=None, **kwargs)
Source code in hypha/apply/activity/adapters/base.py
def process_send(
    self,
    message_type,
    recipients,
    events,
    request,
    user,
    source,
    sources=None,
    related=None,
    **kwargs,
):
    if sources is None:
        sources = []
    try:
        # If this was a batch action we want to pull out the submission
        source = sources[0]
    except IndexError:
        pass

    kwargs = {
        "request": request,
        "user": user,
        "source": source,
        "sources": sources,
        "related": related,
        **kwargs,
    }
    kwargs.update(self.get_neat_related(message_type, related))
    kwargs.update(self.extra_kwargs(message_type, **kwargs))

    message = self.message(message_type, **kwargs)
    if not message:
        return

    for recipient in recipients:
        message_logs = self.create_logs(message, recipient, *events)

        if settings.SEND_MESSAGES or self.always_send:
            status = self.send_message(
                message, recipient=recipient, logs=message_logs, **kwargs
            )
        else:
            status = "Message not sent as SEND_MESSAGES==FALSE"

        message_logs.update_status(status)

        if not settings.SEND_MESSAGES:
            if recipient:
                debug_message = "{} [to: {}]: {}".format(
                    self.adapter_type, recipient, message
                )
            else:
                debug_message = "{}: {}".format(self.adapter_type, message)
            messages.add_message(request, messages.DEBUG, debug_message)

create_logs

create_logs(message, recipient, *events)
Source code in hypha/apply/activity/adapters/base.py
def create_logs(self, message, recipient, *events):
    from ..models import Message

    messages = Message.objects.bulk_create(
        Message(**self.log_kwargs(message, recipient, event)) for event in events
    )
    return Message.objects.filter(id__in=[message.id for message in messages])

log_kwargs

log_kwargs(message, recipient, event)
Source code in hypha/apply/activity/adapters/base.py
def log_kwargs(self, message, recipient, event):
    return {
        "type": self.adapter_type,
        "content": message,
        "recipient": recipient or "",
        "event": event,
    }
slack_links(links, sources)
Source code in hypha/apply/activity/adapters/slack.py
def slack_links(self, links, sources):
    return ", ".join(f"<{links[source.id]}|{source.title}>" for source in sources)

extra_kwargs

extra_kwargs(message_type, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def extra_kwargs(self, message_type, **kwargs):
    source = kwargs["source"]
    sources = kwargs["sources"]
    request = kwargs["request"]
    related = kwargs["related"]
    link = link_to(source, request)
    link_related = link_to(related, request)
    links = {source.id: link_to(source, request) for source in sources}
    return {
        "link": link,
        "link_related": link_related,
        "links": links,
    }

recipients

recipients(message_type, source, related, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def recipients(self, message_type, source, related, **kwargs):
    if message_type in [
        MESSAGES.STAFF_ACCOUNT_CREATED,
        MESSAGES.STAFF_ACCOUNT_EDITED,
    ]:
        return [self.slack_id(kwargs["user"])]

    if message_type == MESSAGES.SEND_FOR_APPROVAL:
        return [
            self.slack_id(user)
            for user in User.objects.approvers()
            if self.slack_id(user)
        ]

    recipients = [self.slack_id(source.lead)]
    # Notify second reviewer when first reviewer is done.
    if message_type in [MESSAGES.NEW_REVIEW, MESSAGES.REVIEW_OPINION] and related:
        submission = source
        role_reviewers = [
            role_reviewer.reviewer
            for role_reviewer in submission.assigned.with_roles()
        ]
        if related.author.reviewer in role_reviewers:
            for reviewer in role_reviewers:
                if reviewer != related.author.reviewer:
                    recipients.append(self.slack_id(reviewer))

    if message_type == MESSAGES.UPDATE_INVOICE_STATUS:
        if related.status in [
            SUBMITTED,
            RESUBMITTED,
            CHANGES_REQUESTED_BY_FINANCE,
            APPROVED_BY_FINANCE_2,
            PAID,
            PAYMENT_FAILED,
        ]:
            # Notify project lead/staff
            return recipients
        if related.status in [APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2]:
            # Notify finance 1
            return [
                self.slack_id(user)
                for user in User.objects.finances_level_1()
                if self.slack_id(user)
            ]
        if related.status in [APPROVED_BY_FINANCE]:
            # Notify finance 2
            return [
                self.slack_id(user)
                for user in User.objects.finances_level_2()
                if self.slack_id(user)
            ]
        return []
    return recipients

batch_recipients

batch_recipients(message_type, sources, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def batch_recipients(self, message_type, sources, **kwargs):
    # We group the messages by lead
    leads = User.objects.filter(id__in=sources.values("lead"))
    return [
        {
            "recipients": [self.slack_id(lead)],
            "sources": sources.filter(lead=lead),
        }
        for lead in leads
    ]

reviewers_updated

reviewers_updated(source, link, user, added=None, removed=None, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def reviewers_updated(self, source, link, user, added=None, removed=None, **kwargs):
    if added is None:
        added = []
    if removed is None:
        removed = []
    submission = source
    message = [
        _("{user} has updated the reviewers on <{link}|{title}>").format(
            user=user, link=link, title=submission.title
        )
    ]

    if added:
        message.append(_("Added:"))
        message.extend(reviewers_message(added))

    if removed:
        message.append(_("Removed:"))
        message.extend(reviewers_message(removed))

    return " ".join(message)

handle_batch_lead

handle_batch_lead(sources, links, user, new_lead, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
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(
        user=user,
        submissions_text=submissions_text,
        new_lead=new_lead,
    )

handle_batch_reviewers

handle_batch_reviewers(sources, links, user, added, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def handle_batch_reviewers(self, sources, links, user, added, **kwargs):
    submissions = sources
    submissions_text = self.slack_links(links, submissions)
    reviewers_text = " ".join(
        [
            _("{user} as {name},").format(user=str(user), name=role.name)
            for role, user in added
            if user
        ]
    )
    return _(
        "{user} has batch added {reviewers_text} as reviewers on: {submissions_text}"
    ).format(
        user=user,
        submissions_text=submissions_text,
        reviewers_text=reviewers_text,
    )

handle_batch_transition

handle_batch_transition(user, links, sources, transitions, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def handle_batch_transition(self, user, links, sources, transitions, **kwargs):
    submissions = sources
    submissions_text = [
        ": ".join(
            [
                self.slack_links(links, [submission]),
                f"{transitions[submission.id].display_name} → {submission.phase}",
            ]
        )
        for submission in submissions
    ]
    submissions_links = ",".join(submissions_text)
    return _(
        "{user} has transitioned the following submissions: {submissions_links}"
    ).format(
        user=user,
        submissions_links=submissions_links,
    )

handle_determination

handle_determination(source, link, determination, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def handle_determination(self, source, link, determination, **kwargs):
    submission = source
    if determination.send_notice:
        return _(
            "A determination for <{link}|{submission_title}> was sent by email. Outcome: {determination_outcome}"
        ).format(
            link=link,
            submission_title=submission.title,
            determination_outcome=determination.clean_outcome,
        )
    return _(
        "A determination for <{link}|{submission_title}> was saved without sending an email. Outcome: {determination_outcome}"
    ).format(
        link=link,
        submission_title=submission.title,
        determination_outcome=determination.clean_outcome,
    )

handle_batch_determination

handle_batch_determination(sources, links, determinations, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def handle_batch_determination(self, sources, links, determinations, **kwargs):
    submissions = sources
    submissions_links = ",".join(
        [self.slack_links(links, [submission]) for submission in submissions]
    )

    outcome = determinations[submissions[0].id].clean_outcome

    return _(
        "Determinations of {outcome} was sent for: {submissions_links}"
    ).format(
        outcome=outcome,
        submissions_links=submissions_links,
    )

handle_batch_delete_submission

handle_batch_delete_submission(sources, links, user, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def handle_batch_delete_submission(self, sources, links, user, **kwargs):
    submissions = sources
    submissions_text = ", ".join([submission.title for submission in submissions])
    return _("{user} has deleted submissions: {title}").format(
        user=user, title=submissions_text
    )

handle_batch_archive_submission

handle_batch_archive_submission(sources, links, user, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def handle_batch_archive_submission(self, sources, links, user, **kwargs):
    submissions = sources
    submissions_text = ", ".join([submission.title for submission in submissions])
    return _("{user} has archived submissions: {title}").format(
        user=user, title=submissions_text
    )

notify_reviewers

notify_reviewers(source, link, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
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):
            reviewers_to_notify.append(reviewer)

    reviewers = ", ".join(str(reviewer) for reviewer in reviewers_to_notify)

    return _(
        "<{link}|{title}> is ready for review. The following are assigned as reviewers: {reviewers}"
    ).format(
        link=link,
        reviewers=reviewers,
        title=submission.title,
    )

batch_notify_reviewers

batch_notify_reviewers(sources, links, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def batch_notify_reviewers(self, sources, links, **kwargs):
    kwargs.pop("source")
    kwargs.pop("link")
    return ". ".join(
        self.notify_reviewers(source, link=links[source.id], **kwargs)
        for source in sources
    )

slack_id

slack_id(user)
Source code in hypha/apply/activity/adapters/slack.py
def slack_id(self, user):
    if user is None:
        return ""

    if not user.slack:
        return ""

    return f"<{user.slack}>"

slack_channels

slack_channels(source, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def slack_channels(self, source, **kwargs):
    # Set the default room as a start.
    target_rooms = [self.target_room]
    try:
        fund_slack_channel = source.get_from_parent("slack_channel").split(",")
    except AttributeError:
        # Not a submission object.
        pass
    else:
        # If there are custom rooms, set them in place of the default room
        custom_rooms = [channel for channel in fund_slack_channel if channel]
        if len(custom_rooms) > 0:
            target_rooms = custom_rooms

    try:
        comment = kwargs["comment"]
    except KeyError:
        # Not a comment, no extra rooms.
        pass
    else:
        if self.comments_room:
            if any(self.comments_type):
                if comment.visibility in self.comments_type:
                    target_rooms.extend([self.comments_room])
            else:
                target_rooms.extend([self.comments_room])

    # Make sure each channel name starts with a "#".
    target_rooms = [
        room.strip() if room.startswith("#") else "#" + room.strip()
        for room in target_rooms
        if room
    ]

    return target_rooms

send_message

send_message(message, recipient, source, **kwargs)
Source code in hypha/apply/activity/adapters/slack.py
def send_message(self, message, recipient, source, **kwargs):
    target_rooms = self.slack_channels(source, **kwargs)

    if not any(target_rooms) or not settings.SLACK_TOKEN:
        errors = []
        if not target_rooms:
            errors.append("Room ID")
        if not settings.SLACK_TOKEN:
            errors.append("Slack Token")
        return "Missing configuration: {}".format(", ".join(errors))

    message = " ".join([recipient, message]).strip()

    data = {
        "message": message,
    }
    for room in target_rooms:
        try:
            slack_message("messages/slack_message.html", data, channel=room)
        except Exception as e:
            logger.exception(e)
            return "400: Bad Request"
    return "200: OK"