Skip to content

Commit

Permalink
Merge branch 'main' into feature/tag-incident-commander
Browse files Browse the repository at this point in the history
  • Loading branch information
GabDug authored Oct 16, 2024
2 parents 5a0c221 + 84cb015 commit 7fade0b
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 8 deletions.
25 changes: 23 additions & 2 deletions docs/usage/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,34 @@ Some tags have special meaning:
- `dev_firefighter`: Where users can get help with the bot. Will be shown in `/incident help` for instance.
- `it_deploy`: Where the bot send notifications for deployment freezes.

##### Usergroups
## User Group Management in Back-Office

You can add or import usergroups in the back-office.
You can **add** or **import user groups** in the back-office.

!!! note "Hint"
When adding a usergroup in the BackOffice, you can put only its ID. The rest of the information will be fetched from Slack.

### How users are invited into an incident

Users are invited to incidents through a system that listens for invitation requests. For critical incidents, specific user groups are automatically included in the invitation process.

The system also checks if the incident is public or private, ensuring that only the appropriate users with Slack accounts are invited. This creates a complete list of responders from all connected platforms, making sure the right people are notified.

### Custom Invitation Strategy

For users looking to create a custom invitation strategy, here’s what you need to know:

- **Django Signals**: We use Django signals to manage invitations. You can refer to the [Django signals documentation](https://docs.djangoproject.com/en/4.2/topics/signals/) for more information.


- **Registering on the Signal**: You need to register on the [`get_invites`][firefighter.incidents.signals.get_invites] signal, which provides the incident object and expects to receive a list of [`users`][firefighter.slack.models.user].

- **Signal Example**: You can check one of our [signals][firefighter.slack.signals.get_users] for a concrete example.

!!! note "Tips"
The signal can be triggered during the creation and update of an incident.
Invitations will only be sent once all signals have responded. It is advisable to avoid API calls and to store data in the database beforehand.

##### SOSes

You can configure [SOSes][firefighter.slack.models.sos.Sos] in the back-office.
Expand Down
1 change: 1 addition & 0 deletions src/firefighter/incidents/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@


class Group(models.Model):
"""Group of [Components][firefighter.incidents.models.component.Component]. Not a group of users."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=128, unique=True)
description = models.TextField(blank=True)
Expand Down
4 changes: 2 additions & 2 deletions src/firefighter/incidents/models/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
logger = logging.getLogger(__name__)

if TYPE_CHECKING:
from collections.abc import Sequence # noqa: F401
from collections.abc import Iterable, Sequence # noqa: F401
from decimal import Decimal
from uuid import UUID

Expand Down Expand Up @@ -460,7 +460,7 @@ def build_invite_list(self) -> list[User]:
users_list: list[User] = []

# Send signal to modules (Confluence, PagerDuty...)
result_users: list[tuple[Any, Exception | list[User]]] = (
result_users: list[tuple[Any, Exception | Iterable[User]]] = (
signals.get_invites.send_robust(sender=None, incident=self)
)

Expand Down
39 changes: 38 additions & 1 deletion src/firefighter/slack/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,45 @@ class MessageAdmin(admin.ModelAdmin[Message]):
class UserGroupAdmin(admin.ModelAdmin[UserGroup]):
model = UserGroup

list_display = [
"name",
"handle",
"usergroup_id",
"is_external",
"tag",
]

list_display_links = [
"name",
"handle",
"usergroup_id",
]

readonly_fields = (
"created_at",
"updated_at",
)

autocomplete_fields = ["components", "members"]
search_fields = ["name", "handle", "usergroup_id"]
search_fields = ["name", "handle", "description", "usergroup_id", "tag"]

fieldsets = (
(
("Slack attributes"),
{
"description" : ("These fields are synchronized automatically with Slack API"),
"fields": (
"name",
"handle",
"usergroup_id",
"description",
"is_external",
"members",
)
},
),
(_("Firefighter attributes"), {"fields": ("tag", "components", "created_at", "updated_at")}),
)

def save_model(
self,
Expand Down
22 changes: 22 additions & 0 deletions src/firefighter/slack/migrations/0002_usergroup_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.16 on 2024-10-14 10:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("slack", "0001_initial_oss"),
]

operations = [
migrations.AddField(
model_name="usergroup",
name="tag",
field=models.CharField(
blank=True,
help_text="Used by FireFighter internally to mark special users group (@team-secu, @team-incidents ...). Must be empty or unique.",
max_length=80,
),
),
]
6 changes: 6 additions & 0 deletions src/firefighter/slack/models/user_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ class UserGroup(models.Model):
help_text="Incident created with this usergroup automatically add the group members to these components.",
)

tag = models.CharField(
max_length=80,
blank=True,
help_text="Used by FireFighter internally to mark special user groups (e.g. @team-secu, @team-incidents...). Must be empty or unique.",
)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

Expand Down
32 changes: 29 additions & 3 deletions src/firefighter/slack/signals/get_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,22 @@

from firefighter.incidents import signals
from firefighter.incidents.models.user import User
from firefighter.slack.models.user_group import UserGroup
from firefighter.slack.slack_app import SlackApp

if TYPE_CHECKING:
from collections.abc import Iterable

from django.db.models.query import QuerySet

from firefighter.incidents.models.incident import Incident
from firefighter.slack.models.conversation import Conversation
from firefighter.slack.models.user_group import UserGroup

logger = logging.getLogger(__name__)


@receiver(signal=signals.get_invites)
def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> list[User]:
def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> Iterable[User]:
"""New version using cached users instead of querying Slack API."""
# Prepare sub-queries
slack_usergroups: QuerySet[UserGroup] = incident.component.usergroups.all()
Expand All @@ -39,4 +41,28 @@ def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> list[User]:
)
.distinct()
)
return list(queryset)
return set(queryset)


@receiver(signal=signals.get_invites)
def get_invites_from_slack_for_p1(incident: Incident, **kwargs: Any) -> Iterable[User]:

if incident.priority.value > 1:
return []

if incident.private:
return []

slack_usergroups: QuerySet[UserGroup] = UserGroup.objects.filter(
tag="invited_for_all_public_p1"
)

queryset = (
User.objects.filter(slack_user__isnull=False)
.exclude(slack_user__slack_id=SlackApp().details["user_id"])
.exclude(slack_user__slack_id="")
.exclude(slack_user__slack_id__isnull=True)
.filter(usergroup__in=slack_usergroups)
.distinct()
)
return set(queryset)

0 comments on commit 7fade0b

Please sign in to comment.