Skip to content

Commit b83cea6

Browse files
authored
Merge pull request #213 from mapswipe/fix/global-exports
fix/global exports
2 parents f1a9c83 + f13cc57 commit b83cea6

File tree

9 files changed

+111
-27
lines changed

9 files changed

+111
-27
lines changed

apps/common/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import typing
2-
from datetime import datetime
32

43
from django.contrib import admin
54
from django.db import models
65
from django.http import HttpRequest
6+
from django.utils import timezone
77
from djangoql.admin import DjangoQLSearchMixin # type: ignore[reportMissingTypeStubs]
88

99
from apps.common.firebase.push import FirebaseAnnouncementPush
@@ -127,7 +127,7 @@ def save_model(self, request, obj, form, change): # type: ignore[reportMissingP
127127
obj.modified_by = request.user
128128
if obj.is_archived:
129129
obj.archived_by = request.user
130-
obj.archived_at = datetime.now()
130+
obj.archived_at = timezone.now()
131131
else:
132132
obj.archived_by = None
133133
obj.archived_at = None

apps/common/firebase/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ class RelaxedModel(self.firebase_model_class):
164164
model_obj.update_firebase_push_status(FirebasePushStatusEnum.FAILED)
165165
except Exception:
166166
logger.error(
167-
"Firebase push error: Unexpected error occurred",
167+
"Firebase push error (%s): Unexpected error occurred",
168+
f"{self.firebase_model_class.__module__}.{self.firebase_model_class.__qualname__}",
168169
extra={"id": model_obj.pk},
169170
exc_info=True,
170171
)

apps/contributor/firebase/pull.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33

44
from django.db import connection, transaction
5+
from django.utils import timezone
56
from pyfirebase_mapswipe import extended_models as firebase_ext_models
67
from pyfirebase_mapswipe import models as firebase_models
78

@@ -37,13 +38,17 @@ def pull_users_from_firebase():
3738

3839
users_to_pull = list[ContributorUser]()
3940
for key, valid_user in valid_users:
41+
username = valid_user.username
42+
# XXX: For OSM users, firebase doesn't include username
43+
if username in ["", None, firebase_models.UNDEFINED]:
44+
username = key
4045
user = ContributorUser(
4146
firebase_id=key,
42-
username=valid_user.username or key, # XXX: For OSM users, firebase doesn't include username
47+
username=username,
4348
created_at=valid_user.created,
4449
modified_at=valid_user.created,
4550
# NOTE: Setting firebase_last_pushed so that we can send updates to firebase.
46-
firebase_last_pushed=datetime.datetime.now(),
51+
firebase_last_pushed=timezone.now(),
4752
firebase_push_status=FirebasePushStatusEnum.SUCCESS,
4853
)
4954
users_to_pull.append(user)

apps/existing_database/management/commands/loaddata_from_existing_database.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,8 @@ def create_project(
574574
requesting_organization=get_organization_by_name(requesting_organization, bot_user),
575575
created_by_id=get_user_by_contributor_user_firebase_id(existing_project.created_by, fallback=bot_user),
576576
modified_by_id=get_user_by_contributor_user_firebase_id(existing_project.created_by, fallback=bot_user),
577-
project_type_specifics=existing_project.project_type_specifics,
577+
# This was modified in the database manually for some projects
578+
# project_type_specifics=existing_project.project_type_specifics,
578579
description=existing_project.project_details.strip() if existing_project.project_details else "",
579580
)
580581

apps/mapping/firebase/utils.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import csv
12
import logging
23
import typing
4+
from pathlib import Path
35

46
import dateutil.parser
57
from django.db import connection, transaction
@@ -322,6 +324,68 @@ def _cleanup(_cursor: "CursorWrapper"):
322324
_cleanup(cursor_)
323325

324326

