Skip to content
Open
11 changes: 9 additions & 2 deletions apps/mapping/firebase/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,14 +248,16 @@ def results_complete(
{fd_name(MappingSessionResult.session)},
{fd_name(MappingSessionResult.project_task)},
-- Value
{fd_name(MappingSessionResult.result)}
{fd_name(MappingSessionResult.result)},
{fd_name(MappingSessionResult.reference)}
) (
SELECT
-- Ref
MS.{fd_name(MappingSession.id)}, -- mapping_session_id
RT.{fd_name(MappingSessionResultTemp.task_id)}, -- task_id
-- Value
RT.{fd_name(MappingSessionResultTemp.result)} -- result [TODO: ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(RT.result), 3857), 4326)]
RT.{fd_name(MappingSessionResultTemp.result)}, -- result [TODO: ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(RT.result), 3857), 4326)]
RT.{fd_name(MappingSessionResultTemp.reference)}
FROM {tb_name(MappingSessionResultTemp)} RT
LEFT JOIN {tb_name(MappingSession)} MS ON
MS.{fd_name(MappingSession.project_task_group)} = RT.{fd_name(MappingSessionResultTemp.group_id)} AND
Expand Down Expand Up @@ -489,6 +491,8 @@ def results_to_temp_table(
continue

# Collect results for each tasks
reference_map = mapping_session_data.get("reference", {})

for task_firebase_id, result in session_results_iterator:
if result is None:
# TODO: Do we treat it as 0?
Expand All @@ -499,6 +503,8 @@ def results_to_temp_table(
# if result_type == "geometry":
# result = geojson.dumps(geojson.GeometryCollection(result))

reference_for_task = reference_map.get(task_firebase_id)

bulk_create_manager.add(
MappingSessionResultTemp(
project_firebase_id=project.firebase_id,
Expand All @@ -508,6 +514,7 @@ def results_to_temp_table(
start_time=start_time,
end_time=end_time,
result=result,
reference=reference_for_task,
app_version=app_version,
client_type=client_type,
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.5 on 2025-12-08 14:47

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('mapping', '0003_alter_mappingsession_app_version'),
]

operations = [
migrations.AddField(
model_name='mappingsessionresult',
name='reference',
field=models.JSONField(blank=True, null=True),
),
migrations.AddField(
model_name='mappingsessionresulttemp',
name='reference',
field=models.JSONField(blank=True, null=True),
),
]
2 changes: 2 additions & 0 deletions apps/mapping/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class MappingSessionResult(models.Model):
session = models.ForeignKey[MappingSession, MappingSession](MappingSession, on_delete=models.PROTECT)
project_task = models.ForeignKey[ProjectTask, ProjectTask](ProjectTask, on_delete=models.PROTECT)
result = models.PositiveSmallIntegerField[int, int]()
reference = models.JSONField(null=True, blank=True)

# TODO(thenav56): Add constraint to make sure we have non-duplicate row with task_id, .session.user_id

Expand Down Expand Up @@ -155,6 +156,7 @@ class MappingSessionResultTemp(models.Model):
start_time = models.DateTimeField[datetime.datetime, datetime.datetime]()
end_time = models.DateTimeField[datetime.datetime, datetime.datetime]()
result = models.PositiveSmallIntegerField[int, int]()
reference = models.JSONField(null=True, blank=True)
app_version = models.CharField[str, str](max_length=255)
client_type: int = IntegerChoicesField(choices_enum=MappingSessionClientTypeEnum) # type: ignore[reportAssignmentType]

Expand Down
9 changes: 5 additions & 4 deletions apps/project/custom_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,12 @@ def get_fallback_custom_options_for_export(project_type: ProjectTypeEnum) -> lis
project_type == ProjectTypeEnum.FIND
or project_type == ProjectTypeEnum.COMPARE
or project_type == ProjectTypeEnum.COMPLETENESS
or project_type == ProjectTypeEnum.CONFLATION
):
return [
0, # No
1, # Yes
2, # Maybe
3, # Bad Imagery
0, # No / Conflation: No | OSM feature is more accurate
1, # Yes / Conflation: Yes | Other feature is more accurate
2, # Maybe / Conflation: Not sure | Neither is accurate
3, # Bad Imagery / Conflation: Skipped because of multiple OSM features intersecting
]
typing.assert_never(project_type)
5 changes: 3 additions & 2 deletions apps/project/exports/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from apps.user.models import User
from main.config import Config
from main.logging import log_extra
from project_types.conflation.project import ConflationProjectProperty
from project_types.store import get_project_type_handler
from project_types.tile_map_service.compare.project import CompareProjectProperty
from project_types.tile_map_service.completeness.project import CompletenessProjectProperty
Expand Down Expand Up @@ -87,11 +88,11 @@ def _export_project_data(project: Project, tmp_directory: Path):

custom_options_raw = []

# NOTE: We do not have custom options for Compare, Completeness and Find projects
# NOTE: We do not have custom options for Compare, Completeness, Conflation and Find projects
if not isinstance(
project_type_handler.project_type_specifics,
# NOTE: Using negate test to throw type error if new project type is added
(CompareProjectProperty | CompletenessProjectProperty | FindProjectProperty),
(CompareProjectProperty | CompletenessProjectProperty | FindProjectProperty | ConflationProjectProperty),
):
custom_options_raw = [
{"value": custom_option.value}
Expand Down
1 change: 1 addition & 0 deletions apps/project/exports/mapping_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def generate_mapping_results(*, destination_filename: Path, project: Project) ->
{MappingSessionClientTypeEnum.get_client_type_label_sql(f"MS.{fd_name(MappingSession.client_type)}")}
) as client_type,
MSR.{fd_name(MappingSessionResult.result)} as result,
MSR.{fd_name(MappingSessionResult.reference)}::text as reference,
-- the username for users which login to MapSwipe with their
-- OSM account is not defined or ''.
-- We capture this here as it will cause problems
Expand Down
27 changes: 27 additions & 0 deletions apps/project/exports/mapping_results_aggregate/task.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import typing
from pathlib import Path

Expand Down Expand Up @@ -87,6 +88,30 @@ def _get_custom_options(custom_options: CustomOptionType):
}


def _add_reference_to_agg_results(
results_df: pd.DataFrame,
agg_results_df: pd.DataFrame,
) -> pd.DataFrame:
"""Adds a 'reference' column to agg_results_df if it exists in results_df.
For each task_id, all unique non-empty refs are collected into a list.
If no refs exist for a task, the corresponding value is empty string.
If results_df has no 'ref' column, agg_results_df is returned unchanged.
"""
if "reference" not in results_df.columns:
return agg_results_df

refs_per_task = (
results_df.groupby("task_id")["reference"]
.apply(lambda x: list({r for r in x if pd.notna(r) and r not in ({}, "")}))
.apply(lambda lst: json.dumps([json.loads(r) for r in lst]) if lst else "")
)

if refs_per_task.apply(lambda x: len(x) > 0).any():
agg_results_df["reference"] = agg_results_df["task_id"].map(refs_per_task).fillna("")

return agg_results_df


def generate_mapping_results_aggregate_by_task(
*,
destination_filename: Path,
Expand Down Expand Up @@ -153,6 +178,8 @@ def generate_mapping_results_aggregate_by_task(
right_on="task_id",
)

agg_results_df = _add_reference_to_agg_results(results_df, agg_results_df)

agg_results_df.to_csv(destination_filename, index_label="idx")

return agg_results_df
2 changes: 2 additions & 0 deletions apps/project/graphql/inputs/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from .project_types.compare import CompareProjectPropertyInput
from .project_types.completeness import CompletenessProjectPropertyInput
from .project_types.conflation import ConflationProjectPropertyInput
from .project_types.find import FindProjectPropertyInput
from .project_types.street import StreetProjectPropertyInput
from .project_types.validate import ValidateProjectPropertyInput
Expand Down Expand Up @@ -53,6 +54,7 @@ class ProjectTypeSpecificInput:
validate: ValidateProjectPropertyInput | None = strawberry.UNSET
validate_image: ValidateImageProjectPropertyInput | None = strawberry.UNSET
street: StreetProjectPropertyInput | None = strawberry.UNSET
conflation: ConflationProjectPropertyInput | None = strawberry.UNSET


# NOTE: Make sure this matches with the serializers ../serializers.py
Expand Down
11 changes: 11 additions & 0 deletions apps/project/graphql/inputs/project_types/conflation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import strawberry

from project_types.conflation import project as conflation_project


@strawberry.experimental.pydantic.input(model=conflation_project.ConflationObjectSourceConfig, all_fields=True)
class ConflationObjectSourceConfigInput: ...


@strawberry.experimental.pydantic.input(model=conflation_project.ConflationProjectProperty, all_fields=True)
class ConflationProjectPropertyInput: ...
14 changes: 14 additions & 0 deletions apps/project/graphql/types/project_types/conflation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import strawberry

from project_types.conflation import project as conflation_project


@strawberry.experimental.pydantic.type(model=conflation_project.ConflationObjectSourceConfig, all_fields=True)
class ConflationObjectSourceConfig: ...


@strawberry.experimental.pydantic.type(model=conflation_project.ConflationProjectProperty, all_fields=True)
class ConflationProjectPropertyType: ...


DEFAULT_TEST_RESPONSE_ERROR_MESSAGE: str = "Something unexpected has occurred. Please contact an admin to fix this issue."
8 changes: 8 additions & 0 deletions apps/project/graphql/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from apps.tutorial.graphql.types.types import TutorialType
from main.config import Config
from main.graphql.context import Info
from project_types.conflation import project as conflation_project
from project_types.street import project as street_project
from project_types.tile_map_service.compare import project as compare_project
from project_types.tile_map_service.completeness import project as completeness_project
Expand All @@ -31,6 +32,7 @@

from .project_types.compare import CompareProjectPropertyType
from .project_types.completeness import CompletenessProjectPropertyType
from .project_types.conflation import ConflationProjectPropertyType
from .project_types.find import FindProjectPropertyType
from .project_types.street import StreetProjectPropertyType
from .project_types.validate import ValidateProjectPropertyType
Expand Down Expand Up @@ -219,6 +221,7 @@ async def project_type_specifics(
| ValidateImageProjectPropertyType
| CompletenessProjectPropertyType
| StreetProjectPropertyType
| ConflationProjectPropertyType
| None
):
data = project.project_type_specifics
Expand All @@ -245,4 +248,9 @@ async def project_type_specifics(
"StreetProjectPropertyType",
street_project.StreetProjectProperty.model_validate(data),
)
if project.project_type_enum == Project.Type.CONFLATION:
return typing.cast(
"ConflationProjectPropertyType",
conflation_project.ConflationProjectProperty.model_validate(data),
)
typing.assert_never(project.project_type_enum)
19 changes: 19 additions & 0 deletions apps/project/migrations/0011_alter_project_project_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.2.5 on 2025-12-08 14:47

import django_choices_field.fields
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('project', '0010_alter_projecttask_unique_together'),
]

operations = [
migrations.AlterField(
model_name='project',
name='project_type',
field=django_choices_field.fields.IntegerChoicesField(choices=[(1, 'Find Features'), (2, 'Validate Footprints'), (10, 'Assess Images'), (3, 'Compare Dates'), (4, 'Check Completeness'), (7, 'View Streets'), (8, 'Conflate Features')]),
),
]
5 changes: 5 additions & 0 deletions apps/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ class ProjectTypeEnum(models.IntegerChoices):
STREET = 7, "View Streets"
""" Street project type. """

CONFLATION = 8, "Conflate Features"
""" Conflation project type. """

# TODO(thenav56): Confirm if we have more/less

@classmethod
Expand All @@ -136,6 +139,8 @@ def to_firebase(self) -> firebase_models.FbEnumProjectType:
return firebase_models.FbEnumProjectType.VALIDATE_IMAGE
case ProjectTypeEnum.STREET:
return firebase_models.FbEnumProjectType.STREET
case ProjectTypeEnum.CONFLATION:
return firebase_models.FbEnumProjectType.CONFLATION


# TODO(tnagorra): Reset the values later
Expand Down
2 changes: 2 additions & 0 deletions apps/project/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ def _validate_group_size(self, attrs: dict[str, typing.Any]):
group_size = 80
case Project.Type.STREET:
group_size = 25
case Project.Type.CONFLATION:
group_size = 25

attrs["group_size"] = group_size

Expand Down
2 changes: 2 additions & 0 deletions apps/project/slack_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def format_project_type(project_type: ProjectTypeEnum):
Project.Type.STREET: ":street:",
Project.Type.COMPLETENESS: ":completeness:",
Project.Type.VALIDATE_IMAGE: ":validate_image:",
# TODO: add custom :conflation: icon in slack
Project.Type.CONFLATION: ":construction:",
}
# FIXME: better way to concatenate this
return f"{label} {type_to_icon.get(project_type, '')}".strip()
Expand Down
Loading
Loading