diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index c3f6ec86a..632561bcb 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -707,6 +707,11 @@ msgstr "" "vaihtoehdot on hylätty tai lukittu.
%(rejected)s: Kaikki haetut " "toistokerrat tälle hakemuksen osalle ovat lukittu tai hylätty.
" +#: tilavarauspalvelu/admin/application_section/form.py +#: tilavarauspalvelu/admin/reservation/form.py +msgid "External UUID" +msgstr "Ulkoinen UUID" + #: tilavarauspalvelu/admin/application_section/form.py #: tilavarauspalvelu/admin/reservation/form.py msgid "Number of persons" @@ -724,6 +729,11 @@ msgstr "Varausten loppumispäivä" msgid "Purpose" msgstr "Käyttötarkoitus" +#: tilavarauspalvelu/admin/application_section/form.py +#: tilavarauspalvelu/admin/reservation/form.py +msgid "ID for external systems to use" +msgstr "Tunniste ulkoisten järjestelmien käyttöön" + #: tilavarauspalvelu/admin/application_section/form.py msgid "Name that describes this section." msgstr "Nimi joka kuvaa tätä hakeuksen osaa." @@ -791,6 +801,10 @@ msgstr "" "Onko ilmoitus luonnos. Luonnoksia ei näytetä käyttäjille, vaikka ne olisivat " "aktiivisia." +#: tilavarauspalvelu/admin/bug_report/admin.py +msgid "Search by name or description" +msgstr "Etsi nimellä tai kuvauksella" + #: tilavarauspalvelu/admin/city/admin.py msgid "Municipality" msgstr "Kunta" @@ -1188,10 +1202,6 @@ msgstr "Ei" msgid "Paid reservation" msgstr "Maksullinen varaus" -#: tilavarauspalvelu/admin/reservation/form.py -msgid "External UUID" -msgstr "Ulkoinen UUID" - #: tilavarauspalvelu/admin/reservation/form.py #: tilavarauspalvelu/admin/reservation_unit/form.py msgid "SKU" @@ -1352,10 +1362,6 @@ msgstr "Syy hylkäämiselle" msgid "Reason for cancellation" msgstr "Syy perumiselle" -#: tilavarauspalvelu/admin/reservation/form.py -msgid "ID for external systems to use" -msgstr "Tunniste ulkoisten järjestelmien käyttöön" - #: tilavarauspalvelu/admin/reservation/form.py msgid "SKU for this particular reservation" msgstr "Tämän varauksen SKU" @@ -1560,6 +1566,19 @@ msgstr "Julkaisun tila" msgid "Reservation state" msgstr "Varattavuuden tila" +#: tilavarauspalvelu/admin/reservation_unit/form.py +msgid "" +"Additional search terms that will bring up this reservation unit when making " +"text searches in the customer UI. These terms should be added to make sure " +"search results using text search in links from external sources work " +"regardless of the UI language." +msgstr "" +"Ylimääräiset hakusanat, joilla tätä varausyksikköä on mahdollista hakea " +"tekstihaun kautta asiakaskäyttöliittymässä. Hakusanoja tulee lisätä tähän " +"listaan silloin kun halutaan taata, että järjestelmän ulkopuolelta tulevissa " +"linkeissä käytettävät hakusanat tuottavat oikeat hakutulokset " +"käyttöliittymän kielestä rippumatta." + #: tilavarauspalvelu/admin/reservation_unit/form.py msgid "Description (Finnish)" msgstr "Kuvaus (Suomeksi)" @@ -1978,19 +1997,6 @@ msgstr "Maksujen kirjanpidon tiliöintitiedot" msgid "Product used for payments" msgstr "Tuote, jota käytetään maksuihin" -#: tilavarauspalvelu/admin/reservation_unit/form.py -msgid "" -"Additional search terms that will bring up this reservation unit when making " -"text searches in the customer UI. These terms should be added to make sure " -"search results using text search in links from external sources work " -"regardless of the UI language." -msgstr "" -"Ylimääräiset hakusanat, joilla tätä varausyksikköä on mahdollista hakea " -"tekstihaun kautta asiakaskäyttöliittymässä. Hakusanoja tulee lisätä tähän " -"listaan silloin kun halutaan taata, että järjestelmän ulkopuolelta tulevissa " -"linkeissä käytettävät hakusanat tuottavat oikeat hakutulokset " -"käyttöliittymän kielestä rippumatta." - #: tilavarauspalvelu/admin/space/admin.py msgid "Search by name or unit name" msgstr "Etsi nimellä tai toimipisteen nimellä" @@ -3274,6 +3280,18 @@ msgstr "" "'active_from' ja 'active_until' täytyy olla joko tyhjiä tai asetettuja. Jos " "molemmat on asetettu, 'active_until' täytyy olla 'active_from' jälkeen." +#: tilavarauspalvelu/models/bug_report/model.py +msgid "Someone else, who?" +msgstr "Joku muu, kuka?" + +#: tilavarauspalvelu/models/bug_report/model.py +msgid "bug report" +msgstr "virheilmoitus" + +#: tilavarauspalvelu/models/bug_report/model.py +msgid "bug reports" +msgstr "virheilmoitukset" + #: tilavarauspalvelu/models/city/model.py msgid "city" msgstr "kotikunta" diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po index 7b5ad61c7..ebe37a614 100644 --- a/locale/sv/LC_MESSAGES/django.po +++ b/locale/sv/LC_MESSAGES/django.po @@ -681,6 +681,11 @@ msgid "" "applied slots for this application section have been locked or rejected.
" msgstr "" +#: tilavarauspalvelu/admin/application_section/form.py +#: tilavarauspalvelu/admin/reservation/form.py +msgid "External UUID" +msgstr "" + #: tilavarauspalvelu/admin/application_section/form.py #: tilavarauspalvelu/admin/reservation/form.py msgid "Number of persons" @@ -698,6 +703,11 @@ msgstr "" msgid "Purpose" msgstr "" +#: tilavarauspalvelu/admin/application_section/form.py +#: tilavarauspalvelu/admin/reservation/form.py +msgid "ID for external systems to use" +msgstr "" + #: tilavarauspalvelu/admin/application_section/form.py msgid "Name that describes this section." msgstr "" @@ -758,6 +768,10 @@ msgid "" "would be active." msgstr "" +#: tilavarauspalvelu/admin/bug_report/admin.py +msgid "Search by name or description" +msgstr "" + #: tilavarauspalvelu/admin/city/admin.py msgid "Municipality" msgstr "" @@ -1145,10 +1159,6 @@ msgstr "" msgid "Paid reservation" msgstr "" -#: tilavarauspalvelu/admin/reservation/form.py -msgid "External UUID" -msgstr "" - #: tilavarauspalvelu/admin/reservation/form.py #: tilavarauspalvelu/admin/reservation_unit/form.py msgid "SKU" @@ -1309,10 +1319,6 @@ msgstr "" msgid "Reason for cancellation" msgstr "" -#: tilavarauspalvelu/admin/reservation/form.py -msgid "ID for external systems to use" -msgstr "" - #: tilavarauspalvelu/admin/reservation/form.py msgid "SKU for this particular reservation" msgstr "" @@ -1517,6 +1523,14 @@ msgstr "" msgid "Reservation state" msgstr "" +#: tilavarauspalvelu/admin/reservation_unit/form.py +msgid "" +"Additional search terms that will bring up this reservation unit when making " +"text searches in the customer UI. These terms should be added to make sure " +"search results using text search in links from external sources work " +"regardless of the UI language." +msgstr "" + #: tilavarauspalvelu/admin/reservation_unit/form.py msgid "Description (Finnish)" msgstr "" @@ -1921,14 +1935,6 @@ msgstr "" msgid "Product used for payments" msgstr "" -#: tilavarauspalvelu/admin/reservation_unit/form.py -msgid "" -"Additional search terms that will bring up this reservation unit when making " -"text searches in the customer UI. These terms should be added to make sure " -"search results using text search in links from external sources work " -"regardless of the UI language." -msgstr "" - #: tilavarauspalvelu/admin/space/admin.py msgid "Search by name or unit name" msgstr "" @@ -3203,6 +3209,18 @@ msgid "" "are set, 'active_until' must be after 'active_from'." msgstr "" +#: tilavarauspalvelu/models/bug_report/model.py +msgid "Someone else, who?" +msgstr "" + +#: tilavarauspalvelu/models/bug_report/model.py +msgid "bug report" +msgstr "" + +#: tilavarauspalvelu/models/bug_report/model.py +msgid "bug reports" +msgstr "" + #: tilavarauspalvelu/models/city/model.py msgid "city" msgstr "" diff --git a/tests/test_admin/test_django_admin_site.py b/tests/test_admin/test_django_admin_site.py index e16f53fcb..0957ed3f8 100644 --- a/tests/test_admin/test_django_admin_site.py +++ b/tests/test_admin/test_django_admin_site.py @@ -9,9 +9,11 @@ from django.urls import reverse from tilavarauspalvelu.enums import EmailType -from tilavarauspalvelu.models import RequestLog, Reservation, SQLLog +from tilavarauspalvelu.models import BugReport, RequestLog, Reservation, SQLLog +from tilavarauspalvelu.models.bug_report.model import BugReportPhaseChoice, BugReportUserChoice from tilavarauspalvelu.tasks import create_or_update_reservation_statistics from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient +from utils.date_utils import local_datetime from tests import factories from tests.helpers import patch_method @@ -46,6 +48,12 @@ def create_all_models(): duration_ns=123, stack_info="stack_info", ) + BugReport.objects.create( + name="Example", + phase=BugReportPhaseChoice.PULL_REQUEST, + found_by=BugReportUserChoice.MATTI, + found_at=local_datetime(), + ) create_or_update_reservation_statistics(Reservation.objects.values_list("pk", flat=True)) @@ -78,6 +86,9 @@ def test_django_admin_site__pages_load__model_admins(create_all_models): # Edit & Delete views elif url_pattern.lookup_str.endswith((".change_view", ".delete_view")): first_object = model.objects.first() + if first_object is None: + pytest.fail(f"Did not find any objects for model '{model}'") + admin_url = reverse(f"admin:{url_pattern.name}", args=[first_object.pk]) assert client.get(admin_url).status_code == 200, admin_url diff --git a/tilavarauspalvelu/admin/__init__.py b/tilavarauspalvelu/admin/__init__.py index c98fb0a38..664370fa3 100644 --- a/tilavarauspalvelu/admin/__init__.py +++ b/tilavarauspalvelu/admin/__init__.py @@ -8,6 +8,7 @@ from .application_round.admin import ApplicationRoundAdmin from .application_section.admin import ApplicationSectionAdmin from .banner_notification.admin import BannerNotificationAdmin +from .bug_report.admin import BugReportAdmin from .city.admin import CityAdmin from .equipment.admin import EquipmentAdmin from .equipment_category.admin import EquipmentCategoryAdmin @@ -53,6 +54,7 @@ "ApplicationRoundAdmin", "ApplicationSectionAdmin", "BannerNotificationAdmin", + "BugReportAdmin", "CityAdmin", "EquipmentAdmin", "EquipmentCategoryAdmin", diff --git a/tilavarauspalvelu/admin/bug_report/__init__.py b/tilavarauspalvelu/admin/bug_report/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tilavarauspalvelu/admin/bug_report/admin.py b/tilavarauspalvelu/admin/bug_report/admin.py new file mode 100644 index 000000000..4c1fd55af --- /dev/null +++ b/tilavarauspalvelu/admin/bug_report/admin.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from import_export.admin import ExportMixin +from import_export.formats.base_formats import CSV + +from tilavarauspalvelu.models import BugReport + + +@admin.register(BugReport) +class BugReportAdmin(ExportMixin, admin.ModelAdmin): + # Functions + search_fields = [ + "name", + "description", + ] + search_help_text = _("Search by name or description") + formats = [CSV] + + # List + list_display = [ + "name", + "found_at", + "fixed_at", + ] + list_filter = [ + "phase", + "was_real_issue", + "found_by", + "fixed_by", + ] + + # Form + fields = [ + "name", + "description", + "phase", + "was_real_issue", + "found_by", + "found_at", + "fixed_by", + "fixed_at", + "created_at", + "updated_at", + ] + readonly_fields = [ + "created_at", + "updated_at", + ] diff --git a/tilavarauspalvelu/migrations/0054_bugreport.py b/tilavarauspalvelu/migrations/0054_bugreport.py new file mode 100644 index 000000000..827622937 --- /dev/null +++ b/tilavarauspalvelu/migrations/0054_bugreport.py @@ -0,0 +1,93 @@ +# Generated by Django 5.1.3 on 2024-12-16 11:52 +from __future__ import annotations + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tilavarauspalvelu", "0053_unique_application_section_ext_uuid"), + ] + + operations = [ + migrations.CreateModel( + name="BugReport", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, default="")), + ( + "phase", + models.CharField( + choices=[ + ("Pull Request", "Pull Request"), + ("Manual Testing", "Manual Testing"), + ("Production", "Production"), + ], + max_length=255, + ), + ), + ("was_real_issue", models.BooleanField(default=True)), + ( + "found_by", + models.CharField( + choices=[ + ("Eemeli", "Eemeli"), + ("Jesse", "Jesse"), + ("Joonatan", "Joonatan"), + ("Jusa", "Jusa"), + ("Matti", "Matti"), + ("Matu", "Matu"), + ("Milla", "Milla"), + ("Oiva", "Oiva"), + ("Risto", "Risto"), + ("Other", "Someone else, who?"), + ], + max_length=255, + ), + ), + ("found_at", models.DateTimeField()), + ( + "fixed_by", + models.CharField( + blank=True, + choices=[ + ("Eemeli", "Eemeli"), + ("Jesse", "Jesse"), + ("Joonatan", "Joonatan"), + ("Jusa", "Jusa"), + ("Matti", "Matti"), + ("Matu", "Matu"), + ("Milla", "Milla"), + ("Oiva", "Oiva"), + ("Risto", "Risto"), + ("Other", "Someone else, who?"), + ], + max_length=255, + null=True, + ), + ), + ("fixed_at", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "bug report", + "verbose_name_plural": "bug reports", + "db_table": "bug_report", + "ordering": ["pk"], + "base_manager_name": "objects", + "constraints": [ + models.CheckConstraint( + check=models.Q( + models.Q(("fixed_by__isnull", True), ("fixed_at__isnull", True)), + models.Q(("fixed_by__isnull", False), ("fixed_at__isnull", False)), + _connector="OR", + ), + name="must_set_both_fixed_by_and_fixed_at_together", + violation_error_message="Bug report fixed by and fixed at must be set together.", + ) + ], + }, + ), + ] diff --git a/tilavarauspalvelu/models/__init__.py b/tilavarauspalvelu/models/__init__.py index 736a9d223..3b0bdb919 100644 --- a/tilavarauspalvelu/models/__init__.py +++ b/tilavarauspalvelu/models/__init__.py @@ -10,6 +10,7 @@ from .application_round_time_slot.model import ApplicationRoundTimeSlot from .application_section.model import ApplicationSection from .banner_notification.model import BannerNotification +from .bug_report.model import BugReport from .city.model import City from .equipment.model import Equipment from .equipment_category.model import EquipmentCategory @@ -67,6 +68,7 @@ "ApplicationRoundTimeSlot", "ApplicationSection", "BannerNotification", + "BugReport", "City", "Equipment", "EquipmentCategory", diff --git a/tilavarauspalvelu/models/bug_report/__init__.py b/tilavarauspalvelu/models/bug_report/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tilavarauspalvelu/models/bug_report/actions.py b/tilavarauspalvelu/models/bug_report/actions.py new file mode 100644 index 000000000..9f66bbca2 --- /dev/null +++ b/tilavarauspalvelu/models/bug_report/actions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from tilavarauspalvelu.models import BugReport + +__all__ = [ + "BugReportActions", +] + + +class BugReportActions: + def __init__(self, bug_report: BugReport) -> None: + self.bug_report = bug_report diff --git a/tilavarauspalvelu/models/bug_report/model.py b/tilavarauspalvelu/models/bug_report/model.py new file mode 100644 index 000000000..fc640c86b --- /dev/null +++ b/tilavarauspalvelu/models/bug_report/model.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .actions import BugReportActions +from .queryset import BugReportManager + +if TYPE_CHECKING: + import datetime + +__all__ = [ + "BugReport", +] + + +class BugReportUserChoice(models.TextChoices): + # Known users + EEMELI = "Eemeli" + JESSE = "Jesse" + JOONATAN = "Joonatan" + JUSA = "Jusa" + MATTI = "Matti" + MATU = "Matu" + MILLA = "Milla" + OIVA = "Oiva" + RISTO = "Risto" + # Unknown users + OTHER = "Other", _("Someone else, who?") + + +class BugReportPhaseChoice(models.TextChoices): + PULL_REQUEST = "Pull Request" + MANUAL_TESTING = "Manual Testing" + PRODUCTION = "Production" + + +class BugReport(models.Model): + name: str = models.CharField(max_length=255) + description: str = models.TextField(blank=True, default="") + + phase: str = models.CharField(max_length=255, choices=BugReportPhaseChoice.choices) + was_real_issue: bool = models.BooleanField(default=True) + + found_by: str = models.CharField(max_length=255, choices=BugReportUserChoice.choices) + found_at: datetime.datetime = models.DateTimeField() + + fixed_by: str | None = models.CharField(max_length=255, choices=BugReportUserChoice.choices, null=True, blank=True) + fixed_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + objects = BugReportManager() + + class Meta: + db_table = "bug_report" + base_manager_name = "objects" + verbose_name = _("bug report") + verbose_name_plural = _("bug reports") + ordering = ["pk"] + constraints = [ + models.CheckConstraint( + check=( + (models.Q(fixed_by__isnull=True) & models.Q(fixed_at__isnull=True)) + | (models.Q(fixed_by__isnull=False) & models.Q(fixed_at__isnull=False)) + ), + name="must_set_both_fixed_by_and_fixed_at_together", + violation_error_message="Bug report fixed by and fixed at must be set together.", + ), + ] + + def __str__(self) -> str: + return f"{str(_('bug report')).capitalize()} {self.pk}: {self.name}" + + @cached_property + def actions(self) -> BugReportActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + return BugReportActions(self) diff --git a/tilavarauspalvelu/models/bug_report/queryset.py b/tilavarauspalvelu/models/bug_report/queryset.py new file mode 100644 index 000000000..0ab768dfc --- /dev/null +++ b/tilavarauspalvelu/models/bug_report/queryset.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "BugReportManager", + "BugReportQuerySet", +] + + +class BugReportQuerySet(models.QuerySet): ... + + +class BugReportManager(models.Manager.from_queryset(BugReportQuerySet)): ...