Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1b76e14
fix(project): update uniqueness constraint on project tasks
tnagorra Oct 13, 2025
2fb8662
fix(tileserver): remove maxar tile servers from the list
tnagorra Oct 13, 2025
cb034ac
fix(project): fix area conversion to km sq
tnagorra Oct 13, 2025
211bd2d
fix(project): fix type for slack_progress_notifications
tnagorra Oct 13, 2025
8424a73
fix(validate-image): use annotation_id instead of image_id when possible
tnagorra Oct 13, 2025
bfcd563
fix(loaddata): add missing projects's project_type_specifics and desc…
thenav56 Oct 14, 2025
b190c38
fix(client-type): add missing ios mappings
thenav56 Oct 14, 2025
8ac62f9
fix(export): use ISO8601 date format for mapping export
thenav56 Oct 14, 2025
7c57a2d
fix(release): update uv sync command
thenav56 Oct 14, 2025
6836329
chore(project): add deprecation_reason for project's total_area
thenav56 Oct 14, 2025
7321bf3
fix(contributor): set firebase status initially during sync
tnagorra Oct 14, 2025
01fffc2
fix(slack): update project messages
tnagorra Oct 14, 2025
e08ff69
fix(project): change default group_size when creating a project
tnagorra Oct 15, 2025
885c55b
fix(project): set 0 as default for total_area and number_of_tasks
tnagorra Oct 15, 2025
0a6a16a
fix(project): handle exceptions from external sources
tnagorra Oct 15, 2025
05b7563
feat(project): update only necessary field to firebase for project stats
tnagorra Oct 15, 2025
f8197b2
feat!(firebase): add FirebaseOrInternalIdInputType
thenav56 Oct 15, 2025
c640894
feat(public): show PAUSED projects in public endpoint
thenav56 Oct 15, 2025
60bf58a
fix(tasks): fix use of context manager for redis lock
thenav56 Oct 15, 2025
c37f5ac
fix(loaddata): fix project's status mapping
thenav56 Oct 15, 2025
f42c8ce
chore(firebase): update submodule for firebase
tnagorra Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/common/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def clear_expired_django_sessions():
if not acquired:
logger.warning("Clear expired django sessions")
return
management.call_command("clearsessions", verbosity=0)
management.call_command("clearsessions", verbosity=0)


@shared_task
Expand Down
14 changes: 8 additions & 6 deletions apps/community_dashboard/graphql/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
import strawberry
from asgiref.sync import sync_to_async
from django.db import models
from django.shortcuts import aget_object_or_404
from django.utils import timezone

from apps.community_dashboard.models import AggregatedUserGroupStatData, AggregatedUserStatData
from apps.contributor.models import ContributorUser
from apps.contributor.models import ContributorUser, ContributorUserGroup
from utils.graphql.inputs import FirebaseOrInternalIdInputType

