Skip to content

Commit

Permalink
Merge pull request #331 from inbo/backup-development
Browse files Browse the repository at this point in the history
Backup development
  • Loading branch information
mainlyIt authored Feb 4, 2025
2 parents 25d43ad + c172641 commit cb69f36
Show file tree
Hide file tree
Showing 12 changed files with 295 additions and 181 deletions.
1 change: 1 addition & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ echo "Load waarnemingen observation data via: python manage.py load_waarnemingen

# Start Gunicorn
echo "Starting Gunicorn..."
echo "Starting Gunicorn..."
gunicorn --workers 3 \
--worker-class gthread \
--threads 4 \
Expand Down
46 changes: 25 additions & 21 deletions src/components/ObservationDetailsComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
<div class="container mt-2">
<text class="text-muted text-uppercase small">
Melding <span id="identifier">{{ selectedObservation.id }}</span>
<template v-if="selectedObservation.wn_id">
(WAARNEMING
<a :href="'https://waarnemingen.be/observation/' + selectedObservation.wn_id" target="_blank">{{
selectedObservation.wn_id }}</a>)
<template v-if="sourceUrl">
(<a :href="sourceUrl" target="_blank">{{ selectedObservation.source }}</a>)
</template>
<template v-else>
{{ selectedObservation.source }}
</template>
</text>
<h3 class="mt-3 mb-3">
<span id="observation-datetime">{{ selectedObservation.observation_datetime ?
formatDate(selectedObservation.observation_datetime) : '' }}</span>,
<span id="municipality-name">{{ selectedObservation.municipality_name || '' }}</span>
</h3>

