Skip to content

Commit

Permalink
apps/kiezradar: add automatic naming to SearchProfile based on an inc…
Browse files Browse the repository at this point in the history
…reasing number per user
  • Loading branch information
goapunk committed Jan 13, 2025
1 parent de1db1a commit 9b13c2d
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 3 deletions.
10 changes: 10 additions & 0 deletions changelog/8664.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Added

- add a new field `number` to SearchProfile which indexes the search profiles per
user
- add auto-naming to SearchProfiles for unnamed search profiles

### Changed

- make `name` field on SearchProfile optional as users can only change it after
creation and it makes accommodating the auto-naming scheme easier
27 changes: 27 additions & 0 deletions meinberlin/apps/kiezradar/migrations/0004_searchprofile_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.11 on 2025-01-09 15:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("meinberlin_kiezradar", "0003_searchprofile_notification"),
]

operations = [
migrations.AddField(
model_name="searchprofile",
name="number",
field=models.PositiveSmallIntegerField(null=True),
),
migrations.AlterModelOptions(
name="searchprofile",
options={"ordering": ["number"]},
),
migrations.AlterField(
model_name="searchprofile",
name="name",
field=models.CharField(max_length=255, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.11 on 2025-01-09 15:26

from django.db import migrations, models


def add_number_to_search_profiles(apps, schema_editor):
SearchProfile = apps.get_model("meinberlin_kiezradar", "SearchProfile")
for sp in SearchProfile.objects.all():
if sp.number is None:
next_number = 1
latest = SearchProfile.objects.filter(user=sp.user).latest("number")
if latest and latest.number:
next_number = latest.number + 1
sp.number = next_number
sp.save()


class Migration(migrations.Migration):
dependencies = [
("meinberlin_kiezradar", "0004_searchprofile_number"),
]

operations = [
migrations.RunPython(add_number_to_search_profiles, migrations.RunPython.noop),
migrations.AlterField(
model_name="searchprofile",
name="number",
field=models.PositiveSmallIntegerField(),
),
migrations.AddConstraint(
model_name="searchprofile",
constraint=models.UniqueConstraint(
models.F("user"), models.F("number"), name="unique-search-profile"
),
),
]
8 changes: 6 additions & 2 deletions meinberlin/apps/kiezradar/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ class SearchProfile(models.Model):
on_delete=models.CASCADE,
related_name="search_profiles",
)
name = models.CharField(max_length=255)
name = models.CharField(max_length=255, null=True)
number = models.PositiveSmallIntegerField()
description = models.TextField(blank=True, null=True)
disabled = models.BooleanField(default=False)
notification = models.BooleanField(default=False)
Expand Down Expand Up @@ -86,7 +87,10 @@ class SearchProfile(models.Model):
)

class Meta:
ordering = ["name"]
ordering = ["number"]
constraints = [
models.UniqueConstraint("user", "number", name="unique-search-profile")
]

def __str__(self):
return f"kiezradar search profile - {self.name}, disabled {self.disabled}"
16 changes: 15 additions & 1 deletion meinberlin/apps/kiezradar/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.conf import settings
from django.utils.module_loading import import_string
from django.utils.translation import gettext as _
from rest_framework import serializers

from adhocracy4.administrative_districts.models import AdministrativeDistrict
Expand Down Expand Up @@ -42,6 +43,7 @@ class Meta:
"id",
"user",
"name",
"number",
"description",
"disabled",
"notification",
Expand All @@ -55,9 +57,10 @@ class Meta:
"topics",
]

read_only_fields = ["user"]
read_only_fields = ["user", "number"]

def create(self, validated_data):
self._set_number(validated_data)
# Pop many-to-many and one-to-many fields from validated_data
query_text = validated_data.pop("query_text", None)

Expand Down Expand Up @@ -102,4 +105,15 @@ def to_representation(self, instance):
{"id": topic.id, "name": topics_enum(topic.code).label}
for topic in instance.topics.all()
]
if not instance.name:
representation["name"] = _("Searchprofile %d") % instance.number
return representation

def _set_number(self, validated_data):
"""Calculate the next free number"""
user = validated_data["user"]
next_number = 1
if SearchProfile.objects.filter(user=user).exists():
latest = SearchProfile.objects.filter(user=user).latest("number")
next_number = latest.number + 1
validated_data["number"] = next_number
1 change: 1 addition & 0 deletions meinberlin/test/factories/kiezradar.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Meta:

