Skip to content

Commit d162d4d

Browse files
authored
Merge pull request #185 from mapswipe/fix/validation-project
2 parents 3d08e88 + b60178b commit d162d4d

37 files changed

+1201
-186
lines changed

apps/common/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,6 @@ def save_model(self, request, obj, form, change): # type: ignore[reportMissingP
195195
previous_announcements = Announcement.objects.exclude(id=obj.id)
196196
previous_announcements.update(is_active=False)
197197

198-
FirebaseAnnouncementPush(obj).trigger()
198+
FirebaseAnnouncementPush(obj).trigger(force_update=True)
199199
else:
200200
FirebaseAnnouncementPush(obj).trigger(delete=True)

apps/common/firebase/base.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ def handle_object_update_on_firebase(self, model_obj: T, fb_obj: K, fb_reference
5959
@abc.abstractmethod
6060
def get_firebase_path(self, firebase_id: str, model: type[T]) -> str: ...
6161

62-
def trigger(self, *, delete: bool | None = None) -> None:
62+
def trigger(
63+
self,
64+
*,
65+
delete: bool | None = None,
66+
force_update: bool | None = None,
67+
) -> None:
6368
model_obj = self.obj
6469
model_obj.update_firebase_push_status(FirebasePushStatusEnum.PENDING)
6570

@@ -73,7 +78,7 @@ def trigger(self, *, delete: bool | None = None) -> None:
7378
return
7479
self._delete(model_obj)
7580
else:
76-
self._push(model_obj)
81+
self._push(model_obj, force_update=force_update)
7782

7883
def _delete(self, model_obj: T) -> None:
7984
if model_obj.firebase_push_status_enum != FirebasePushStatusEnum.PENDING:
@@ -106,7 +111,12 @@ def _delete(self, model_obj: T) -> None:
106111
model_obj.firebase_push_status = None
107112
model_obj.save(update_fields=["firebase_last_pushed", "firebase_push_status"])
108113

109-
def _push(self, model_obj: T) -> None:
114+
def _push(
115+
self,
116+
model_obj: T,
117+
*,
118+
force_update: bool | None = None,
119+
) -> None:
110120
if model_obj.firebase_push_status_enum != FirebasePushStatusEnum.PENDING:
111121
logger.warning(
112122
"Firebase push error: push is not required for %s",
@@ -124,13 +134,14 @@ def _push(self, model_obj: T) -> None:
124134
fb_model: typing.Any = model_ref.get()
125135

126136
if not model_obj.firebase_last_pushed:
127-
if fb_model is not None:
137+
if not force_update and fb_model is not None:
128138
logger.error(
129139
"Firebase create error: %s already exists in Firebase",
130140
model_obj._meta.label,
131141
extra={"id": model_obj.pk},
132142
)
133143
raise InvalidObjectPushException
144+
134145
self.handle_new_object_on_firebase(model_obj, model_ref)
135146
else:
136147
if fb_model is None:

apps/common/tests/common_test.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from apps.common.models import Announcement
77
from apps.user.models import User
8+
from main.config import Config
89
from main.tests import TestCase
910

1011

@@ -14,18 +15,26 @@ class MockRequest(typing.NamedTuple):
1415

1516
class TestAnnouncement(TestCase):
1617
@typing.override
17-
def setUp(self):
18+
@classmethod
19+
def setUpClass(cls):
20+
super().setUpClass()
1821
# Create a superuser for admin login
1922
User = get_user_model()
20-
self.admin_user = User.objects.create_superuser( # type: ignore[reportAttributeAccessIssue]
23+
cls.admin_user = User.objects.create_superuser( # type: ignore[reportAttributeAccessIssue]
2124
2225
password="password123", # noqa: S106
2326
)
24-
self.client.login(email="[email protected]", password="password123") # noqa: S106
2527

2628
def test_create_announcement(self):
29+
request = MockRequest(user=self.admin_user)
30+
self.force_login(request.user)
31+
2732
url = reverse("admin:common_announcement_add")
2833

34+
announcement_ref = self.firebase_helper.ref(
35+
Config.FirebaseKeys.announcement(),
36+
)
37+
2938
data = {
3039
"client_id": "01K44YMVYTKY1R3906XW3QQK05",
3140
"text": "We have a new release v1.2.3",
@@ -43,6 +52,10 @@ def test_create_announcement(self):
4352
assert announcement_1.is_active
4453
assert announcement_1.url == "https://play.google.com/store/apps/details?id=org.missingmaps.mapswipe"
4554

55+
firebase_announcement: typing.Any = announcement_ref.get()
56+
assert firebase_announcement is not None
57+
assert firebase_announcement.get("url") == "https://play.google.com/store/apps/details?id=org.missingmaps.mapswipe"
58+
4659
# test active only one announcement at once
4760
data = {
4861
"client_id": "01K44ZF3KMS6GV2AD93EG6WP9X",
@@ -61,9 +74,32 @@ def test_create_announcement(self):
6174
assert announcement_2.is_active
6275
assert announcement_2.url == "https://mapswipe.org/en/blogs/2025-04-03-papua-new-guinea-swiping-to-find-airstrips"
6376

77+
firebase_announcement: typing.Any = announcement_ref.get()
78+
assert firebase_announcement is not None
79+
assert (
80+
firebase_announcement.get("url")
81+
== "https://mapswipe.org/en/blogs/2025-04-03-papua-new-guinea-swiping-to-find-airstrips"
82+
)
83+
6484
# check only one active announcement
6585
assert Announcement.objects.filter(is_active=True).count() == 1
66-
announcement_1.refresh_from_db()
6786

68-
# check if other announcements are inactive
87+
# check if current announcement is active
88+
announcement_1.refresh_from_db()
6989
assert not announcement_1.is_active
90+
91+
# test announcement de-activation
92+
url = reverse("admin:common_announcement_change", args=[announcement_2.pk])
93+
data = {
94+
"client_id": "01K44ZF3KMS6GV2AD93EG6WP9X",
95+
"text": "Checkout the latest blog post about airstrips",
96+
"is_active": False,
97+
"url": "https://mapswipe.org/en/blogs/2025-04-03-papua-new-guinea-swiping-to-find-airstrips",
98+
"created_by": self.admin_user.id,
99+
"modified_by": self.admin_user.id,
100+
}
101+
response = self.client.post(url, data, follow=True)
102+
assert response.status_code == 200
103+
104+
firebase_announcement: typing.Any = announcement_ref.get()
105+
assert firebase_announcement is None

apps/common/views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from rest_framework.views import APIView
1515

1616
from apps.contributor.models import ContributorUser
17+
from main.config import Config
1718

1819
from .serializers import FirebaseAuthRequestSerializer
1920

@@ -29,7 +30,7 @@
2930

3031
# FIXME: Maybe a better approach then this?
3132
def _get_version_from_pyproject(base_path: Path) -> str:
32-
data = toml.load(settings.BASE_DIR / base_path / "pyproject.toml")
33+
data = toml.load(Config.BASE_DIR / base_path / "pyproject.toml")
3334
return data["project"]["version"]
3435

3536

@@ -46,7 +47,7 @@ def render_to_response_json(
4647
**json.loads(response.content),
4748
"app": {
4849
"environment": settings.APP_ENVIRONMENT,
49-
"version": _get_version_from_pyproject(settings.BASE_DIR),
50+
"version": _get_version_from_pyproject(Config.BASE_DIR),
5051
"git": {
5152
"branch": git_helper.branch,
5253
"commit": git_helper.commit_sha,

apps/contributor/management/commands/create_contributor_users.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import typing
33
import uuid
44

5-
from django.conf import settings
65
from django.core.management.base import BaseCommand
76
from django.utils import timezone
87
from pyfirebase_mapswipe import extended_models as firebase_extended_models
@@ -62,7 +61,7 @@ class Command(BaseCommand):
6261

6362
@typing.override
6463
def handle(self, *args, **options): # type: ignore[reportMissingParameterType]
65-
if not settings.ENABLE_DANGER_MODE:
64+
if not Config.ENABLE_DANGER_MODE:
6665
logger.warning("Dummy data generation is disabled")
6766
return
6867

apps/contributor/tests/e2e_usergroup_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from pathlib import Path
44

55
import json5
6-
from django.conf import settings
76

87
from apps.common.utils import remove_object_keys
98
from apps.contributor.factories import ContributorUserFactory
109
from apps.user.factories import UserFactory
10+
from main.config import Config
1111
from main.tests import TestCase
1212

1313

@@ -83,7 +83,7 @@ def setUpClass(cls):
8383
def test_usergroup_e2e(self):
8484
self.force_login(self.user)
8585

86-
data_file = Path(settings.BASE_DIR, "assets/tests/usergroup/data.json5")
86+
data_file = Path(Config.BASE_DIR, "assets/tests/usergroup/data.json5")
8787
with data_file.open("r", encoding="utf-8") as f:
8888
test_data_list = json5.load(f)
8989

apps/project/exports/exports.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@
66
from django.db import transaction
77
from ulid import ULID
88

9-
from apps.common.models import AssetTypeEnum
9+
from apps.common.models import AssetTypeEnum, FirebasePushStatusEnum
1010
from apps.project.custom_options import get_fallback_custom_options_for_export
1111
from apps.project.exports.geojson import gzipped_csv_to_gzipped_geojson
1212
from apps.project.models import Project, ProjectAsset, ProjectAssetExportTypeEnum, ProjectProgressStatusEnum, ProjectTypeEnum
13-
from apps.project.tasks import send_slack_message_for_project
13+
from apps.project.tasks import push_project_to_firebase, send_slack_message_for_project
1414
from apps.user.models import User
1515
from main.config import Config
1616
from main.logging import log_extra
1717
from project_types.store import get_project_type_handler
1818
from project_types.tile_map_service.compare.project import CompareProjectProperty
1919
from project_types.tile_map_service.completeness.project import CompletenessProjectProperty
2020
from project_types.tile_map_service.find.project import FindProjectProperty
21+
from utils.geo.raster_tile_server.config import RasterTileServerNameEnum
2122

2223
from .mapping_results import generate_mapping_results
2324
from .mapping_results_aggregate.task import generate_mapping_results_aggregate_by_task
@@ -56,8 +57,32 @@ def _export_project_data(project: Project, tmp_directory: Path):
5657
# legacy system path: /api/hot_tm/hot_tm_{project.id}.geojson
5758
tmp_tasking_manager_hot_tm_geojson = tmp_directory / f"hot_tm_{project.id}.geojson"
5859

59-
# TODO: if maxar is used for tile_server_name, this should be true
60-
add_metadata = False
60+
# FIXME(tnagorra): move this to project handler
61+
tile_servers = set[RasterTileServerNameEnum]()
62+
if isinstance(
63+
project_type_handler.project_type_specifics,
64+
FindProjectProperty,
65+
):
66+
tile_servers.add(project_type_handler.project_type_specifics.tile_server_property.name)
67+
elif isinstance(
68+
project_type_handler.project_type_specifics,
69+
CompareProjectProperty,
70+
):
71+
tile_servers.add(project_type_handler.project_type_specifics.tile_server_property.name)
72+
tile_servers.add(project_type_handler.project_type_specifics.tile_server_b_property.name)
73+
elif isinstance(
74+
project_type_handler.project_type_specifics,
75+
CompletenessProjectProperty,
76+
):
77+
tile_servers.add(project_type_handler.project_type_specifics.tile_server_property.name)
78+
if project_type_handler.project_type_specifics.overlay_tile_server_property.raster:
79+
tile_servers.add(
80+
project_type_handler.project_type_specifics.overlay_tile_server_property.raster.tile_server.name,
81+
)
82+
83+
add_metadata = (
84+
RasterTileServerNameEnum.MAXAR_STANDARD in tile_servers or RasterTileServerNameEnum.MAXAR_PREMIUM in tile_servers
85+
)
6186

6287
custom_options_raw = []
6388

@@ -136,6 +161,7 @@ def _export_project_data(project: Project, tmp_directory: Path):
136161
tmp_project_stats_by_date_csv.name,
137162
)
138163

164+
# FIXME(tnagorra): move this to project handler
139165
generate_hot_tm_geometries = project.project_type_enum in [
140166
ProjectTypeEnum.COMPARE,
141167
ProjectTypeEnum.COMPLETENESS,
@@ -151,14 +177,17 @@ def _export_project_data(project: Project, tmp_directory: Path):
151177
)
152178

153179
if not project_stats_by_date_df.empty:
154-
project.progress = project_stats_by_date_df["cum_progress"].iloc[-1] * 100
155-
if project.progress >= 100:
156-
project.progress_status = ProjectProgressStatusEnum.COMPLETED
157180
project.number_of_contributor_users = project_stats_by_date_df["cum_number_of_users"].iloc[-1]
158181
project.number_of_results = project_stats_by_date_df["cum_number_of_results"].iloc[-1]
159182
project.number_of_results_for_progress = project_stats_by_date_df["cum_number_of_results_progress"].iloc[-1]
160183
project.last_contribution_date = project_stats_by_date_df.index[-1]
161-
# TODO: Trigger slack notifications on progress change
184+
185+
previous_progress = project.progress
186+
project.progress = project_stats_by_date_df["cum_progress"].iloc[-1] * 100
187+
188+
if project.progress >= 100:
189+
project.progress_status = ProjectProgressStatusEnum.COMPLETED
190+
162191
if project.progress >= 90 and project.slack_progress_notifications < 90:
163192
transaction.on_commit(
164193
lambda: send_slack_message_for_project.delay(project_id=project.id, action="progress-change"),
@@ -169,6 +198,13 @@ def _export_project_data(project: Project, tmp_directory: Path):
169198
lambda: send_slack_message_for_project.delay(project_id=project.id, action="progress-change"),
170199
)
171200

201+
if project.progress != previous_progress:
202+
# FIXME(tnagorra): Do we only send updates for the 2 fields?
203+
transaction.on_commit(
204+
lambda: push_project_to_firebase.delay(project_id=project.id),
205+
)
206+
project.update_firebase_push_status(FirebasePushStatusEnum.PENDING, False)
207+
172208
project.save(
173209
update_fields=(
174210
"progress",
@@ -177,6 +213,8 @@ def _export_project_data(project: Project, tmp_directory: Path):
177213
"number_of_results",
178214
"number_of_results_for_progress",
179215
"last_contribution_date",
216+
"firebase_push_status",
217+
"firebase_last_pushed",
180218
),
181219
)
182220

apps/project/exports/mapping_results_aggregate/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)