Skip to content

Slack

hypha.apply.activity.adapters.slack

logger module-attribute

logger = getLogger(__name__)

User module-attribute

User = get_user_model()

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_text_display}> by {user}'), UPDATE_LEAD: gettext('The lead of <{link}|{source.title_text_display}> 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_text_display}>'), APPLICANT_EDIT: gettext('{user} has edited <{link}|{source.title_text_display}>'), REVIEWERS_UPDATED: 'reviewers_updated', BATCH_REVIEWERS_UPDATED: 'handle_batch_reviewers', PARTNERS_UPDATED: gettext('{user} has updated the partners on <{link}|{source.title_text_display}>'), TRANSITION: gettext('{user} has updated the status of <{link}|{source.title_text_display}>: {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_text_display}>'), INVITED_TO_PROPOSAL: gettext('<{link}|{source.title_text_display}> by {source.user} has been invited to submit a proposal'), NEW_REVIEW: gettext('{user} has submitted a review for <{link}|{source.title_text_display}>. 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_text_display}>'), REVIEW_OPINION: gettext('{user} {opinion.opinion_display}s with {opinion.review.author}s review of <{link}|{source.title_text_display}>'), BATCH_READY_FOR_REVIEW: 'batch_notify_reviewers', DELETE_SUBMISSION: gettext('{user} has deleted {source.title_text_display}'), DELETE_REVIEW: gettext('{user} has deleted {review.author} review for <{link}|{source.title_text_display}>'), DELETE_REVIEW_OPINION: gettext('{user} has deleted {review_opinion.author} review opinion for <{link}|{source.title_text_display}>'), 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}'), UPDATE_PROJECT_TITLE: gettext('The project title has been updated from <{link}|{old_title}> to <{link}|{source.title}> by {user}'), EDIT_REVIEW: gettext('{user} has edited {review.author} review for <{link}|{source.title_text_display}>'), 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_text_display}'), UNARCHIVE_SUBMISSION: gettext('{user} has unarchived the submission: {source.title_text_display}')}

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))

    for recipient in recipients:
        # Allow for customization of message based on recipient string (will vary based on adapter)
        message_kwargs = {**kwargs, "recipient": recipient}
        message = self.message(message_type, **message_kwargs)
        if not message:
            continue

        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]}|{getattr(source, 'title_text_display', 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,
            PAID,
            PAYMENT_FAILED,
        ]:
            # Notify project lead/staff
            return recipients
        if related.status in [APPROVED_BY_STAFF]:
            # Notify finance 1
            return [
                self.slack_id(user)
                for user in User.objects.finances()
                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_text_display
        )
    ]

    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_text_display,
            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_text_display,
        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_text_display 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_text_display,
    )

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"