from .types import (
AggregateHelper,
Expand Down Expand Up @@ -117,17 +117,19 @@ async def community_filtered_stats(
) -> CommunityFilteredStats:
return CommunityFilteredStats(date_range=date_range)

# By Internal ID
@strawberry.field
async def community_user_stats(
self,
firebase_id: strawberry.ID,
user_id: FirebaseOrInternalIdInputType,
) -> ContributorUserStats:
user = await aget_object_or_404(ContributorUser, firebase_id=firebase_id)
user = await FirebaseOrInternalIdInputType.aget_object_or_404(ContributorUser, object_id=user_id)
return ContributorUserStats(user=user)

@strawberry.field
async def community_user_group_stats(
self,
user_group_id: strawberry.ID,
user_group_id: FirebaseOrInternalIdInputType,
) -> ContributorUserGroupStats:
return ContributorUserGroupStats(user_group_id=int(user_group_id))
user_group = await FirebaseOrInternalIdInputType.aget_object_or_404(ContributorUserGroup, object_id=user_group_id)
return ContributorUserGroupStats(user_group=user_group)
14 changes: 9 additions & 5 deletions apps/community_dashboard/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django_cte import With # type: ignore[reportMissingTypeStubs]

from apps.community_dashboard.models import AggregatedUserGroupStatData, AggregatedUserStatData
from apps.contributor.models import ContributorUser
from apps.contributor.models import ContributorUser, ContributorUserGroup
from apps.project.models import Project, ProjectTypeEnum
from utils.graphql.inputs import DateRangeInput
from utils.graphql.types import AreaSqKm, GenericJSON
Expand Down Expand Up @@ -349,6 +349,10 @@ def __post_init__(self, user: ContributorUser):
async def id(self) -> strawberry.ID:
return typing.cast("strawberry.ID", self._user.pk)

@strawberry.field
async def firebase_id(self) -> strawberry.ID:
return typing.cast("strawberry.ID", self._user.firebase_id)

@strawberry.field
async def stats(self) -> ContributorUserStatType:
# TODO: Cache this
Expand Down Expand Up @@ -423,15 +427,15 @@ def __post_init__(self, date_range: DateRangeInput | None, user_group_id: int):

@strawberry.type
class ContributorUserGroupStats:
user_group_id: InitVar[int]
user_group: InitVar[ContributorUserGroup]

_user_group_id: strawberry.Private[int] = dataclass_field(init=False)
_ug_qs: strawberry.Private[models.QuerySet[AggregatedUserGroupStatData]] = dataclass_field(init=False)

def __post_init__(self, user_group_id: int):
self._user_group_id = user_group_id
def __post_init__(self, user_group: ContributorUserGroup):
self._user_group_id = user_group.pk
self._ug_qs = AggregatedUserGroupStatData.objects.filter(
user_group_id=user_group_id,
user_group_id=user_group.pk,
)

@strawberry.field
Expand Down
2 changes: 1 addition & 1 deletion apps/community_dashboard/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ def update_aggregated_data():
logger.warning("Community Dashboard update aggregate already running")
return

UpdateAggregateCommand().handle()
UpdateAggregateCommand().handle()
10 changes: 5 additions & 5 deletions apps/community_dashboard/tests/query_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def test_filtered_community_stats(self):
def test_user_group_aggregated_calc(self):
query = """
query MyQuery($userGroupId: ID!) {
communityUserGroupStats(userGroupId: $userGroupId) {
communityUserGroupStats(userGroupId: {id: $userGroupId}) {
stats {
totalAreaSwiped
totalContributors
Expand Down Expand Up @@ -306,7 +306,7 @@ def test_user_group_aggregated_calc(self):
def test_user_group_query(self):
query = """
query MyQuery($userGroupId: ID!, $pagination: OffsetPaginationInput!) {
contributorUserGroup(id: $userGroupId) {
contributorUserGroup(userGroupId: {id: $userGroupId}) {
id
name
createdAt
Expand Down Expand Up @@ -502,13 +502,13 @@ def test_user_query(self):
$toDate: Date!,
) {

contributorUserByFirebaseId(firebaseId: $firebaseId) {
contributorUser(userId: {firebaseId: $firebaseId}) {
id
firebaseId
username
}

communityUserStats(firebaseId: $firebaseId) {
communityUserStats(userId: {firebaseId: $firebaseId}) {
id
stats {
totalSwipes
Expand Down Expand Up @@ -644,7 +644,7 @@ def test_user_query(self):
)

assert {
"contributorUserByFirebaseId": {
"contributorUser": {
"id": self.gID(contributor_user.pk),
"firebaseId": contributor_user.firebase_id,
"username": contributor_user.username,
Expand Down
4 changes: 4 additions & 0 deletions apps/contributor/firebase/pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pyfirebase_mapswipe import extended_models as firebase_ext_models
from pyfirebase_mapswipe import models as firebase_models

from apps.common.models import FirebasePushStatusEnum
from apps.contributor.models import (
ContributorUser,
ContributorUserGroupMembershipLogActionEnum,
Expand Down Expand Up @@ -41,6 +42,9 @@ def pull_users_from_firebase():
username=valid_user.username,
created_at=valid_user.created,
modified_at=valid_user.created,
# NOTE: Setting firebase_last_pushed so that we can send updates to firebase.
firebase_last_pushed=datetime.datetime.now(),
firebase_push_status=FirebasePushStatusEnum.SUCCESS,
)
users_to_pull.append(user)

Expand Down
21 changes: 14 additions & 7 deletions apps/contributor/graphql/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import strawberry
import strawberry_django
from django.db.models import QuerySet
from django.shortcuts import aget_object_or_404
from strawberry_django.pagination import OffsetPaginated
from strawberry_django.permissions import IsAuthenticated

from apps.contributor.models import ContributorTeam, ContributorUser, ContributorUserGroup, ContributorUserGroupMembership
from utils.graphql.inputs import FirebaseOrInternalIdInputType

from .filters import (
ContributorTeamFilter,
Expand All @@ -31,18 +31,25 @@ class Query:
filters=ContributorUserFilter,
)

contributor_user: ContributorUserType = strawberry_django.field()

contributor_user_group: ContributorUserGroupType = strawberry_django.field()

# Team
contributor_team: ContributorTeamType = strawberry_django.field()

@strawberry.field
async def contributor_user_by_firebase_id(self, firebase_id: strawberry.ID) -> ContributorUserType:
obj = await aget_object_or_404(ContributorUser, firebase_id=firebase_id)
async def contributor_user(
self,
user_id: FirebaseOrInternalIdInputType,
) -> ContributorUserType:
obj = await FirebaseOrInternalIdInputType.aget_object_or_404(ContributorUser, object_id=user_id)
return typing.cast("ContributorUserType", obj)

@strawberry.field
async def contributor_user_group(
self,
user_group_id: FirebaseOrInternalIdInputType,
) -> ContributorUserGroupType:
obj = await FirebaseOrInternalIdInputType.aget_object_or_404(ContributorUserGroup, object_id=user_group_id)
return typing.cast("ContributorUserGroupType", obj)

# --- Paginated
# --- ContributorUserGroup
@strawberry_django.offset_paginated(
Expand Down
4 changes: 2 additions & 2 deletions apps/contributor/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def pull_users_from_firebase_task():
logger.warning("Pull users from firebase is already running")
return

pull_users_from_firebase()
pull_users_from_firebase()


@shared_task
Expand All @@ -25,4 +25,4 @@ def pull_user_group_memberships_from_firebase_task():
logger.warning("Pull user group memberships from firebase is already running")
return

pull_user_group_memberships_from_firebase()
pull_user_group_memberships_from_firebase()
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,11 @@ def parse_project_name(
def parse_project_status(existing_project: existing_db_models.Project) -> ProjectStatusEnum:
assert existing_project.status is not None
return {
"inactive": ProjectStatusEnum.PAUSED,
"inactive": ProjectStatusEnum.DISCARDED,
"active": ProjectStatusEnum.PUBLISHED,
"private_active": ProjectStatusEnum.PUBLISHED,
"private_finished": ProjectStatusEnum.FINISHED,
"private_inactive": ProjectStatusEnum.DISCARDED,
"finished": ProjectStatusEnum.FINISHED,
"archived": ProjectStatusEnum.WITHDRAWN,
}[existing_project.status]
Expand Down Expand Up @@ -573,6 +574,8 @@ def create_project(
requesting_organization=get_organization_by_name(requesting_organization, bot_user),
created_by_id=get_user_by_contributor_user_firebase_id(existing_project.created_by, fallback=bot_user),
modified_by_id=get_user_by_contributor_user_firebase_id(existing_project.created_by, fallback=bot_user),
project_type_specifics=existing_project.project_type_specifics,
description=existing_project.project_details.strip() if existing_project.project_details else "",
)

# Progress metadata
Expand Down Expand Up @@ -986,7 +989,7 @@ def handle_project(self):
output_field=GeometryField(geography=True),
),
)
/ 100_000,
/ 1_000_000,
)

self.stdout.write("\n")
Expand Down
6 changes: 5 additions & 1 deletion apps/mapping/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@


class MappingSessionClientTypeEnum(models.IntegerChoices):
"""Enum representing client type used during a mapping session."""
"""Enum representing client type used during a mapping session.
https://github.com/react-native-device-info/react-native-device-info#getsystemname.
"""

UNKNOWN = 0, "Unknown"
MOBILE_ANDROID = 1, "Mobile (Android)"
Expand All @@ -23,6 +25,8 @@ def get_client_type(cls, value: str) -> "MappingSessionClientTypeEnum":
return {
"mobile-android": cls.MOBILE_ANDROID,
"mobile-ios": cls.MOBILE_IOS,
"mobile-iphone os": cls.MOBILE_IOS,
"mobile-ipados": cls.MOBILE_IOS,
"web": cls.WEB,
}.get(value, cls.UNKNOWN)

Expand Down
2 changes: 1 addition & 1 deletion apps/mapping/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ def pull_mapping_session_from_firebase():
logger.warning("Mapping Session Pull from Firebase already running")
return

pull_results_from_firebase()
pull_results_from_firebase()
6 changes: 3 additions & 3 deletions apps/project/exports/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,20 +188,20 @@ def _export_project_data(project: Project, tmp_directory: Path):
if project.progress >= 100:
project.progress_status = ProjectProgressStatusEnum.COMPLETED

if project.progress >= 90 and project.slack_progress_notifications < 90:
if project.progress >= 90 and (project.slack_progress_notifications or 0) < 90:
transaction.on_commit(
lambda: send_slack_message_for_project.delay(project_id=project.id, action="progress-change"),
)

if project.progress >= 100 and project.slack_progress_notifications < 100:
if project.progress >= 100 and (project.slack_progress_notifications or 0) < 100:
transaction.on_commit(
lambda: send_slack_message_for_project.delay(project_id=project.id, action="progress-change"),
)

if project.progress != previous_progress:
# FIXME(tnagorra): Do we only send updates for the 2 fields?
transaction.on_commit(
lambda: push_project_to_firebase.delay(project_id=project.id),
lambda: push_project_to_firebase.delay(project_id=project.id, only_stats=True),
)
project.update_firebase_push_status(FirebasePushStatusEnum.PENDING, False)

Expand Down
2 changes: 1 addition & 1 deletion apps/project/exports/mapping_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def generate_mapping_results(*, destination_filename: Path, project: Project) ->
logger.info("there are no results for this project %s", project.id)
else:
# TODO: Is this required?
df["timestamp"] = pd.to_datetime(df["timestamp"])
df["timestamp"] = pd.to_datetime(df["timestamp"], format="ISO8601")
df["day"] = df["timestamp"].apply(lambda x: datetime.datetime(year=x.year, month=x.month, day=x.day))
logger.info("added day attribute for results for %s", project.id)
return df
8 changes: 5 additions & 3 deletions apps/project/graphql/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ def _get_vector_tile_server_type(enum: VectorTileServerNameEnumWithoutCustom):
raster=[
_get_raster_tile_server_type(RasterTileServerNameEnum.BING),
_get_raster_tile_server_type(RasterTileServerNameEnum.MAPBOX),
_get_raster_tile_server_type(RasterTileServerNameEnum.MAXAR_STANDARD),
_get_raster_tile_server_type(RasterTileServerNameEnum.MAXAR_PREMIUM),
# NOTE: Disabled because it's not working for 2+ years
# _get_raster_tile_server_type(RasterTileServerNameEnum.MAXAR_STANDARD),
# _get_raster_tile_server_type(RasterTileServerNameEnum.MAXAR_PREMIUM),
_get_raster_tile_server_type(RasterTileServerNameEnum.ESRI),
_get_raster_tile_server_type(RasterTileServerNameEnum.ESRI_BETA),
],
Expand Down Expand Up @@ -154,8 +155,9 @@ def public_projects(
) -> QuerySet[Project]:
return Project.objects.filter(
status__in=[
Project.Status.FINISHED,
Project.Status.PUBLISHED,
Project.Status.PAUSED,
Project.Status.FINISHED,
],
).all()

Expand Down
3 changes: 2 additions & 1 deletion apps/project/graphql/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ class ProjectType(UserResourceTypeMixin, ProjectExportAssetTypeMixin, FirebasePu
processing_status: strawberry.auto
status_message: strawberry.auto

total_area: strawberry.auto
team: ContributorTeamType | None
is_private: strawberry.auto
required_results: strawberry.auto
Expand All @@ -181,6 +180,8 @@ class ProjectType(UserResourceTypeMixin, ProjectExportAssetTypeMixin, FirebasePu
number_of_results_for_progress: strawberry.auto
last_contribution_date: strawberry.auto

total_area: strawberry.auto = strawberry.field(deprecation_reason="Use AOI Geometry instead")

@strawberry_django.field(
description=str(Project._meta.get_field("progress").help_text), # type: ignore[reportAttributeAccessIssue]
)
Expand Down
17 changes: 17 additions & 0 deletions apps/project/migrations/0010_alter_projecttask_unique_together.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.2.5 on 2025-10-13 07:55

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('project', '0009_merge_20251008_1121'),
]

operations = [
migrations.AlterUniqueTogether(
name='projecttask',
unique_together=set(),
),
]
16 changes: 9 additions & 7 deletions apps/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ class Project(UserResource, FirebasePushResource):
help_text=gettext_lazy("Timestamp of the base slack message"),
)

slack_progress_notifications = models.PositiveIntegerField[int, int](
slack_progress_notifications = models.PositiveIntegerField[int | None, int | None](
null=True,
blank=True,
help_text=gettext_lazy("Stores the last progress checkpoint notified via Slack."),
Expand Down Expand Up @@ -692,12 +692,14 @@ class ProjectTask(FirebasePushResource):
id: int
task_group_id: int

class Meta: # type: ignore[reportIncompatibleVariableOverride]
unique_together = (
# FIXME(tnagorra): Should we use project instead of task_group here?
"task_group",
"firebase_id",
)
# FIXME: Quick fix involves removing uniqueness constraint
# As firebase_id for tasks are derived from user input,
# we should discuss if we need db level uniqueness
# class Meta:
# unique_together = (
# "task_group",
# "firebase_id",
# )

@typing.override
def __str__(self):
Expand Down
Loading
Loading