Skip to content

Commit 1c36f53

Browse files
Merge pull request #368 from City-of-Helsinki/1.1.9
1.1.9
2 parents d4440b6 + 9270edf commit 1c36f53

30 files changed

Lines changed: 6772 additions & 257 deletions

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Backups
22
*~
33

4+
# Copilot instructions
5+
.github/copilot-instructions.md
6+
#vscode
7+
.vscode/
48
# Byte-compiled / optimized / DLL files
59
__pycache__/
610
*.py[cod]
@@ -116,3 +120,15 @@ media/
116120

117121
# MacOS
118122
.DS_Store
123+
124+
# Locally removed files
125+
010925_projektitiedot.xlsx
126+
1.0_aikataulutiedot.xlsx
127+
1.0_projektitiedot.xlsx
128+
121125_projektitiedot.xlsx
129+
200225_aikataulutiedot.xlsx
130+
230925_projektitiedot.xlsx
131+
media.zip
132+
test_251125_aikataulutiedot.xlsx
133+
test_261125_aikataulutiedot.xlsx
134+
~$040425_projektitiedot.xlsx

conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import os
2+
import django
3+
4+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kaavapino.settings")
5+
django.setup()

projects/deadline_utils.py

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
"""
2+
Shared utilities for deadline management.
3+
4+
This module contains constants and functions used across deadline-related
5+
commands and features, particularly for KAAV-3492 stale deadline detection/cleanup.
6+
"""
7+
from projects.serializers.utils import VIS_BOOL_MAP
8+
9+
10+
# Map each deadline group to its associated date fields
11+
# These are the fields that should be cleared when the group is deleted
12+
DEADLINE_GROUP_DATE_FIELDS = {
13+
# Periaatteet esilläolo
14+
'periaatteet_esillaolokerta_1': [
15+
'milloin_periaatteet_esillaolo_alkaa',
16+
'milloin_periaatteet_esillaolo_paattyy',
17+
'periaatteet_esillaolo_aineiston_maaraaika',
18+
'viimeistaan_mielipiteet_periaatteista',
19+
],
20+
'periaatteet_esillaolokerta_2': [
21+
'milloin_periaatteet_esillaolo_alkaa_2',
22+
'milloin_periaatteet_esillaolo_paattyy_2',
23+
'periaatteet_esillaolo_aineiston_maaraaika_2',
24+
'viimeistaan_mielipiteet_periaatteista_2',
25+
],
26+
'periaatteet_esillaolokerta_3': [
27+
'milloin_periaatteet_esillaolo_alkaa_3',
28+
'milloin_periaatteet_esillaolo_paattyy_3',
29+
'periaatteet_esillaolo_aineiston_maaraaika_3',
30+
'viimeistaan_mielipiteet_periaatteista_3',
31+
],
32+
# Periaatteet lautakunta
33+
'periaatteet_lautakuntakerta_1': [
34+
'milloin_periaatteet_lautakunnassa',
35+
'periaatteet_lautakunta_aineiston_maaraaika',
36+
],
37+
'periaatteet_lautakuntakerta_2': [
38+
'milloin_periaatteet_lautakunnassa_2',
39+
'periaatteet_lautakunta_aineiston_maaraaika_2',
40+
],
41+
'periaatteet_lautakuntakerta_3': [
42+
'milloin_periaatteet_lautakunnassa_3',
43+
'periaatteet_lautakunta_aineiston_maaraaika_3',
44+
],
45+
'periaatteet_lautakuntakerta_4': [
46+
'milloin_periaatteet_lautakunnassa_4',
47+
'periaatteet_lautakunta_aineiston_maaraaika_4',
48+
],
49+
# OAS esilläolo
50+
'oas_esillaolokerta_1': [
51+
'milloin_oas_esillaolo_alkaa',
52+
'milloin_oas_esillaolo_paattyy',
53+
'oas_esillaolo_aineiston_maaraaika',
54+
'viimeistaan_mielipiteet_oas',
55+
],
56+
'oas_esillaolokerta_2': [
57+
'milloin_oas_esillaolo_alkaa_2',
58+
'milloin_oas_esillaolo_paattyy_2',
59+
'oas_esillaolo_aineiston_maaraaika_2',
60+
'viimeistaan_mielipiteet_oas_2',
61+
],
62+
'oas_esillaolokerta_3': [
63+
'milloin_oas_esillaolo_alkaa_3',
64+
'milloin_oas_esillaolo_paattyy_3',
65+
'oas_esillaolo_aineiston_maaraaika_3',
66+
'viimeistaan_mielipiteet_oas_3',
67+
],
68+
# Luonnos esilläolo
69+
'luonnos_esillaolokerta_1': [
70+
'milloin_luonnos_esillaolo_alkaa',
71+
'milloin_luonnos_esillaolo_paattyy',
72+
'kaavaluonnos_esillaolo_aineiston_maaraaika',
73+
'viimeistaan_mielipiteet_luonnos',
74+
],
75+
'luonnos_esillaolokerta_2': [
76+
'milloin_luonnos_esillaolo_alkaa_2',
77+
'milloin_luonnos_esillaolo_paattyy_2',
78+
'kaavaluonnos_esillaolo_aineiston_maaraaika_2',
79+
'viimeistaan_mielipiteet_luonnos_2',
80+
],
81+
'luonnos_esillaolokerta_3': [
82+
'milloin_luonnos_esillaolo_alkaa_3',
83+
'milloin_luonnos_esillaolo_paattyy_3',
84+
'kaavaluonnos_esillaolo_aineiston_maaraaika_3',
85+
'viimeistaan_mielipiteet_luonnos_3',
86+
],
87+
# Luonnos lautakunta
88+
'luonnos_lautakuntakerta_1': [
89+
'milloin_kaavaluonnos_lautakunnassa',
90+
'kaavaluonnos_kylk_aineiston_maaraaika',
91+
],
92+
'luonnos_lautakuntakerta_2': [
93+
'milloin_kaavaluonnos_lautakunnassa_2',
94+
'kaavaluonnos_kylk_aineiston_maaraaika_2',
95+
],
96+
'luonnos_lautakuntakerta_3': [
97+
'milloin_kaavaluonnos_lautakunnassa_3',
98+
'kaavaluonnos_kylk_aineiston_maaraaika_3',
99+
],
100+
'luonnos_lautakuntakerta_4': [
101+
'milloin_kaavaluonnos_lautakunnassa_4',
102+
'kaavaluonnos_kylk_aineiston_maaraaika_4',
103+
],
104+
# Ehdotus nähtävilläolo
105+
'ehdotus_nahtavillaolokerta_1': [
106+
'milloin_ehdotuksen_nahtavilla_alkaa_pieni',
107+
'milloin_ehdotuksen_nahtavilla_alkaa_iso',
108+
'milloin_ehdotuksen_nahtavilla_paattyy',
109+
'ehdotus_nahtaville_aineiston_maaraaika',
110+
'viimeistaan_lausunnot_ehdotuksesta',
111+
],
112+
'ehdotus_nahtavillaolokerta_2': [
113+
'milloin_ehdotuksen_nahtavilla_alkaa_pieni_2',
114+
'milloin_ehdotuksen_nahtavilla_alkaa_iso_2',
115+
'milloin_ehdotuksen_nahtavilla_paattyy_2',
116+
'ehdotus_nahtaville_aineiston_maaraaika_2',
117+
'viimeistaan_lausunnot_ehdotuksesta_2',
118+
],
119+
'ehdotus_nahtavillaolokerta_3': [
120+
'milloin_ehdotuksen_nahtavilla_alkaa_pieni_3',
121+
'milloin_ehdotuksen_nahtavilla_alkaa_iso_3',
122+
'milloin_ehdotuksen_nahtavilla_paattyy_3',
123+
'ehdotus_nahtaville_aineiston_maaraaika_3',
124+
'viimeistaan_lausunnot_ehdotuksesta_3',
125+
],
126+
'ehdotus_nahtavillaolokerta_4': [
127+
'milloin_ehdotuksen_nahtavilla_alkaa_pieni_4',
128+
'milloin_ehdotuksen_nahtavilla_alkaa_iso_4',
129+
'milloin_ehdotuksen_nahtavilla_paattyy_4',
130+
'ehdotus_nahtaville_aineiston_maaraaika_4',
131+
'viimeistaan_lausunnot_ehdotuksesta_4',
132+
],
133+
# Ehdotus lautakunta
134+
'ehdotus_lautakuntakerta_1': [
135+
'milloin_kaavaehdotus_lautakunnassa',
136+
'ehdotus_kylk_aineiston_maaraaika',
137+
],
138+
'ehdotus_lautakuntakerta_2': [
139+
'milloin_kaavaehdotus_lautakunnassa_2',
140+
],
141+
'ehdotus_lautakuntakerta_3': [
142+
'milloin_kaavaehdotus_lautakunnassa_3',
143+
],
144+
'ehdotus_lautakuntakerta_4': [
145+
'milloin_kaavaehdotus_lautakunnassa_4',
146+
],
147+
# Tarkistettu ehdotus lautakunta
148+
'tarkistettu_ehdotus_lautakuntakerta_1': [
149+
'milloin_tarkistettu_ehdotus_lautakunnassa',
150+
'tarkistettu_ehdotus_kylk_aineiston_maaraaika',
151+
],
152+
'tarkistettu_ehdotus_lautakuntakerta_2': [
153+
'milloin_tarkistettu_ehdotus_lautakunnassa_2',
154+
'tarkistettu_ehdotus_kylk_aineiston_maaraaika_2',
155+
],
156+
'tarkistettu_ehdotus_lautakuntakerta_3': [
157+
'milloin_tarkistettu_ehdotus_lautakunnassa_3',
158+
'tarkistettu_ehdotus_kylk_aineiston_maaraaika_3',
159+
],
160+
'tarkistettu_ehdotus_lautakuntakerta_4': [
161+
'milloin_tarkistettu_ehdotus_lautakunnassa_4',
162+
'tarkistettu_ehdotus_kylk_aineiston_maaraaika_4',
163+
],
164+
}
165+
166+
167+
def find_stale_deadline_fields(attribute_data):
168+
"""
169+
Find stale deadline date fields in project attribute data.
170+
171+
A date field is considered stale when:
172+
1. The associated deadline group's visibility bool is False
173+
2. But the date field still has a value
174+
175+
Args:
176+
attribute_data (dict): The project's attribute_data dictionary
177+
178+
Returns:
179+
list: List of tuples (deadline_group, vis_bool_name, stale_fields_list)
180+
where stale_fields_list is a list of dicts with 'field' and 'value' keys
181+
"""
182+
stale_data = []
183+
attr_data = attribute_data or {}
184+
185+
for deadline_group, vis_bool_name in VIS_BOOL_MAP.items():
186+
# Skip groups without visibility bools (kaynnistys, hyvaksyminen, voimaantulo)
187+
if vis_bool_name is None:
188+
continue
189+
190+
# Get the visibility bool value
191+
vis_bool_value = attr_data.get(vis_bool_name)
192+
193+
# Only check if vis_bool is explicitly False
194+
if vis_bool_value is not False:
195+
continue
196+
197+
# Get date fields for this group
198+
date_fields = DEADLINE_GROUP_DATE_FIELDS.get(deadline_group, [])
199+
200+
# Check for stale dates
201+
stale_fields = []
202+
for field in date_fields:
203+
value = attr_data.get(field)
204+
if value is not None:
205+
stale_fields.append({
206+
'field': field,
207+
'value': value,
208+
})
209+
210+
if stale_fields:
211+
stale_data.append((deadline_group, vis_bool_name, stale_fields))
212+
213+
return stale_data
214+
215+
216+
def clean_stale_deadline_fields(attribute_data):
217+
"""
218+
Clear deadline date fields when their visibility bool is False.
219+
220+
When a visibility bool is explicitly False, sets associated date fields to None
221+
to prevent stale data from leaking through during dict merge.
222+
223+
Args:
224+
attribute_data (dict): The project's attribute_data (modified in-place)
225+
226+
Returns:
227+
int: Number of fields that were cleared
228+
"""
229+
if not isinstance(attribute_data, dict):
230+
return 0
231+
232+
cleared_count = 0
233+
234+
for deadline_group, vis_bool_name in VIS_BOOL_MAP.items():
235+
# Skip groups without visibility bools
236+
if vis_bool_name is None:
237+
continue
238+
239+
# Only clean if vis_bool is explicitly False
240+
vis_bool_value = attribute_data.get(vis_bool_name)
241+
if vis_bool_value is not False:
242+
continue
243+
244+
# Get date fields for this group
245+
date_fields = DEADLINE_GROUP_DATE_FIELDS.get(deadline_group, [])
246+
247+
# Set None to override DB values during merge: {**db_data, **request_data}
248+
for field in date_fields:
249+
# Only count as cleared if value was non-None (for idempotency)
250+
if attribute_data.get(field) is not None:
251+
cleared_count += 1
252+
attribute_data[field] = None
253+
254+
return cleared_count

