Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development->main #255

Merged
merged 4 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion src/components/MapPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ export default {
try {
await vespaStore.fetchObservationDetails(properties.id);
if (vespaStore.selectedObservation && !vespaStore.selectedObservation.visible) {
console.error("Observation is not visible");
return;
}
vespaStore.isDetailsPaneOpen = true;
Expand Down
11 changes: 5 additions & 6 deletions src/components/NavbarComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
Vespa-Watch
</a>
<div class="d-flex align-items-center">
<!-- View Mode Toggle -->
<div class="btn-group me-2" role="group">
<router-link to="/map" class="btn btn-outline-dark" active-class="active"
aria-current="page">Map</router-link>
<!-- View Mode Toggle (Hidden on Medium Devices and below)-->
<div class="btn-group me-2 d-none d-md-inline-flex" role="group">
<router-link to="/map" class="btn btn-outline-dark" active-class="active" aria-current="page">Map</router-link>
<router-link to="/table" class="btn btn-outline-dark" active-class="active">Tabel</router-link>
</div>

<!-- Export Toggle (Hidden on Medium Devices and below) -->
<div class="btn-group me-2 d-md-inline-flex">
<div class="btn-group me-2 d-none d-md-inline-flex">
<button type="button" class="btn btn-outline-dark dropdown-toggle" data-bs-toggle="dropdown"
aria-expanded="false">
Export
Expand Down Expand Up @@ -44,7 +43,7 @@
</div>
</nav>
<ModalMessage :title="modalTitle" :message="modalMessage" :isVisible="isModalVisible"
@close="isModalVisible = false" />
@close="isModalVisible = false" />
</template>

<script>
Expand Down
120 changes: 70 additions & 50 deletions src/components/ObservationDetailsComponent.vue

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions src/components/TableViewPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,12 @@ export default {


// if (vespaStore.lastAppliedFilters === null || vespaStore.lastAppliedFilters === 'null') {
// console.log("set hier?")
// vespaStore.setLastAppliedFilters();
// }

// Avoid calling getObservations if data is already loaded with the same filters
if (vespaStore.table_observations.length === 0 || JSON.stringify(vespaStore.filters) !== JSON.stringify(vespaStore.lastAppliedFilters)) {



//vespaStore.setLastAppliedFilters();
vespaStore.getObservations(page.value, pageSize.value, sortBy.value, sortOrder.value);
vespaStore.getObservationsGeoJson();
Expand Down
8 changes: 0 additions & 8 deletions src/stores/vespaStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ export const useVespaStore = defineStore('vespaStore', {
}
} catch (error) {
console.error('Error fetching observation details:', error);
this.error = 'Het ophalen van observatiedetails is mislukt.';
}
},
formatToISO8601(datetime) {
Expand All @@ -293,17 +292,10 @@ export const useVespaStore = defineStore('vespaStore', {
return date.toISOString();
},
async updateObservation(observation) {
if (observation.observation_datetime) {
observation.observation_datetime = this.formatToISO8601(observation.observation_datetime);
}
if (observation.eradication_date) {
observation.eradication_date = this.formatDateWithEndOfDayTime(observation.eradication_date);
}
try {
const response = await ApiService.patch(`/observations/${observation.id}/`, observation);
if (response.status === 200) {
this.selectedObservation = response.data;

const colorByResult = this.getColorByStatus(response.data.eradication_result);
this.updateMarkerColor(observation.id, colorByResult, '#ea792a', 4, 'active-marker');
return response.data;
Expand Down
2 changes: 1 addition & 1 deletion vespadb/observations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class ObservationAdmin(gis_admin.GISModelAdmin):
actions = ["send_email_to_observers", "mark_as_eradicated", "mark_as_not_visible"]

readonly_fields = (
"wn_notes",
"notes",
"source",
"wn_id",
"wn_validation_status",
Expand Down
18 changes: 18 additions & 0 deletions vespadb/observations/migrations/0031_observation_source_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-11 18:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('observations', '0030_alter_observation_observer_phone_number'),
]

operations = [
migrations.AddField(
model_name='observation',
name='source_id',
field=models.IntegerField(blank=True, help_text='Original identifier when importing data', null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-11 18:13

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('observations', '0031_observation_source_id'),
]

operations = [
migrations.RenameField(
model_name='observation',
old_name='wn_notes',
new_name='notes',
),
]
3 changes: 2 additions & 1 deletion vespadb/observations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,9 @@ class Observation(models.Model):
modified_datetime = models.DateTimeField(auto_now=True, help_text="Datetime when the observation was last modified")
location = gis_models.PointField(help_text="Geographical location of the observation")
source = models.CharField(max_length=255, blank=True, null=True, help_text="Source of the observation")
source_id = models.IntegerField(blank=True, null=True, help_text="Original identifier when importing data")

wn_notes = models.TextField(blank=True, null=True, help_text="Notes about the observation")
notes = models.TextField(blank=True, null=True, help_text="Notes about the observation")
wn_admin_notes = models.TextField(blank=True, null=True, help_text="Admin notes about the observation")
wn_validation_status = models.CharField(
max_length=50,
Expand Down
38 changes: 19 additions & 19 deletions vespadb/observations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"observation_datetime",
"modified_by",
"created_by",
"province",
"eradication_date",
"municipality",
"province",
Expand All @@ -45,7 +44,7 @@
"municipality_name",
"modified_by_first_name",
"created_by_first_name",
"wn_notes",
"notes",
"eradication_result",
"wn_id",
"wn_validation_status",
Expand Down Expand Up @@ -139,7 +138,7 @@ class Meta:
"modified_datetime": {"help_text": "Datetime when the observation was last modified."},
"location": {"help_text": "Geographical location of the observation as a point."},
"source": {"help_text": "Source of the observation."},
"wn_notes": {"help_text": "Notes about the observation."},
"notes": {"help_text": "Notes about the observation."},
"wn_admin_notes": {"write_only": True},
"wn_validation_status": {"help_text": "Validation status of the observation."},
"nest_height": {"help_text": "Height of the nest."},
Expand Down Expand Up @@ -262,23 +261,24 @@ def to_representation(self, instance: Observation) -> dict[str, Any]: # noqa: C

if request and request.user.is_authenticated:
user: VespaUser = request.user
permission_level = user.get_permission_level()
user_municipality_ids = user.municipalities.values_list("id", flat=True)
is_inside_user_municipality = (
instance.municipality and instance.municipality.id in user_municipality_ids
)

# Voor gebruikers zonder toegang tot specifieke gemeenten
if permission_level == "logged_in_without_municipality":
return {field: data[field] for field in public_read_fields if field in data}

if not request.user.is_superuser:
# Non-admins should not see admin-specific fields
admin_fields = set(admin_or_special_permission_fields)
for field in admin_fields:
data.pop(field, None)

is_inside_user = instance.municipality and instance.municipality.id in user_municipality_ids
if not is_inside_user:
# Do not show reserved_by for users outside the municipality and not admins
data.pop("reserved_by", None)
return {
field: data[field]
for field in set(user_read_fields + conditional_fields + admin_or_special_permission_fields)
if field in data
}
# Voor gebruikers met toegang tot specifieke gemeenten, extra gegevens tonen indien binnen hun gemeenten
if is_inside_user_municipality or request.user.is_superuser:
return {field: data[field] for field in user_read_fields if field in data}

# Voor observaties buiten de gemeenten van de gebruiker, beperk tot publieke velden
return {field: data[field] for field in public_read_fields if field in data}

# Voor niet-ingelogde gebruikers, retourneer enkel de publieke velden
return {field: data[field] for field in public_read_fields if field in data}

def validate_reserved_by(self, value: VespaUser) -> VespaUser:
Expand Down Expand Up @@ -360,7 +360,7 @@ def update(self, instance: Observation, validated_data: dict[Any, Any]) -> Obser
if eradication_result == EradicationResultEnum.SUCCESSFUL:
validated_data["reserved_datetime"] = None
validated_data["reserved_by"] = None
validated_data["eradication_date"] = datetime.now(timezone("EST")).date()
#validated_data["eradication_date"] = datetime.now(timezone("EST")).date()

if not user.is_superuser:
# Non-admins cannot update admin-specific fields, so remove them
Expand Down
105 changes: 67 additions & 38 deletions vespadb/observations/tasks/observation_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,42 +26,75 @@

logger = logging.getLogger("vespadb.observations.tasks")

mapping_dict: dict[int, dict[str, str]] = {
329: {
"Hoger dan 4 meter": "hoger_dan_4_meter",
"Higher than 4 meters": "hoger_dan_4_meter",
"Lager dan 4 meter": "lager_dan_4_meter",
"Lower than 4 meters": "lager_dan_4_meter",
},
330: {
"Groter dan 25 cm": "groter_dan_25_cm",
"Kleiner dan 25 cm": "kleiner_dan_25_cm",
"Larger than 25cm": "groter_dan_25_cm",
"Smaller than 25cm": "kleiner_dan_25_cm",
},
331 : {
"Binnen, in gebouw of constructie": "binnen_in_gebouw_of_constructie",
"Buiten, maar overdekt door constructie": "buiten_maar_overdekt_door_constructie",
"Buiten, natuurlijk overdekt": "buiten_natuurlijk_overdekt",
"Buiten, onbedekt in boom of struik": "buiten_onbedekt_in_boom_of_struik",
"Buiten, onbedekt op gebouw": "buiten_onbedekt_op_gebouw",
"Inside, in a building or construction": "binnen_in_gebouw_of_constructie",
"Outside, but covered by construction": "buiten_maar_overdekt_door_constructie",
"Outside, natural cover": "buiten_natuurlijk_overdekt",
"Outside, uncovered in a tree or bush": "buiten_onbedekt_in_boom_of_struik",
"Outside, uncovered on building": "buiten_onbedekt_op_gebouw",
}
}

ENUMS_MAPPING: dict[str, type[TextChoices]] = {
"Nesthoogte": NestHeightEnum,
"Nestgrootte": NestSizeEnum,
"Nestplaats": NestLocationEnum,
"Nesttype": NestTypeEnum,
"Resultaat": EradicationResultEnum,
"Problemen": EradicationProblemsEnum,
"Methode": EradicationMethodEnum,
"Nest height": NestHeightEnum,
"Nest size": NestSizeEnum,
"Nest location": NestLocationEnum,
"Nest type": NestTypeEnum,
"Result": EradicationResultEnum,
"Problems": EradicationProblemsEnum,
"Method": EradicationMethodEnum,
"Product": EradicationProductEnum,
}
ENUM_FIELD_MAPPING: dict[str, str] = {
"Nesthoogte": "nest_height",
"Nestgrootte": "nest_size",
"Nestplaats": "nest_location",
"Nesttype": "nest_type",
"Resultaat": "eradication_result",
"Problemen": "eradication_problems",
"Methode": "eradication_method",
"Product": "eradication_product",
ENUM_FIELD_MAPPING: dict[int, str] = {
329: "nest_height",
330: "nest_size",
331: "nest_location",
}
# Literal mapping functions
def map_nest_height_attribute_to_enum(value: str) -> Any | None:
"""Maps Nest height values to enums based on literal mapping."""
return mapping_dict[329].get(value.strip())

def map_nest_size_attribute_to_enum(value: str) -> Any | None:
"""Maps Nest size values to enums based on literal mapping."""
return mapping_dict[330].get(value.strip())

def map_attribute_to_enum(value: str, enum: type[TextChoices]) -> TextChoices | None:
"""
Map a single attribute value to an enum using close match.
def map_nest_location_attribute_to_enum(value: str) -> str | None:
"""Maps Nest location values to enums based on literal mapping."""
return mapping_dict[331].get(value.strip())

:param value: The value from the API that needs to be mapped to an enum.
:param enum: The enum type that the value is expected to map to.
:return: The corresponding enum value if a match is found, otherwise None.
def map_attribute_to_enum(attribute_id: int, value: str) -> str | None:
"""
enum_dict = {e.value: e for e in enum}
closest_match = get_close_matches(value, enum_dict.keys(), n=1, cutoff=0.6)
return enum_dict.get(closest_match[0]) if closest_match else None

Maps a single attribute value to an enum using literal mapping functions.
"""
if attribute_id == 329:
return map_nest_height_attribute_to_enum(value)
elif attribute_id == 330:
return map_nest_size_attribute_to_enum(value)
elif attribute_id == 331:
return map_nest_location_attribute_to_enum(value)
else:
return None

def map_attributes_to_enums(api_attributes: list[dict[str, str]]) -> dict[str, TextChoices]:
def map_attributes_to_enums(api_attributes: list[dict[str, Any]]) -> dict[str, str]:
"""
Map API attributes to model enums based on configured mappings.

Expand All @@ -70,17 +103,17 @@ def map_attributes_to_enums(api_attributes: list[dict[str, str]]) -> dict[str, T
"""
mapped_values = {}
for attribute in api_attributes:
attribute_id = int(attribute.get("attribute", 0))
attr_name = attribute.get("name")
value = str(attribute.get("value"))
if attr_name in ENUMS_MAPPING:
mapped_enum = map_attribute_to_enum(value, ENUMS_MAPPING[attr_name])
if attribute_id in mapping_dict:
mapped_enum = map_attribute_to_enum(attribute_id, value)
if mapped_enum:
mapped_values[ENUM_FIELD_MAPPING[attr_name]] = mapped_enum
mapped_values[ENUM_FIELD_MAPPING[attribute_id]] = mapped_enum
else:
logger.warning(f"No enum match found for {attr_name}: {value}")
logger.debug(f"No enum match found for {attr_name}: {value}")
return mapped_values


def map_validation_status_to_enum(validation_status: str) -> ValidationStatusEnum | None:
"""
Map a single validation status to an enum.
Expand Down Expand Up @@ -165,6 +198,7 @@ def map_external_data_to_observation_model(external_data: dict[str, Any]) -> dic
cluster_id = None
if nest:
cluster_id = nest.get("id")

mapped_data = {
"wn_id": external_data["id"],
"location": location,
Expand All @@ -182,7 +216,6 @@ def map_external_data_to_observation_model(external_data: dict[str, Any]) -> dic
**mapped_enums,
}

# Additional user data
user_data = external_data.get("user", {})
if user_data:
mapped_data.update({
Expand All @@ -191,10 +224,8 @@ def map_external_data_to_observation_model(external_data: dict[str, Any]) -> dic
"observer_name": user_data.get("name"),
})

# Eradication specifics
eradication_flagged = False

# Check for eradication keywords in notes
if (
"notes" in external_data
and external_data["notes"]
Expand All @@ -203,9 +234,8 @@ def map_external_data_to_observation_model(external_data: dict[str, Any]) -> dic
):
eradication_flagged = True

# Check for "BESTREDEN" in 'Remark (Asian hornet)' attribute
for attribute in external_data.get("attributes", []):
if attribute.get("name") == "Remark (Asian hornet)" and "BESTREDEN" in attribute.get("value", "").upper():
if attribute.get("attribute") == 369 and "BESTREDEN" in attribute.get("value", "").upper():
eradication_flagged = True
break

Expand All @@ -215,7 +245,6 @@ def map_external_data_to_observation_model(external_data: dict[str, Any]) -> dic

return mapped_data


def check_existing_eradication_date(wn_id: str) -> bool:
"""
Check if the eradication_date is already set for the given wn_id.
Expand Down
Loading