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 8, 2025
1 parent 1b5e0d6 commit 01a5ab6
Show file tree
Hide file tree
Showing 15 changed files with 207 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
18 changes: 15 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,20 @@ 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,
"name": project_status.get_status_display(),
}
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}
{"id": topic.id, "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|default:"" }}"
data-participation-choices="{{ participation_choices }}"></div>
{% endblock content %}

Expand Down
23 changes: 23 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 @@ -97,6 +100,25 @@ def get_participation_choices(self):
choices = [str(choice[1]) for choice in Plan.participation.field.choices]
return json.dumps(choices)

def get_search_profile(self):
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
)
return (
JSONRenderer()
.render(SearchProfileSerializer(instance=search_profile).data)
.decode("utf-8")
)
except SearchProfile.DoesNotExist:
pass
return None

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

Expand All @@ -113,6 +135,7 @@ def get_context_data(self, **kwargs):
if hasattr(settings, "A4_OPENMAPTILES_TOKEN"):
omt_token = settings.A4_OPENMAPTILES_TOKEN

context["search_profile"] = self.get_search_profile()
context["districts"] = self.get_district_polygons()
context["organisations"] = self.get_organisations()
context["district_names"] = self.get_district_names()
Expand Down
1 change: 1 addition & 0 deletions meinberlin/react/kiezradar/SearchProfile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default function SearchProfile ({ apiUrl, planListUrl, profile: profile_,
]
.map((filter) => filter.map(({ name }) => name))
.map((names) => names.join(', '))
.filter(Boolean)

return (
<div className="search-profile">
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 = el.getAttribute('data-search-profile') && 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']
}
Loading

0 comments on commit 01a5ab6

Please sign in to comment.