diff --git a/.env.example b/.env.example index 5f04b9ff..ca80d771 100644 --- a/.env.example +++ b/.env.example @@ -95,6 +95,7 @@ ENABLE_RAID=False RAID_DEFAULT_JIRA_QRAFT_USER_ID="XXXXXXXX" #gitleaks:allow RAID_JIRA_PROJECT_KEY="T2" +RAID_TOOLBOX_URL=https://toolbox.mycompany.com/login FF_SLACK_SKIP_CHECKS=true # Disable SSO redirect for local dev by setting to true. When SSO is disabled, go to /admin/ to login diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a00fab6..e7e3a532 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,13 @@ jobs: pdm run collectstatic pdm run tests-cov + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + - name: Type check with mypy run: | pdm run lint-mypy diff --git a/docker-compose.yaml b/docker-compose.yaml index 8afa5f8f..97beddda 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,8 +2,6 @@ # This configuration file is for the **development** setup. # It will launch Postgres and Redis containers, not the app itself. -version: "3.1" - services: db: container_name: ff-db diff --git a/pdm.lock b/pdm.lock index b99cec42..f3d0abda 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "docs", "tests", "types", "docs-img"] strategy = ["cross_platform", "static_urls"] lock_version = "4.4.1" -content_hash = "sha256:c5dfc2093b498ae65246942e71dc670f808363ccf6c104884d8182e26093de12" +content_hash = "sha256:78d7bf38ebaa89258e7bb1b21cda0ca3f2bd8ac00b15ffe635c160be97b02890" [[package]] name = "aiohttp" @@ -1661,11 +1661,10 @@ files = [ [[package]] name = "jira" -version = "3.8.0" +version = "3.5.2" requires_python = ">=3.8" summary = "Python library for interacting with JIRA via REST APIs." dependencies = [ - "Pillow>=2.1.0", "defusedxml", "packaging", "requests-oauthlib>=1.1.0", @@ -1674,8 +1673,8 @@ dependencies = [ "typing-extensions>=3.7.4.2", ] files = [ - {url = "https://files.pythonhosted.org/packages/4f/52/bb617020064261ba31cc965e932943458b7facfd9691ad7f76a2b631f44f/jira-3.8.0-py3-none-any.whl", hash = "sha256:12190dc84dad00b8a6c0341f7e8a254b0f38785afdec022bd5941e1184a5a3fb"}, - {url = "https://files.pythonhosted.org/packages/78/b4/557e4c80c0ea12164ffeec0e29372c085bfb263faad53cef5e1455523bec/jira-3.8.0.tar.gz", hash = "sha256:63719c529a570aaa01c3373dbb5a104dab70381c5be447f6c27f997302fa335a"}, + {url = "https://files.pythonhosted.org/packages/57/e3/6e7dec954fed0a8d7f4b02d7f0a2f4628cfb9fc8ccfee699d7c1139db09b/jira-3.5.2-py3-none-any.whl", hash = "sha256:f97716cd1e35523d04cb75b742143f1f3aaf098eb0ab61ad33e91812beb9edcc"}, + {url = "https://files.pythonhosted.org/packages/ea/40/8db893f2d1c0fa6e64f8500d54b1e756f6ff00cada962c0ea8d2daab19a8/jira-3.5.2.tar.gz", hash = "sha256:d23a0e0c62c0d6926ac37c68bc0b362065ae19106fe5c79d4ba964fad80d6cf2"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 128d2366..63f2817d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "django-htmx>=1.17.2", "django-simple-menu>=2.1.3", "markdown>=3.5.1", - "jira>=3.5.2", + "jira>=3.5.2,<3.6.0", # 3.6.0 stop accepting user id for add_watcher, see https://github.com/pycontribs/jira/issues/1855 "drf-standardized-errors>=0.12.6", "aiohttp>=3.9.1", # Transitive dependency of slack_sdk "nh3>=0.2.15", diff --git a/src/firefighter/firefighter/settings/components/raid.py b/src/firefighter/firefighter/settings/components/raid.py index 5cf2c66d..d11b8e75 100644 --- a/src/firefighter/firefighter/settings/components/raid.py +++ b/src/firefighter/firefighter/settings/components/raid.py @@ -17,3 +17,6 @@ RAID_JIRA_USER_IDS: dict[str, str] = {} "Mapping of domain to default Jira user ID" + + RAID_TOOLBOX_URL: str = config("RAID_TOOLBOX_URL") + "Toolbox URL" diff --git a/src/firefighter/incidents/forms/edit.py b/src/firefighter/incidents/forms/edit.py new file mode 100644 index 00000000..a9ea49e2 --- /dev/null +++ b/src/firefighter/incidents/forms/edit.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from django import forms + +from firefighter.incidents.models import Environment + + +def initial_environments() -> Environment: + return Environment.objects.get(default=True) + + +class EditMetaForm(forms.Form): + title = forms.CharField( + label="Title", + max_length=128, + min_length=10, + widget=forms.TextInput(attrs={"placeholder": "What's going on?"}), + ) + description = forms.CharField( + label="Summary", + widget=forms.Textarea( + attrs={ + "placeholder": "Help people responding to the incident. This will be posted to #tech-incidents and on our internal status page.\nThis description can be edited later." + } + ), + min_length=10, + max_length=1200, + ) + environment = forms.ModelChoiceField( + label="Environment", + queryset=Environment.objects.all(), + initial=initial_environments, + ) diff --git a/src/firefighter/incidents/migrations/0004_incidentupdate_environment.py b/src/firefighter/incidents/migrations/0004_incidentupdate_environment.py new file mode 100644 index 00000000..6fbc0de6 --- /dev/null +++ b/src/firefighter/incidents/migrations/0004_incidentupdate_environment.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.14 on 2024-09-27 10:06 + +from django.db import migrations, models + +import firefighter.incidents.models.environment + + +class Migration(migrations.Migration): + + dependencies = [ + ("incidents", "0003_delete_featureteam"), + ] + + operations = [ + migrations.AddField( + model_name="incidentupdate", + name="environment", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=models.SET( + firefighter.incidents.models.environment.Environment.get_default + ), + to="incidents.environment", + ), + ), + ] diff --git a/src/firefighter/incidents/models/incident.py b/src/firefighter/incidents/models/incident.py index 5d147746..a84eaaaa 100644 --- a/src/firefighter/incidents/models/incident.py +++ b/src/firefighter/incidents/models/incident.py @@ -335,21 +335,27 @@ def can_be_closed(self) -> tuple[bool, list[tuple[str, str]]]: return True, [] if self.needs_postmortem: if self.status.value != IncidentStatus.POST_MORTEM: - cant_closed_reasons.append(( - "STATUS_NOT_POST_MORTEM", - f"Incident is not in PostMortem status, and needs one because of its priority and environment ({self.priority.name}/{self.environment.value}).", - )) + cant_closed_reasons.append( + ( + "STATUS_NOT_POST_MORTEM", + f"Incident is not in PostMortem status, and needs one because of its priority and environment ({self.priority.name}/{self.environment.value}).", + ) + ) elif self.status.value < IncidentStatus.FIXED: - cant_closed_reasons.append(( - "STATUS_NOT_MITIGATED", - f"Incident is not in {IncidentStatus.FIXED.label} status (currently {self.status.label}).", - )) + cant_closed_reasons.append( + ( + "STATUS_NOT_MITIGATED", + f"Incident is not in {IncidentStatus.FIXED.label} status (currently {self.status.label}).", + ) + ) missing_milestones = self.missing_milestones() if len(missing_milestones) > 0: - cant_closed_reasons.append(( - "MISSING_REQUIRED_KEY_EVENTS", - f"Missing key events: {', '.join(missing_milestones)}", - )) + cant_closed_reasons.append( + ( + "MISSING_REQUIRED_KEY_EVENTS", + f"Missing key events: {', '.join(missing_milestones)}", + ) + ) if len(cant_closed_reasons) > 0: return False, cant_closed_reasons @@ -534,7 +540,7 @@ def update_roles( return incident_update - def create_incident_update( + def create_incident_update( # noqa: PLR0913 self: Incident, message: str | None = None, status: int | None = None, @@ -544,6 +550,7 @@ def create_incident_update( event_type: str | None = None, title: str | None = None, description: str | None = None, + environment_id: str | None = None, event_ts: datetime | None = None, ) -> IncidentUpdate: updated_fields: list[str] = [] @@ -560,6 +567,7 @@ def _update_incident_field( _update_incident_field(self, "component_id", component_id, updated_fields) _update_incident_field(self, "title", title, updated_fields) _update_incident_field(self, "description", description, updated_fields) + _update_incident_field(self, "environment_id", environment_id, updated_fields) old_priority = self.priority if priority_id is not None else None @@ -573,6 +581,7 @@ def _update_incident_field( incident=self, status=status, # type: ignore priority_id=priority_id, + environment_id=environment_id, component_id=component_id, message=message, created_by=created_by, diff --git a/src/firefighter/incidents/models/incident_update.py b/src/firefighter/incidents/models/incident_update.py index d78ed0b8..10c0950a 100644 --- a/src/firefighter/incidents/models/incident_update.py +++ b/src/firefighter/incidents/models/incident_update.py @@ -12,6 +12,7 @@ from firefighter.incidents.enums import IncidentStatus from firefighter.incidents.models.component import Component +from firefighter.incidents.models.environment import Environment from firefighter.incidents.models.priority import Priority from firefighter.incidents.models.severity import Severity from firefighter.incidents.models.user import User @@ -65,6 +66,12 @@ class IncidentUpdate(models.Model): priority = models.ForeignKey[Priority | None, Priority | None]( Priority, null=True, blank=True, on_delete=models.SET(Priority.get_default) ) + environment = models.ForeignKey[Environment | None, Environment | None]( + Environment, + null=True, + blank=True, + on_delete=models.SET(Environment.get_default), + ) incident = models.ForeignKey["Incident", "Incident"]( "Incident", on_delete=models.CASCADE ) diff --git a/src/firefighter/incidents/templates/layouts/partials/incident_timeline.html b/src/firefighter/incidents/templates/layouts/partials/incident_timeline.html index 35ff6698..2959df7c 100644 --- a/src/firefighter/incidents/templates/layouts/partials/incident_timeline.html +++ b/src/firefighter/incidents/templates/layouts/partials/incident_timeline.html @@ -40,6 +40,26 @@
+ Title update +
++ Title changed to: {{ incident_update.title }} +
+ {% endif %} + {% if incident_update.description or incident_update.description|length > 0 %} ++ Description update +
+Key event: {{ incident_update.event_type|title }} @@ -65,6 +85,17 @@ {% include "./status_pill.html" with status=incident_update.status IncidentStatus=IncidentStatus only %}
{% endif %} + {% if incident_update.environment %} ++ Environment update +
+ {% endif %} + {% if incident_update.environment or incident_update.environment|length > 0 %} ++ Environment changed to: + {% include "./environment_pill.html" with environment=incident_update.environment only %} +
+ {% endif %} {% if incident_update.severity %}Severity update @@ -84,8 +115,6 @@
Component update
- {% endif %} - {% if incident_update.component %}Component impacted changed to: {{ incident_update.component}} diff --git a/src/firefighter/incidents/templates/pages/incident_detail.html b/src/firefighter/incidents/templates/pages/incident_detail.html index bf89767a..0c39ecc6 100644 --- a/src/firefighter/incidents/templates/pages/incident_detail.html +++ b/src/firefighter/incidents/templates/pages/incident_detail.html @@ -189,7 +189,7 @@