Skip to content

Commit

Permalink
search-profile: load preset filters from search profile into project …
Browse files Browse the repository at this point in the history
…overview
  • Loading branch information
vellip committed Jan 7, 2025
1 parent 7aef611 commit d93b1e2
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 33 deletions.
6 changes: 6 additions & 0 deletions changelog/8646.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Added
- functions for the project overview to get its default state based on if a search profile exists or not
- functionality to load a search profile's preset filters into project overview

### Changed
- added project status model instead of a field to be able to store a list of project statuses inside a search profile
47 changes: 47 additions & 0 deletions meinberlin/apps/kiezradar/migrations/0004_add_project_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.11 on 2025-01-07 07:58

from django.db import migrations, models


class Migration(migrations.Migration):

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

operations = [
migrations.CreateModel(
name="ProjectStatus",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"status",
models.SmallIntegerField(
choices=[(0, "running"), (1, "done"), (2, "future")],
verbose_name="Status",
),
),
],
),
migrations.RemoveField(
model_name="searchprofile",
name="status",
),
migrations.AddField(
model_name="searchprofile",
name="status",
field=models.ManyToManyField(
blank=True,
related_name="search_profiles",
to="meinberlin_kiezradar.projectstatus",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.11 on 2025-01-07 07:59

from django.db import migrations


def populate_project_status(apps, schema_editor):
ProjectStatus = apps.get_model("meinberlin_kiezradar", "ProjectStatus")
ProjectStatus.objects.bulk_create(
[
ProjectStatus(status=0),
ProjectStatus(status=1),
ProjectStatus(status=2),
]
)


class Migration(migrations.Migration):

dependencies = [
("meinberlin_kiezradar", "0004_add_project_status"),
]

operations = [
migrations.RunPython(populate_project_status),
]
29 changes: 22 additions & 7 deletions meinberlin/apps/kiezradar/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,26 @@ def __str__(self):
return f"participation type - {self.participation}"


class SearchProfile(models.Model):
class ProjectStatus(models.Model):
STATUS_ONGOING = 0
STATUS_DONE = 1
STATUS_CHOICES = ((STATUS_ONGOING, _("running")), (STATUS_DONE, _("done")))
STATUS_FUTURE = 2
STATUS_CHOICES = (
(STATUS_ONGOING, _("running")),
(STATUS_DONE, _("done")),
(STATUS_FUTURE, _("future")),
)

status = models.SmallIntegerField(
choices=STATUS_CHOICES,
verbose_name=_("Status"),
)

def __str__(self):
return f"project status - {self.status}"


class SearchProfile(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
Expand All @@ -52,18 +67,18 @@ class SearchProfile(models.Model):
description = models.TextField(blank=True, null=True)
disabled = models.BooleanField(default=False)
notification = models.BooleanField(default=False)
status = models.SmallIntegerField(
choices=STATUS_CHOICES,
default=0,
verbose_name=_("Status"),
)
query = models.ForeignKey(
KiezradarQuery,
models.SET_NULL,
related_name="search_profiles",
blank=True,
null=True,
)
status = models.ManyToManyField(
ProjectStatus,
related_name="search_profiles",
blank=True,
)
organisations = models.ManyToManyField(
Organisation,
related_name="search_profiles",
Expand Down
14 changes: 11 additions & 3 deletions meinberlin/apps/kiezradar/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from adhocracy4.administrative_districts.models import AdministrativeDistrict
from adhocracy4.projects.models import Topic
from meinberlin.apps.kiezradar.models import KiezradarQuery
from meinberlin.apps.kiezradar.models import ProjectStatus
from meinberlin.apps.kiezradar.models import ProjectType
from meinberlin.apps.kiezradar.models import SearchProfile
from meinberlin.apps.organisations.models import Organisation
Expand All @@ -31,10 +32,12 @@ class SearchProfileSerializer(serializers.ModelSerializer):
project_types = serializers.PrimaryKeyRelatedField(
queryset=ProjectType.objects.all(), many=True, allow_null=True, required=False
)
status = serializers.PrimaryKeyRelatedField(
queryset=ProjectStatus.objects.all(), many=True, allow_null=True, required=False
)
topics = serializers.PrimaryKeyRelatedField(
queryset=Topic.objects.all(), many=True, allow_null=True, required=False
)
status_display = serializers.CharField(source="get_status_display", read_only=True)

class Meta:
model = SearchProfile
Expand All @@ -46,7 +49,6 @@ class Meta:
"disabled",
"notification",
"status",
"status_display",
"query",
"query_text",
"organisations",
Expand Down Expand Up @@ -96,10 +98,16 @@ def to_representation(self, instance):
{"id": project_type.id, "name": project_type.get_participation_display()}
for project_type in instance.project_types.all()
]
representation["status"] = [
{"id": project_status.id, "status": project_status.status}
for project_status in instance.status.all()
]

topics_enum = import_string(settings.A4_PROJECT_TOPICS)
representation["topics"] = [
{"id": topic.id, "name": topics_enum(topic.code).label}
{"code": topic.code, "name": topics_enum(topic.code).label}
for topic in instance.topics.all()
]

representation["query_text"] = instance.query.text if instance.query else ""
return representation
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
data-use_vector_map="{{ use_vector_map }}"
data-baseurl="{{ baseurl }}"
data-bounds="{{ bounds }}"
data-search-profile="{{ search_profile }}"
data-participation-choices="{{ participation_choices }}"></div>
{% endblock content %}

Expand Down
20 changes: 20 additions & 0 deletions meinberlin/apps/plans/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.utils.translation import gettext_lazy as _
from django.views import generic
from django.views.generic.detail import SingleObjectMixin
from rest_framework.renderers import JSONRenderer

from adhocracy4.administrative_districts.models import AdministrativeDistrict
from adhocracy4.dashboard import mixins as a4dashboard_mixins
Expand All @@ -21,6 +22,8 @@
from meinberlin.apps.contrib.enums import TopicEnum
from meinberlin.apps.contrib.views import CanonicalURLDetailView
from meinberlin.apps.dashboard.mixins import DashboardProjectListGroupMixin
from meinberlin.apps.kiezradar.models import SearchProfile
from meinberlin.apps.kiezradar.serializers import SearchProfileSerializer
from meinberlin.apps.maps.models import MapPreset
from meinberlin.apps.organisations.models import Organisation
from meinberlin.apps.plans import models
Expand Down Expand Up @@ -113,6 +116,23 @@ def get_context_data(self, **kwargs):
if hasattr(settings, "A4_OPENMAPTILES_TOKEN"):
omt_token = settings.A4_OPENMAPTILES_TOKEN

if (
self.request.GET.get("search-profile", None)
and self.request.user.is_authenticated
):
search_profile_id = self.request.GET.get("search-profile")
try:
search_profile = SearchProfile.objects.get(
id=search_profile_id, user=self.request.user
)
context["search_profile"] = (
JSONRenderer()
.render(SearchProfileSerializer(instance=search_profile).data)
.decode("utf-8")
)
except SearchProfile.DoesNotExist:
pass

context["districts"] = self.get_district_polygons()
context["organisations"] = self.get_organisations()
context["district_names"] = self.get_district_names()
Expand Down
2 changes: 2 additions & 0 deletions meinberlin/react/plans/react_plans_map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function init () {
const attribution = el.getAttribute('data-attribution')
const baseUrl = el.getAttribute('data-baseurl')
const bounds = JSON.parse(el.getAttribute('data-bounds'))
const searchProfile = JSON.parse(el.getAttribute('data-search-profile'))
const selectedDistrict = el.getAttribute('data-selected-district')
const selectedTopic = el.getAttribute('data-selected-topic')
const districts = JSON.parse(el.getAttribute('data-districts'))
Expand Down Expand Up @@ -43,6 +44,7 @@ function init () {
districtNames={districtNames}
topicChoices={topicChoices}
participationChoices={participationChoices}
searchProfile={searchProfile}
/>
</React.StrictMode>)
})
Expand Down
4 changes: 2 additions & 2 deletions meinberlin/react/projects/ProjectsControlBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const statusNames = {
past: django.gettext('done')
}

export const initialState = {
const initialState = {
search: '',
districts: [],
organisation: [],
Expand Down Expand Up @@ -63,7 +63,7 @@ export const ProjectsControlBar = ({
hasContainer
}) => {
const [expandFilters, setExpandFilters] = useState(false)
const [filters, setFilters] = useState(initialState)
const [filters, setFilters] = useState(appliedFilters)
const onFilterChange = (type, choice) => {
setFilters({ ...filters, [type]: choice })
}
Expand Down
24 changes: 9 additions & 15 deletions meinberlin/react/projects/ProjectsListMapBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import ProjectsMap from './ProjectsMap'
import Spinner from '../contrib/Spinner'
import { ProjectsControlBar } from './ProjectsControlBar'
import { filterProjects } from './filter-projects'
import {
getDefaultProjectState,
getDefaultState
} from './getDefaultState'

const pageHeader = django.gettext('Project overview')
const showMapStr = django.gettext('Show map')
Expand Down Expand Up @@ -44,22 +48,15 @@ const ProjectsListMapBox = ({
districtNames,
districts,
participationChoices,
organisations
organisations,
searchProfile
}) => {
const [showMap, setShowMap] = useState(true)
const [loading, setLoading] = useState(true)
const [projectState, setProjectState] = useState(['active', 'future'])
const [projectState, setProjectState] = useState(getDefaultProjectState(searchProfile))
const [items, setItems] = useState([])
const fetchCache = useRef({})
const [appliedFilters, setAppliedFilters] = useState({
search: '',
districts: [],
// organisation is a single select but its simpler to just work with an
// array because of the typeahead component
organisation: [],
participations: [],
topics: []
})
const [appliedFilters, setAppliedFilters] = useState(getDefaultState(searchProfile))

const fetchItems = useCallback(async () => {
setLoading(true)
Expand Down Expand Up @@ -93,10 +90,7 @@ const ProjectsListMapBox = ({
console.error(e)
}
})
)
.finally(() => setLoading(false))
// TODO: Check if needed when implementing filter story:
// this.updateList()
).finally(() => setLoading(false))
}, [plansApiUrl, extprojectApiUrl, privateprojectApiUrl, projectState, projectApiUrl])

useEffect(() => {
Expand Down
37 changes: 37 additions & 0 deletions meinberlin/react/projects/getDefaultState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const defaultState = {
search: '',
districts: [],
// organisation is a single select but its simpler to just work with an
// array because of the typeahead component
organisation: [],
participations: [],
topics: []
}

export const getDefaultState = (searchProfile) => {
let mergeData = {}

if (searchProfile) {
mergeData = {
search: searchProfile.query_text ?? '',
districts: searchProfile.districts.map(d => d.name),
organisation: searchProfile.organisations.map(o => o.name),
participations: searchProfile.project_types.map(p => p.id),
topics: searchProfile.topics.map((t) => t.code)
}
}

return {
...defaultState,
...mergeData
}
}

export const getDefaultProjectState = (searchProfile) => {
const mapping = ['active', 'past', 'future']
if (searchProfile && searchProfile.status.length) {
return searchProfile.status.map(s => mapping[s.status])
}

return ['active', 'future']
}
9 changes: 8 additions & 1 deletion meinberlin/test/factories/kiezradar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from adhocracy4.test import factories as a4_factories
from meinberlin.apps.kiezradar.models import KiezradarQuery
from meinberlin.apps.kiezradar.models import ProjectStatus
from meinberlin.apps.kiezradar.models import ProjectType
from meinberlin.apps.kiezradar.models import SearchProfile

Expand All @@ -13,7 +14,6 @@ class Meta:
user = factory.SubFactory(a4_factories.USER_FACTORY)
name = factory.Faker("sentence", nb_words=4)
description = factory.Faker("sentence", nb_words=16)
status = SearchProfile.STATUS_ONGOING


class KiezradarQueryFactory(factory.django.DjangoModelFactory):
Expand All @@ -28,3 +28,10 @@ class Meta:
model = ProjectType

participation = ProjectType.PARTICIPATION_INFORMATION


class ProjectStatusFactory(factory.django.DjangoModelFactory):
class Meta:
model = ProjectStatus

status = ProjectStatus.STATUS_ONGOING
2 changes: 2 additions & 0 deletions tests/kiezradar/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from pytest_factoryboy import register

from meinberlin.test.factories.kiezradar import KiezradarQueryFactory
from meinberlin.test.factories.kiezradar import ProjectStatusFactory
from meinberlin.test.factories.kiezradar import ProjectTypeFactory
from meinberlin.test.factories.kiezradar import SearchProfileFactory
from meinberlin.test.factories.organisations import OrganisationFactory

register(SearchProfileFactory)
register(KiezradarQueryFactory)
register(ProjectTypeFactory)
register(ProjectStatusFactory)
register(OrganisationFactory)
Loading

0 comments on commit d93b1e2

Please sign in to comment.