<div class="d-flex justify-content-between mb-3" id="reservation">
<button v-if="canReserve && isAuthorizedToReserve && !selectedObservation.reserved_by"
class="btn btn-sm btn-outline-primary" @click="reserveObservation">
Expand Down Expand Up @@ -209,37 +209,31 @@
<label class="col-4 col-form-label">Type</label>
<div class="col-8">
<p class="form-control-plaintext">
{{ selectedObservation.nest_type ? nestTypeEnum[selectedObservation.nest_type] :
'Geen' }}
{{ selectedObservation.nest_type ? nestTypeEnum[selectedObservation.nest_type] : 'Geen' }}
</p>
</div>
</div>
<div class="row mb-2">
<label class="col-4 col-form-label">Locatie</label>
<div class="col-8">
<p class="form-control-plaintext">
{{ selectedObservation.nest_location ?
nestLocationEnum[selectedObservation.nest_location] :
'Geen' }}
{{ selectedObservation.nest_location ? nestLocationEnum[selectedObservation.nest_location] : 'Geen' }}
</p>
</div>
</div>
<div class="row mb-2">
<label class="col-4 col-form-label">Grootte</label>
<div class="col-8">
<p class="form-control-plaintext">
{{ selectedObservation.nest_size ? nestSizeEnum[selectedObservation.nest_size] :
'Geen' }}
{{ selectedObservation.nest_size ? nestSizeEnum[selectedObservation.nest_size] : 'Geen' }}
</p>
</div>
</div>
<div class="row mb-2">
<label class="col-4 col-form-label">Hoogte</label>
<div class="col-8">
<p class="form-control-plaintext">
{{ selectedObservation.nest_height ?
nestHeightEnum[selectedObservation.nest_height] :
'Geen' }}
{{ selectedObservation.nest_height ? nestHeightEnum[selectedObservation.nest_height] : 'Geen' }}
</p>
</div>
</div>
Expand All @@ -259,8 +253,7 @@
<label class="col-4 col-form-label">Validatie</label>
<div class="col-8">
<p class="form-control-plaintext">
{{ validationStatusEnum[selectedObservation.wn_validation_status] || "Geen"
}}
{{ validationStatusEnum[selectedObservation.wn_validation_status] || "Geen" }}
</p>
</div>
</div>
Expand All @@ -277,8 +270,7 @@
<input v-if="selectedObservation.public_domain !== undefined"
v-model="editableObservation.public_domain" class="form-check-input"
type="checkbox" id="public-domain" :disabled="!canViewRestrictedFields" />
<label class="form-check-label" for="public-domain">Nest op publiek
terrein</label>
<label class="form-check-label" for="public-domain">Nest op publiek terrein</label>
</div>
</div>
</div>
Expand Down Expand Up @@ -400,6 +392,17 @@ export default {
const errorMessage = ref('');
const eradicationResultError = ref('');
const editableObservation = ref({});
const sourceUrl = computed(() => {
if (!selectedObservation.value) return '';
const { source, source_id, wn_id } = selectedObservation.value;
if ((source === 'Vespa-Watch' || source === 'iNaturalist') && source_id) {
return `https://www.inaturalist.org/observations/${source_id}`;
}
if (source === 'Waarnemingen.be' && wn_id) {
return `https://waarnemingen.be/observation/${wn_id}`;
}
return '';
});
const isAuthorizedToReserve = computed(() => {
if (vespaStore.isAdmin) return true;
Expand Down Expand Up @@ -476,7 +479,7 @@ export default {
const eradicationAfterCareEnum = {
"nest_volledig_verwijderd": "Nest volledig verwijderd",
"nest_gedeeltelijk verwijderd": "Nest gedeeltelijk verwijderd",
"nest_gedeeltelijk_verwijderd": "Nest gedeeltelijk verwijderd",
"nest_laten_hangen": "Nest laten hangen"
};
Expand Down Expand Up @@ -757,7 +760,8 @@ export default {
errorMessage,
eradicationResultError,
canViewRestrictedFields,
validationStatusEnum
validationStatusEnum,
sourceUrl
};
}
};
Expand Down
4 changes: 3 additions & 1 deletion src/stores/vespaStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const useVespaStore = defineStore('vespaStore', {
}

try {
const response = await ApiService.get(`/observations/dynamic-geojson?${filterQuery}`);
const response = await ApiService.get(`/observations/dynamic-geojson/?${filterQuery}`);
if (response.status === 200) {
this.observations = response.data.features;
this.setLastAppliedFilters();
Expand Down Expand Up @@ -505,6 +505,8 @@ export const useVespaStore = defineStore('vespaStore', {
return '#198754';
} else if (status === 'reserved') {
return '#ea792a';
} else if (status === 'untreatable') {
return '#198754';
}
return '#212529';
},
Expand Down
1 change: 0 additions & 1 deletion vespadb/observations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ class ObservationAdmin(gis_admin.GISModelAdmin):
"created_by",
"wn_modified_datetime",
"wn_created_datetime",
"species",
"wn_cluster_id",
"modified_by",
"modified_datetime",
Expand Down
2 changes: 1 addition & 1 deletion vespadb/observations/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

def invalidate_geojson_cache() -> None:
"""Invalidate the cache for all GeoJSON observations."""
keys = cache.keys("vespadb::/observations/dynamic-geojson*")
keys = cache.keys("vespadb::/observations/dynamic-geojson/*")
cache.delete_many(keys)


Expand Down
17 changes: 17 additions & 0 deletions vespadb/observations/migrations/0034_remove_observation_species.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.1.4 on 2025-01-30 19:13

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('observations', '0033_export'),
]

operations = [
migrations.RemoveField(
model_name='observation',
name='species',
),
]
25 changes: 17 additions & 8 deletions vespadb/observations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ class Observation(models.Model):
help_text="Validation status of the observation",
)

species = models.IntegerField(help_text="Species of the observed nest", blank=True, null=True)
nest_height = models.CharField(
max_length=50, choices=NestHeightEnum.choices, blank=True, null=True, help_text="Height of the nest"
)
Expand Down Expand Up @@ -374,21 +373,31 @@ def save(self, *args: Any, **kwargs: Any) -> None:
:param args: Variable length argument list.
:param kwargs: Arbitrary keyword arguments.
"""
# Only compute the municipality if the location is set and the municipality is not
if self.location:
# Ensure self.location is a Point instance
logger.info(f"Save method called for observation {self.id if self.id else 'new'}")

# Issue #290 - Automatically determine municipality, province and anb
if self.location and not (self.municipality and self.province and self.anb is not None):
if not isinstance(self.location, Point):
self.location = Point(self.location)

long = self.location.x
lat = self.location.y

self.anb = check_if_point_in_anb_area(long, lat)
municipality = get_municipality_from_coordinates(long, lat)
self.municipality = municipality
self.province = municipality.province if municipality else None
if self.anb is None:
self.anb = check_if_point_in_anb_area(long, lat)

if not self.municipality:
municipality = get_municipality_from_coordinates(long, lat)
self.municipality = municipality
if municipality and not self.province:
self.province = municipality.province

logger.info(f"Save method for observation {self.id if self.id else 'new'}: Setting municipality={self.municipality}, province={self.province}, anb={self.anb}")

super().save(*args, **kwargs)

class Meta:
ordering = ['id']

class Export(models.Model):
"""Model for tracking observation exports."""
Expand Down
98 changes: 20 additions & 78 deletions vespadb/observations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from django.conf import settings
from django.contrib.gis.geos import GEOSGeometry, Point
from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, ValidationError
from pytz import timezone
from rest_framework import serializers
from rest_framework.request import Request
Expand Down Expand Up @@ -127,70 +127,7 @@ class Meta:

model = Observation
fields = "__all__"
extra_kwargs = {
"id": {"read_only": True, "help_text": "Unique ID for the observation."},
"wn_id": {
"required": False,
"allow_null": True,
"help_text": "Unique ID for the observation in the source system.",
},
"created_datetime": {"help_text": "Datetime when the observation was created."},
"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."},
"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."},
"nest_size": {"help_text": "Size of the nest."},
"nest_location": {"help_text": "Location of the nest."},
"nest_type": {"help_text": "Type of the nest."},
"observer_phone_number": {"help_text": "Phone number of the observer."},
"observer_email": {"help_text": "Email of the observer."},
"observer_received_email": {"help_text": "Flag indicating if observer received email."},
"observer_name": {"help_text": "Name of the observer."},
"observation_datetime": {"help_text": "Datetime when the observation was made."},
"wn_cluster_id": {"required": False, "allow_null": True, "help_text": "Cluster ID of the observation."},
"admin_notes": {
"required": False,
"allow_blank": True,
"allow_null": True,
"help_text": "Admin notes for the observation.",
},
"wn_modified_datetime": {"help_text": "Datetime when the observation was modified in the source system."},
"wn_created_datetime": {"help_text": "Datetime when the observation was created in the source system."},
"visible": {"help_text": "Flag indicating if the observation is visible."},
"images": {
"required": False,
"allow_null": True,
"help_text": "List of images associated with the observation.",
},
"reserved_by": {"required": False, "allow_null": True, "help_text": "User who reserved the observation."},
"reserved_datetime": {"help_text": "Datetime when the observation was reserved."},
"eradication_date": {
"required": False,
"allow_null": True,
"help_text": "Date when the nest was eradicated.",
},
"eradicator_name": {"help_text": "Name of the person who eradicated the nest."},
"eradication_duration": {
"help_text": "Duration of the eradication in minutes",
"required": False,
"allow_null": True,
},
"eradication_persons": {"help_text": "Number of persons involved in the eradication."},
"eradication_result": {"help_text": "Result of the eradication."},
"eradication_product": {"help_text": "Product used for the eradication."},
"eradication_method": {"help_text": "Method used for the eradication."},
"eradication_aftercare": {"help_text": "Aftercare result of the eradication."},
"eradication_problems": {"help_text": "Problems encountered during the eradication."},
"eradication_notes": {"help_text": "Notes about the eradication."},
"municipality": {"help_text": "Municipality where the observation was made."},
"province": {"help_text": "Province where the observation was made."},
"anb": {"help_text": "Flag indicating if the observation is in ANB area."},
"public_domain": {"help_text": "Flag indicating if the observation is in the public domain."},
}


def get_municipality_name(self, obj: Observation) -> str | None:
"""Retrieve the name of the municipality associated with the observation, if any."""
return obj.municipality.name if obj.municipality else None
Expand Down Expand Up @@ -307,6 +244,17 @@ def validate_reserved_by(self, value: VespaUser) -> VespaUser:
def update(self, instance: Observation, validated_data: dict[Any, Any]) -> Observation: # noqa: C901
"""Update method to handle observation reservations."""
user = self.context["request"].user

# Check if someone is trying to update a nest reserved by another user
if instance.reserved_by and instance.reserved_by != user and not user.is_superuser:
raise serializers.ValidationError("You cannot edit an observation reserved by another user.")

# Only proceed if user has appropriate permissions
if not user.is_superuser:
user_municipality_ids = user.municipalities.values_list("id", flat=True)
if instance.municipality and instance.municipality.id not in user_municipality_ids:
raise PermissionDenied("You do not have permission to update nests in this municipality.")

allowed_admin_fields = [
"location",
"nest_height",
Expand Down Expand Up @@ -377,9 +325,6 @@ def update(self, instance: Observation, validated_data: dict[Any, Any]) -> Obser
raise serializers.ValidationError(f"Field(s) {field}' can not be updated by non-admin users.")

error_fields = []
# Check if 'species' is in validated_data and if it has changed
if "species" in validated_data and validated_data["species"] != instance.species:
error_fields.append("species")

# Check if 'wn_cluster_id' is in validated_data and if it has changed
if "wn_cluster_id" in validated_data and validated_data["wn_cluster_id"] != instance.wn_cluster_id:
Expand All @@ -390,21 +335,18 @@ def update(self, instance: Observation, validated_data: dict[Any, Any]) -> Obser
error_message = f"Following field(s) cannot be updated by any user: {', '.join(error_fields)}"
raise serializers.ValidationError(error_message)

# Conditionally set `reserved_by` and `reserved_datetime` for all users
if "reserved_by" in validated_data and instance.reserved_by is None:
validated_data["reserved_datetime"] = (
datetime.now(timezone("EST")) if validated_data["reserved_by"] else None
)
instance.reserved_by = user

# Prevent non-admin users from updating observations reserved by others
if not user.is_superuser and instance.reserved_by and instance.reserved_by != user:
raise serializers.ValidationError("You cannot edit an observation reserved by another user.")
# Conditionally set `reserved_by` and `reserved_datetime`
if "reserved_by" in validated_data:
if validated_data["reserved_by"] is not None:
validated_data["reserved_datetime"] = datetime.now(timezone("EST"))
else:
validated_data["reserved_datetime"] = None

for field in set(validated_data) - set(allowed_admin_fields):
validated_data.pop(field)
instance = super().update(instance, validated_data)
return instance

def to_internal_value(self, data: dict[str, Any]) -> dict[str, Any]:
"""Convert the incoming data to a Python native representation."""
logger.info("Raw input data: %s", data)
Expand Down
Loading

0 comments on commit cb69f36

Please sign in to comment.