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

Backup development #331

Merged
merged 10 commits into from
Feb 4, 2025
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: 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