user = factory.SubFactory(a4_factories.USER_FACTORY)
name = factory.Faker("sentence", nb_words=4)
number = factory.Sequence(lambda n: n)
description = factory.Faker("sentence", nb_words=16)
status = SearchProfile.STATUS_ONGOING

Expand Down
95 changes: 95 additions & 0 deletions tests/kiezradar/test_api_kiezradar.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def test_create_search_profile(client, user, setup_data):
data = response.json()

assert data["name"] == payload["name"]
assert data["number"] == 1
assert data["description"] == payload["description"]
assert data["disabled"] == payload["disabled"]
assert data["status"] == payload["status"]
Expand All @@ -75,6 +76,68 @@ def test_create_search_profile(client, user, setup_data):
)


@pytest.mark.django_db
def test_create_and_update_search_profile_without_name(client, user, setup_data):
"""Test creating and updating a SearchProfile without name via the API."""
client.login(email=user.email, password="password")

payload = {
"description": "A description for the filters profile.",
"disabled": False,
"status": SearchProfile.STATUS_ONGOING,
"query": setup_data["query"],
"districts": setup_data["districts"],
"topics": setup_data["topics"],
"organisation": setup_data["organisations"],
"project_types": setup_data["project_types"],
}

url = reverse("searchprofiles-list")
response = client.post(url, payload, format="json")

assert response.status_code == 201
data = response.json()

assert data["name"] == "Searchprofile 1"
assert data["number"] == 1
assert data["description"] == payload["description"]
assert data["disabled"] == payload["disabled"]
assert data["status"] == payload["status"]
assert data["query"] == payload["query"]

# Check if the object was created in the database
search_profile = SearchProfile.objects.get(id=data["id"])
assert search_profile.name is None
assert search_profile.description == payload["description"]
assert search_profile.disabled == payload["disabled"]
assert search_profile.status == payload["status"]
assert search_profile.query.id == payload["query"]
assert (
list(search_profile.districts.values_list("id", flat=True))
== payload["districts"]
)
assert list(search_profile.topics.values_list("id", flat=True)) == payload["topics"]
assert (
list(search_profile.project_types.values_list("id", flat=True))
== payload["project_types"]
)

payload = {
"name": "Test Search Profile",
}
url = reverse("searchprofiles-detail", kwargs={"pk": search_profile.id})
response = client.patch(url, data=payload, content_type="application/json")
data = response.json()

assert response.status_code == 200
assert data["name"] == payload["name"]
assert data["number"] == 1

# Check if the object was updated in the database
search_profile = SearchProfile.objects.get(id=data["id"])
assert search_profile.name == payload["name"]


@pytest.mark.django_db
def test_update_search_profile(client, user, setup_data, search_profile_factory):
"""Test updating a SearchProfile via the API."""
Expand Down Expand Up @@ -137,3 +200,35 @@ def test_update_search_profile(client, user, setup_data, search_profile_factory)
# Check if the object was updated in the database
search_profile = SearchProfile.objects.get(id=data["id"])
assert search_profile.query.text == payload["query_text"]


@pytest.mark.django_db
def test_creating_multiple_search_profiles_assign_correct_number(
client, user, setup_data
):
"""Test creating and updating a SearchProfile without name via the API."""
client.login(email=user.email, password="password")

payload = {
"description": "A description for the filters profile.",
"disabled": False,
"status": SearchProfile.STATUS_ONGOING,
"query": setup_data["query"],
"districts": setup_data["districts"],
"topics": setup_data["topics"],
"organisation": setup_data["organisations"],
"project_types": setup_data["project_types"],
}

url = reverse("searchprofiles-list")
for i in range(1, 6):
response = client.post(url, payload, format="json")
assert response.status_code == 201
data = response.json()
assert data["name"] == "Searchprofile " + str(i)
assert data["number"] == i

assert SearchProfile.objects.count() == 5
for i, search_profile in enumerate(SearchProfile.objects.all()):
assert search_profile.name is None
assert search_profile.number == i + 1

0 comments on commit 9b13c2d

Please sign in to comment.