22Forms for the web GUI
33"""
44
5+ from decimal import Decimal
6+
57from django .contrib .auth import get_user_model
68from django .contrib .auth .forms import UserCreationForm
7- from django .forms import CharField , ModelForm , TextInput
9+ from django .core .exceptions import ValidationError
10+ from django .forms import ChoiceField , DecimalField , ModelForm , Select , TextInput
811
912from .models import LeewaySimulation , Webhook
10- from .utils import normalize_dms2dec
13+
14+
15+ def _assemble_decimal (deg , minutes , sec ):
16+ """
17+ Assemble a decimal degree value from components.
18+
19+ If *deg* has a fractional part, *minutes* and *sec* are ignored.
20+ If *minutes* has a fractional part, *sec* is ignored.
21+
22+ :param deg: Absolute degrees (non-negative Decimal)
23+ :type deg: ~decimal.Decimal
24+ :param minutes: Minutes component (Decimal)
25+ :type minutes: ~decimal.Decimal
26+ :param sec: Seconds component (Decimal)
27+ :type sec: ~decimal.Decimal
28+ :rtype: ~decimal.Decimal
29+ """
30+ if deg % 1 != 0 :
31+ return deg
32+ if minutes % 1 != 0 :
33+ return deg + minutes / 60
34+ return deg + minutes / 60 + sec / 3600
1135
1236
1337class LeewaySimulationForm (ModelForm ):
1438 """
1539 Add a form for simulations with some tweaks of the default.
40+
41+ Latitude and longitude are each split into four sub-fields
42+ (degrees, minutes, seconds, direction) and assembled into a single
43+ decimal value in the ``clean_latitude`` / ``clean_longitude`` methods.
1644 """
1745
18- # Override coordinates fields to allow text input instead of float numbers
19- longitude = CharField (
20- help_text = "(E/W) Input in decimal and degrees minutes seconds supported." ,
21- widget = TextInput (attrs = {"placeholder" : "12° 26' 42.684\" " }),
46+ #: Direction choices for latitude
47+ LAT_DIR_CHOICES = [("N" , "N" ), ("S" , "S" )]
48+ #: Direction choices for longitude
49+ LON_DIR_CHOICES = [("E" , "E" ), ("W" , "W" )]
50+
51+ # --- Latitude sub-fields ---
52+ latitude_deg = DecimalField (
53+ label = "Latitude" ,
54+ min_value = Decimal ("0" ),
55+ max_value = Decimal ("90" ),
56+ widget = TextInput (attrs = {"placeholder" : "34" , "class" : "coord-deg coord-lat" }),
57+ )
58+ latitude_min = DecimalField (
59+ label = "" ,
60+ required = False ,
61+ min_value = Decimal ("0" ),
62+ max_value = Decimal ("59.9999999" ),
63+ initial = Decimal ("0" ),
64+ widget = TextInput (attrs = {"placeholder" : "47" , "class" : "coord-min coord-lat" }),
65+ )
66+ latitude_sec = DecimalField (
67+ label = "" ,
68+ required = False ,
69+ min_value = Decimal ("0" ),
70+ max_value = Decimal ("59.9999999" ),
71+ initial = Decimal ("0" ),
72+ widget = TextInput (attrs = {"placeholder" : "49.1" , "class" : "coord-sec coord-lat" }),
73+ )
74+ latitude_dir = ChoiceField (
75+ label = "" ,
76+ choices = LAT_DIR_CHOICES ,
77+ widget = Select (attrs = {"class" : "coord-dir coord-lat" }),
78+ )
79+
80+ # --- Longitude sub-fields ---
81+ longitude_deg = DecimalField (
82+ label = "Longitude" ,
83+ min_value = Decimal ("0" ),
84+ max_value = Decimal ("180" ),
85+ widget = TextInput (attrs = {"placeholder" : "12" , "class" : "coord-deg coord-lon" }),
86+ )
87+ longitude_min = DecimalField (
88+ label = "" ,
89+ required = False ,
90+ min_value = Decimal ("0" ),
91+ max_value = Decimal ("59.9999999" ),
92+ initial = Decimal ("0" ),
93+ widget = TextInput (attrs = {"placeholder" : "26" , "class" : "coord-min coord-lon" }),
2294 )
23- latitude = CharField (
24- help_text = "(N/S) Input in decimal and degrees minutes seconds supported." ,
25- widget = TextInput (attrs = {"placeholder" : "34° 47' 49.1166\" " }),
95+ longitude_sec = DecimalField (
96+ label = "" ,
97+ required = False ,
98+ min_value = Decimal ("0" ),
99+ max_value = Decimal ("59.9999999" ),
100+ initial = Decimal ("0" ),
101+ widget = TextInput (attrs = {"placeholder" : "42.7" , "class" : "coord-sec coord-lon" }),
102+ )
103+ longitude_dir = ChoiceField (
104+ label = "" ,
105+ choices = LON_DIR_CHOICES ,
106+ widget = Select (attrs = {"class" : "coord-dir coord-lon" }),
26107 )
27108
28109 class Meta :
@@ -49,17 +130,49 @@ class Meta:
49130 "start_time" : "All times are UTC. Only simulations +/- 5 days from now are possible." ,
50131 }
51132
52- def clean_longitude (self ):
133+ def _clean_coordinate (self , prefix , max_deg ):
53134 """
54- Convert longitude DMS to decimal
135+ Assemble and validate a coordinate from its sub-fields.
136+
137+ :param prefix: Field name prefix, either ``"latitude"`` or ``"longitude"``
138+ :type prefix: str
139+ :param max_deg: Maximum allowed absolute degree value (90 or 180)
140+ :type max_deg: int
141+ :rtype: float
55142 """
56- return normalize_dms2dec (self .cleaned_data .get ("longitude" , "" ))
143+ data = self .cleaned_data
144+ deg = data .get (f"{ prefix } _deg" )
145+ minutes = data .get (f"{ prefix } _min" ) or Decimal ("0" )
146+ sec = data .get (f"{ prefix } _sec" ) or Decimal ("0" )
147+ direction = data .get (f"{ prefix } _dir" , "N" )
148+
149+ if deg is None :
150+ # deg field validation already raised an error; nothing more to do here
151+ raise ValidationError ("Degrees are required." )
152+
153+ decimal_value = _assemble_decimal (deg , minutes , sec )
154+
155+ if decimal_value > max_deg :
156+ raise ValidationError (
157+ f"Value { decimal_value } exceeds the maximum of { max_deg } °."
158+ )
159+
160+ if direction in ("S" , "W" ):
161+ decimal_value = - decimal_value
162+
163+ return float (decimal_value )
57164
58165 def clean_latitude (self ):
59166 """
60- Convert latitude DMS to decimal
167+ Assemble latitude decimal from sub-fields.
168+ """
169+ return self ._clean_coordinate ("latitude" , 90 )
170+
171+ def clean_longitude (self ):
172+ """
173+ Assemble longitude decimal from sub-fields.
61174 """
62- return normalize_dms2dec ( self .cleaned_data . get ( "latitude " , "" ) )
175+ return self ._clean_coordinate ( "longitude " , 180 )
63176
64177
65178class WebhookForm (ModelForm ):
0 commit comments