projects/helpers.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,32 @@ def safe_float(value):
774774
return float(0)
775775

776776

777+
def check_visibility(project, attribute):
778+
visibility_conditions = attribute.visibility_conditions
779+
if visibility_conditions is None:
780+
return True
781+
782+
for condition in visibility_conditions:
783+
try:
784+
operator = condition["operator"]
785+
variable = condition["variable"]
786+
comparison_value = condition["comparison_value"]
787+
788+
attribute_data_value = project.attribute_data.get(variable, None)
789+
if attribute_data_value is not None:
790+
if operator == "==":
791+
if attribute_data_value == comparison_value:
792+
return True
793+
elif operator == "!=":
794+
if attribute_data_value != comparison_value:
795+
return True
796+
except Exception as ex:
797+
log.error(f"Error on visibility check for attribute: {attribute.identifier}", ex)
798+
return True
799+
800+
return False
801+
802+
777803
def get_attribute_data_filtered_response(attributes, generated_attributes, ignored, project, use_cached=True):
778804
cache_key = f'attribute_data_filtered_{project.pk}'
779805
response = cache.get(cache_key) if use_cached else None
@@ -796,6 +822,9 @@ def get_attribute_data_filtered_response(attributes, generated_attributes, ignor
796822
response[identifier] = ""
797823
continue
798824

825+
if not check_visibility(project, attribute):
826+
continue
827+
799828
if attribute.value_type == "fieldset":
800829
fieldset = []
801830
for entry in value: # fieldset

0 commit comments

Comments
 (0)