327+
def process_invalid_temp_results(
328+
firebase_cleanup: FirebaseCleanup,
329+
):
330+
base_qs = MappingSessionResultTemp.objects.filter(is_firebase_mapping_valid=False)
331+
332+
invalid_results_count = base_qs.count()
333+
if invalid_results_count == 0:
334+
return
335+
336+
logger.warning("%s results has been flagged as invalid", invalid_results_count)
337+
338+
# Add unsynced user to firebase, which will be processed by the user sync task
339+
invalid_user_firebase_ids = (
340+
base_qs.filter(contributor_user_id__isnull=True).values_list("contributor_user_firebase_id", flat=True).distinct()
341+
)
342+
for invalid_user_firebase_id in invalid_user_firebase_ids:
343+
logger.warning(
344+
"Adding %s to the firebase user update %s",
345+
invalid_user_firebase_id,
346+
Config.FirebaseKeys.contributor_user_updates(),
347+
)
348+
Config.FIREBASE_HELPER.ref(
349+
Config.FirebaseKeys.contributor_user_update(invalid_user_firebase_id),
350+
).set(True)
351+
352+
# Skip firebase cleanup for invalid mapping data
353+
invalid_result_temp_qs = base_qs.values_list(
354+
"project_firebase_id",
355+
"group_firebase_id",
356+
"contributor_user_firebase_id",
357+
).distinct()
358+
359+
for project_firebase_id, group_firebase_id, contributor_user_firebase_id in invalid_result_temp_qs:
360+
firebase_cleanup.undo_mark_as_delete(
361+
project_firebase_id=project_firebase_id,
362+
group_firebase_id=group_firebase_id,
363+
contributor_user_firebase_id=contributor_user_firebase_id,
364+
)
365+
366+
try:
367+
# NOTE: For debugging, store the latest invalid dataset to internal directory
368+
with Path.open(
369+
Config.InternalDir.LAST_RUN_MAPPING_SESSION_INVALID_DATA,
370+
"w",
371+
newline="",
372+
encoding="utf-8",
373+
) as f:
374+
fields = [f.name for f in MappingSessionResultTemp._meta.fields]
375+
376+
writer = csv.DictWriter(f, fieldnames=fields)
377+
writer.writeheader()
378+
379+
for row in base_qs.values(*fields).iterator(chunk_size=2000):
380+
writer.writerow(row)
381+
logger.info("Stored invalid mapping data to %s", Config.InternalDir.LAST_RUN_MAPPING_SESSION_INVALID_DATA)
382+
except Exception:
383+
logger.error(
384+
"Failed to generate mapping session invalid data export to internal directory",
385+
exc_info=True,
386+
)
387+
388+
325389
def transfer_results_from_temp_tables(
326390
firebase_cleanup: FirebaseCleanup,
327391
):
@@ -337,21 +401,7 @@ def transfer_results_from_temp_tables(
337401
cursor.execute(SQL_QUERY_TO_TRANSFER_TEMP_TABLE_DATA_TO_MAPPING_SESSION_USER_GROUP)
338402
logger.info("Transferred staging results to real tables")
339403

340-
invalid_result_temp_qs = (
341-
MappingSessionResultTemp.objects.filter(is_firebase_mapping_valid=False)
342-
.values_list(
343-
"project_firebase_id",
344-
"group_firebase_id",
345-
"contributor_user_firebase_id",
346-
)
347-
.distinct()
348-
)
349-
for project_firebase_id, group_firebase_id, contributor_user_firebase_id in invalid_result_temp_qs:
350-
firebase_cleanup.undo_mark_as_delete(
351-
project_firebase_id=project_firebase_id,
352-
group_firebase_id=group_firebase_id,
353-
contributor_user_firebase_id=contributor_user_firebase_id,
354-
)
404+
process_invalid_temp_results(firebase_cleanup)
355405

356406
cleanup_temp_tables(cursor)
357407

apps/project/exports/overall_stats.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import typing
77
from pathlib import Path
88

9+
from django.contrib.gis.db.models.functions import AsWKT
910
from django.core.files.base import ContentFile
1011
from django.db import models
1112
from django.db.models.fields.files import FieldFile
@@ -43,7 +44,7 @@ def regenerate_project_stats_by_types_csv():
4344
"project_type": None,
4445
"project_type_display": MANUAL_FIELD,
4546
"projects_count": models.Count("*"),
46-
"total_area_sqkm": models.F("total_area"),
47+
"total_area_sqkm": models.Sum("total_area"),
4748
"total_number_of_results": models.Count("number_of_results"),
4849
"total_number_of_results_progress": models.Count("number_of_results_for_progress"),
4950
"average_number_of_users_per_project": models.Avg("number_of_contributor_users"),
@@ -75,8 +76,10 @@ def regenerate_project_stats_by_types_csv():
7576

7677
def regenerate_projects_csv(temp_projects_csv: typing.IO): # type: ignore[reportMissingTypeArgument]
7778
logger.info("Processing regenerate_projects_csv")
79+
7880
fieldnames = {
7981
"id": None,
82+
"firebase_id": None,
8083
"name": Project.generate_name_query(),
8184
"description": None,
8285
"look_for": None,
@@ -90,8 +93,10 @@ def regenerate_projects_csv(temp_projects_csv: typing.IO): # type: ignore[repor
9093
"status": None,
9194
"status_display": MANUAL_FIELD,
9295
"area_sqkm": models.F("aoi_geometry__total_area"),
93-
"centroid": None, # TODO: use this after removing from model models.F("aoi_geometry__centroid"),
94-
"geom": models.F("aoi_geometry__geometry"),
96+
# TODO: Change _centroid to centroid after `centroid` field is removed from the project's table
97+
"centroid": MANUAL_FIELD,
98+
"_centroid": AsWKT("aoi_geometry__centroid"),
99+
"geom": AsWKT("aoi_geometry__geometry"),
95100
"progress": None, # NOTE: This is changed to float later
96101
"number_of_contributor_users": None,
97102
"number_of_results": None,
@@ -100,6 +105,9 @@ def regenerate_projects_csv(temp_projects_csv: typing.IO): # type: ignore[repor
100105
}
101106

102107
projects_aggregate_qs = _project_queryset(fieldnames)
108+
109+
fieldnames.pop("_centroid")
110+
103111
writer = csv.DictWriter(temp_projects_csv, fieldnames=fieldnames)
104112
writer.writeheader()
105113

@@ -113,6 +121,8 @@ def regenerate_projects_csv(temp_projects_csv: typing.IO): # type: ignore[repor
113121
name=image_file,
114122
),
115123
)
124+
# TODO: Remove this logic to set centroid after `centroid` field is removed from the project's table
125+
data["centroid"] = data.pop("_centroid")
116126
data["image_url"] = image_file_url
117127
data["status_display"] = ProjectStatusEnum(data["status"]).label
118128
data["project_type_display"] = ProjectTypeEnum(data["project_type"]).label
@@ -144,6 +154,13 @@ def _regenerate_projects_centroid_for_geometry_field(
144154
tmp_geojson_outfile = Config.TEMP_DIR / f"projects_centroid_{geometry_field}_{get_random_string(6)}.geojson"
145155
inputfile_without_path = projects_csv_inputfile.name.split("/")[-1].replace(".csv", "")
146156

157+
# TODO: Use EXCLUDE after upgrading gdal to > 3.9.0 https://github.com/OSGeo/gdal/pull/8675
158+
# With that, we can use `SELECT * EXCLUDE(geom), CAST(...` to exclude one column
159+
with Path.open(projects_csv_inputfile, "r") as fp:
160+
csv_reader = csv.DictReader(fp)
161+
inputfile_columns = [column for column in csv_reader.fieldnames or [] if column != "geom"]
162+
inputfile_columns_str = ",".join(inputfile_columns)
163+
147164
subprocess.run( # noqa: S603
148165
[
149166
"/usr/bin/ogr2ogr",
@@ -154,7 +171,7 @@ def _regenerate_projects_centroid_for_geometry_field(
154171
str(tmp_geojson_outfile),
155172
str(projects_csv_inputfile),
156173
"-sql",
157-
f'SELECT *, CAST({geometry_field} as geometry) FROM "{inputfile_without_path}"',
174+
f'SELECT {inputfile_columns_str}, CAST({geometry_field} as geometry) FROM "{inputfile_without_path}"',
158175
],
159176
check=True,
160177
)

firebase

main/config.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import os
22
import typing
33
from dataclasses import dataclass
4+
from pathlib import Path
45

56
from django.conf import settings
67

78
if typing.TYPE_CHECKING:
8-
from pathlib import Path
99
from urllib.parse import ParseResult as URLParseResult
1010

1111
from utils.firebase import FirebaseHelper
@@ -58,6 +58,11 @@ class Config:
5858
EXISTING_SYSTEM_API = typing.cast("URLParseResult", getattr(settings, "EXISTING_SYSTEM_API", None))
5959
EXISTING_SYSTEM_API_INSECURE = typing.cast("bool", getattr(settings, "EXISTING_SYSTEM_API_INSECURE", False))
6060

61+
class InternalDir:
62+
INTERNAL_ROOT = Path(settings.INTERNAL_ROOT)
63+
64+
LAST_RUN_MAPPING_SESSION_INVALID_DATA = INTERNAL_ROOT / "last-run-invalid-mapping-sessisons.csv"
65+
6166
class CommunityDashboardKeys:
6267
@staticmethod
6368
def contributor_user(firebase_id: str):
@@ -163,6 +168,8 @@ def announcement():
163168
return "/v2/announcement"
164169

165170

171+
Config.InternalDir.INTERNAL_ROOT.mkdir(parents=True, exist_ok=True)
172+
166173
# FIXME: Import utils/geo/raster_tile_server/config.py here
167174
# FIXME: Import utils/geo/vector_tile_server/config.py here
168175

main/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def urlparse(value) -> ParseResult:
8484
# -- Filesystem (default) XXX: Don't use in production?
8585
MEDIA_ROOT=(str, BASE_DIR / ".data/media"),
8686
STATIC_ROOT=(str, BASE_DIR / ".data/static"),
87+
INTERNAL_ROOT=(str, BASE_DIR / ".data/internal"),
8788
# Email
8889
EMAIL_HOST=str,
8990
EMAIL_SUBJECT_PREFIX=(str, "Mapswipe:"),
@@ -385,6 +386,8 @@ def urlparse(value) -> ParseResult:
385386
},
386387
}
387388

389+
INTERNAL_ROOT = env("INTERNAL_ROOT")
390+
388391
assert STORAGE_OVERWRITE_KEY in STORAGES, f"{STORAGE_OVERWRITE_KEY} should be defined in STORAGES"
389392

390393
# Default primary key field type

0 commit comments

Comments
 (0)