2
0
mirror of https://expo.survex.com/repositories/troggle/.git synced 2026-05-15 18:06:33 +01:00
Files
troggle/core/views/new_hole.py
T

314 lines
13 KiB
Python

import re
from django import forms
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from troggle.parsers.people import who_is_this
from core.position_utils import which_area # file-type import, not module type.
class NewHoleForm(forms.Form):
"""The validation on this form is a bit of a beast, sorry.
"""
discovery_date = forms.DateField(label="Trip date", widget=forms.DateInput(attrs={'type': 'date'}), required=True)
# Identification
cave_id = forms.CharField(label="New Cave Identifier for internal identifiers. Cannot easily be changed.",
widget=forms.TextInput(attrs={'placeholder':
'e.g. 2035-ZB-03 '}),
max_length=50, required=True)
tag_text = forms.CharField(label="Exact text on tag if placed, or seen in-place",
widget=forms.TextInput(attrs={'placeholder': 'e.g. 29 CUCC 35'}),
max_length=50, required=False)
# Naming
proposed_name = forms.CharField(label="Proposed Cave Name, can easily be changed later",
max_length=90, required=True)
# Discovery
discoverers = forms.CharField(label="Discoverers / Investigators today",
widget=forms.TextInput(attrs={'placeholder': 'e.g. Dour, Animal, Becka'}),
max_length=255, required=True)
surface_wallet = forms.CharField(label="Old wallet used to find the entrance (if any)",
widget=forms.TextInput(attrs={'placeholder': 'e.g. 2005 # 63'}),
max_length=100, required=False)
survey_wallet = forms.CharField(label="New Wallet for all this data (must match date of trip)",
widget=forms.TextInput(attrs={'placeholder': 'e.g. 2029 # 88'}),
required=True)
# GPS Data
gps_owner = forms.CharField(label="GPS: Whose device?",
widget=forms.TextInput(attrs={'placeholder': 'e.g. Becka'}),
max_length=100, required=True)
gps_lat = forms.FloatField(
label="GPS Latitude",
widget=forms.TextInput(attrs={'placeholder': 'e.g. 47.6964483 N'}), required=True
)
gps_long = forms.FloatField(
label="GPS Longitude",
widget=forms.TextInput(attrs={'placeholder': 'e.g. 13.8160500 E'}), required=True
)
gps_time = forms.TimeField(
label="Time of GPS reading",
widget=forms.TimeInput(attrs={'type': 'time'})
)
gps_screenshot = forms.BooleanField(label="Screenshot taken of GPSTest while GPS device in situ?", required=False)
gps_photo = forms.BooleanField(label="Photo taken of GPS device in situ with view of entrance?", required=False)
# Navigation
dist_to_ent = forms.FloatField(label="Distance from GPS to entrance (m)",
widget=forms.TextInput(attrs={'placeholder': 'e.g. 11.5'})
)
bear_to_ent = forms.FloatField(label="Compass bearing from GPS to entrance (degrees)",
widget=forms.TextInput(attrs={'placeholder': 'e.g. 217'})
)
# Status & Surveys
is_explored = forms.BooleanField(label="Exploration complete?", required=False)
ug_survey_done = forms.BooleanField(label="Survex data recorded?", required=False)
# Media: Entrance Photo (Replaced dropdown with checkboxes)
photo_ent_no = forms.BooleanField(label="Entrance photos ?", required=False)
photo_ent_who = forms.CharField(label="Who has photos of entrance, tag and GPS?", required=False)
who_are_you = forms.CharField(strip=True, label="Who are you ? (You do not need to have been on this trip)",
widget=forms.TextInput(
attrs={"size": 100, "placeholder": "You are entering data, who are you ? e.g. 'Becka' or 'Animal <mta@gasthof.expo>'",
"style": "vertical-align: text-top;"}
)
)
# VALIDATIONS
# Django's validation logic will automatically trigger these.
# Trigger: When form.is_valid() is called in your new_hole view, Django automatically looks
# for methods named clean_<fieldname>.
# Using self.cleaned_data.get('fieldname') is a safer habit than direct square brackets.
# If a field fails basic validation (e.g., someone typed letters into a numeric field),
# it won't exist in cleaned_data. .get() returns None
def _validate_caver_list(self, field_name, year, raw_data):
"""
Helper to split comma-separated names and validate them against who_is_this.
Returns a list of validated person_ids.
"""
if not raw_data:
self._add_caver_error(field_name, "No one", year)
return []
# Handle both single names and comma-separated lists
names = [n.strip() for n in raw_data.split(',') if n.strip()]
validated_ids = []
for name in names:
try:
person_id = who_is_this(year, name)
if person_id:
validated_ids.append(person_id)
else:
self._add_caver_error(field_name, name, year)
except (ValueError, IndexError) as e:
self._add_caver_error(field_name, name, year)
return validated_ids
def _add_caver_error(self, field_name, name, year):
"""Standardized HTML error reporter"""
error_html = mark_safe(
f"'{name}' is not a recognized caver for the year {year}. "
f"See <a href='/aliases/{year}'>aliases list</a>"
)
self.add_error(field_name, error_html)
def clean(self):
# Unlike clean_<fieldname>, which validates one field at a time, the general clean() method
# allows you to compare multiple fields against each other.
# Always call the parent clean() first to get cleaned_data dictionary
cleaned_data = super().clean()
trip_date = cleaned_data.get("discovery_date")
wallet_id = cleaned_data.get("survey_wallet")
cave_id = cleaned_data.get("cave_id")
if cave_id and trip_date:
try:
clean_cave = "".join(cave_id.split()) # removes whitespace
cave_year = int(clean_cave[:4])
trip_year = trip_date.year
if cave_year != trip_year:
self.add_error('cave_id',
f"Year mismatch: Cave identifier year ({cave_year}) does not match Trip date year ({trip_year}).")
except (ValueError, IndexError):
# Individual field cleaners (regex) will handle malformed wallet strings
pass
if wallet_id and trip_date:
try:
clean_wallet = "".join(wallet_id.split()) # should already be cleaned
wallet_year = int(clean_wallet[:4])
trip_year = trip_date.year
if wallet_year != trip_year:
self.add_error('survey_wallet',
f"Year mismatch: Wallet year ({wallet_year}) does not match Trip date year ({trip_year}).")
except (ValueError, IndexError):
# Individual field cleaners (regex) will handle malformed wallet strings
pass
# 2. Extract the year from survey_wallet (YYYY#NN)
# We only proceed if wallet_id passed its own validation earlier,
# which removes whitespace and checks the year
if wallet_id and "#" in wallet_id:
year = int(clean_wallet[:4])
intrepids = self._validate_caver_list(
'discoverers', year, cleaned_data.get("discoverers"))
if cleaned_data.get("photo_ent_no") or cleaned_data.get("gps_photo"):
cameramen = self._validate_caver_list(
'photo_ent_who', year, cleaned_data.get("photo_ent_who"))
gps_user = self._validate_caver_list(
'gps_owner', year, cleaned_data.get("gps_owner"))
# can now store these lists in cleaned_data if we want
# cleaned_data['intrepid_ids'] = intrepids
# cleaned_data['cameramen_ids'] = cameramen
# Entrance Photo Logic
photo_ent_on_camera = cleaned_data.get("photo_ent_on_camera")
gps_screenshot = cleaned_data.get("gps_screenshot")
photo_ent_who = cleaned_data.get("photo_ent_who")
if photo_ent_on_camera and not photo_ent_who:
# This attaches the error specifically to the 'who' field
self.add_error('photo_ent_who', "Please specify who has the entrance photo.")
if gps_screenshot and not photo_ent_who:
# This attaches the error specifically to the 'who' field
self.add_error('photo_ent_who', "Please specify who has the photo of the GPS device.")
# Tag Photo Logic (Applying the same logic for consistency)
photo_tag_on_camera = cleaned_data.get("photo_tag_on_camera")
photo_tag_who = cleaned_data.get("photo_tag_who")
if photo_tag_on_camera and not photo_tag_who:
self.add_error('photo_tag_who', "Please specify who has the tag photo.")
return cleaned_data
def clean_cave_id(self):
data = self.cleaned_data.get('cave_id')
# 1. Remove whitespace and force uppercase for consistency
clean_text = "".join(data.split()).upper()
# 2. Regex check: 4 digits, hyphen, 1-4 letters, hyphen, 2 digits
# \d{4} = year, [A-Z]{1,4} = 1 to 4 letters, \d{2} = number
match = re.match(r'^(\d{4})-([A-Z]{1,4})-(\d{2})$', clean_text)
if not match:
raise forms.ValidationError(
"Tag ID must be in format 'YYYY-A[AAA]-NN' (e.g., 2035-AA-99). "
"The middle can be 1 to 4 letters."
)
# 3. Validate year range from the first capture group
year = int(match.group(1))
if year < 1976 or year > 2099:
raise forms.ValidationError(
f"The year {year} must be between 1976 and 2099."
)
return clean_text
def clean_gps_lat(self):
data = self.cleaned_data['gps_lat']
min_val = 47.5
max_val = 47.9
if data < min_val or data > max_val:
raise ValidationError(
f"A valid latitude is between {min_val} and {max_val}")
return data
def clean_gps_long(self):
data = self.cleaned_data['gps_long']
placeholder_val = 13.8160500
min_val = 13.6
max_val = 14.0
if data < min_val or data > max_val:
raise ValidationError(
f"A valid longitude is between {min_val} and {max_val}")
return data
def clean_dist_to_ent(self):
dist = self.cleaned_data.get('dist_to_ent')
if dist is None:
raise forms.ValidationError("Distance cannot empty. Please enter a value between 0 and 20.")
else:
if dist < 0 or dist > 20:
raise forms.ValidationError("Distance must be between 0 and 20 meters.")
return dist
def clean_bear_to_ent(self):
bearing = self.cleaned_data.get('bear_to_ent')
if bearing is None:
raise forms.ValidationError("Bearing cannot empty. Please enter a value between 0 and 360.")
else:
if bearing < 0 or bearing > 360:
raise forms.ValidationError("Bearing must be between 0 and 360 degrees.")
return bearing
def clean_survey_wallet(self):
data = self.cleaned_data.get('survey_wallet')
if not data:
return data # Skip if empty (assumes required=False)
# 1. Remove all whitespace, catches tabs etc (\t), (\n)
clean_text = "".join(data.split())
# 2. Check general format using Regex (4 digits, #, 2 digits)
# ^\d{4}#\d{2}$ ensures the entire string matches exactly
if not re.match(r'^\d{4}#\d{2}$', clean_text):
raise forms.ValidationError(
"Survey Wallet must be in the format 'YYYY#NN' (e.g., 2019#99)."
)
# 3. Validate the year range (1976 to 2099)
year = int(clean_text[:4])
if year < 1976 or year > 2099:
raise forms.ValidationError(
f"The year {year} must be between 1976 and 2099."
)
# Return the whitespace-stripped version to be saved
return clean_text
from django.shortcuts import render, redirect
from django.contrib import messages
def new_hole(request):
areacode = None
if request.method == 'POST':
form = NewHoleForm(request.POST, request.FILES)
lat = float(form.data['gps_lat'])
long = float(form.data['gps_long'])
valid, area = which_area(lat, long)
if valid:
areacode = f"in {area}"
else:
areacode = "Not in 1623 or 1626"
if form.is_valid():
# Data processing to models and filesystem will go here
messages.success(request, "New cave data successfully received.")
return redirect('new_hole')
return redirect('some_success_url')
else:
form = NewHoleForm()
return render(request, 'new_hole.html', {'form': form, "areacode": areacode})