igs-slm 0.1.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. igs_slm-0.1.0b0.dist-info/LICENSE +21 -0
  2. igs_slm-0.1.0b0.dist-info/METADATA +151 -0
  3. igs_slm-0.1.0b0.dist-info/RECORD +447 -0
  4. igs_slm-0.1.0b0.dist-info/WHEEL +4 -0
  5. igs_slm-0.1.0b0.dist-info/entry_points.txt +3 -0
  6. igs_tools/__init__.py +0 -0
  7. igs_tools/connection.py +88 -0
  8. igs_tools/defines/__init__.py +8 -0
  9. igs_tools/defines/constellation.py +21 -0
  10. igs_tools/defines/data_center.py +75 -0
  11. igs_tools/defines/rinex.py +49 -0
  12. igs_tools/directory.py +247 -0
  13. igs_tools/utils.py +66 -0
  14. slm/__init__.py +21 -0
  15. slm/admin.py +674 -0
  16. slm/api/edit/__init__.py +0 -0
  17. slm/api/edit/serializers.py +316 -0
  18. slm/api/edit/views.py +1632 -0
  19. slm/api/fields.py +89 -0
  20. slm/api/filter.py +504 -0
  21. slm/api/pagination.py +55 -0
  22. slm/api/permissions.py +65 -0
  23. slm/api/public/__init__.py +0 -0
  24. slm/api/public/serializers.py +249 -0
  25. slm/api/public/views.py +606 -0
  26. slm/api/serializers.py +132 -0
  27. slm/api/views.py +148 -0
  28. slm/apps.py +323 -0
  29. slm/authentication.py +198 -0
  30. slm/bin/__init__.py +0 -0
  31. slm/bin/startproject.py +262 -0
  32. slm/bin/templates/{{ project_dir }}/pyproject.toml +35 -0
  33. slm/bin/templates/{{ project_dir }}/sites/__init__.py +0 -0
  34. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/__init__.py +0 -0
  35. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/base.py +15 -0
  36. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/develop/__init__.py +56 -0
  37. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/develop/local.py +4 -0
  38. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/develop/wsgi.py +16 -0
  39. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/manage.py +34 -0
  40. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/production/__init__.py +61 -0
  41. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/production/wsgi.py +16 -0
  42. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/urls.py +7 -0
  43. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/validation.py +11 -0
  44. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/__init__.py +0 -0
  45. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/admin.py +5 -0
  46. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/apps.py +14 -0
  47. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/management/__init__.py +0 -0
  48. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/management/commands/__init__.py +0 -0
  49. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/management/commands/import_archive.py +64 -0
  50. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/migrations/__init__.py +0 -0
  51. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/models.py +6 -0
  52. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/templates/slm/base.html +8 -0
  53. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/urls.py +10 -0
  54. slm/bin/templates/{{ project_dir }}/{{ extension_app }}/views.py +5 -0
  55. slm/defines/AlertLevel.py +24 -0
  56. slm/defines/AntennaCalibration.py +25 -0
  57. slm/defines/AntennaFeatures.py +27 -0
  58. slm/defines/AntennaReferencePoint.py +22 -0
  59. slm/defines/Aspiration.py +13 -0
  60. slm/defines/CardinalDirection.py +19 -0
  61. slm/defines/CollocationStatus.py +12 -0
  62. slm/defines/EquipmentState.py +22 -0
  63. slm/defines/FlagSeverity.py +14 -0
  64. slm/defines/FractureSpacing.py +15 -0
  65. slm/defines/FrequencyStandardType.py +15 -0
  66. slm/defines/GeodesyMLVersion.py +48 -0
  67. slm/defines/ISOCountry.py +1194 -0
  68. slm/defines/Instrumentation.py +19 -0
  69. slm/defines/LogEntryType.py +30 -0
  70. slm/defines/SLMFileType.py +18 -0
  71. slm/defines/SiteFileUploadStatus.py +61 -0
  72. slm/defines/SiteLogFormat.py +49 -0
  73. slm/defines/SiteLogStatus.py +78 -0
  74. slm/defines/TectonicPlates.py +28 -0
  75. slm/defines/__init__.py +46 -0
  76. slm/forms.py +1126 -0
  77. slm/jinja2/slm/sitelog/ascii_9char.log +346 -0
  78. slm/jinja2/slm/sitelog/legacy.log +346 -0
  79. slm/jinja2/slm/sitelog/xsd/0.4/collocationInformation.xml +12 -0
  80. slm/jinja2/slm/sitelog/xsd/0.4/condition.xml +12 -0
  81. slm/jinja2/slm/sitelog/xsd/0.4/contact.xml +52 -0
  82. slm/jinja2/slm/sitelog/xsd/0.4/formInformation.xml +5 -0
  83. slm/jinja2/slm/sitelog/xsd/0.4/frequencyStandard.xml +12 -0
  84. slm/jinja2/slm/sitelog/xsd/0.4/gnssAntenna.xml +16 -0
  85. slm/jinja2/slm/sitelog/xsd/0.4/gnssReceiver.xml +11 -0
  86. slm/jinja2/slm/sitelog/xsd/0.4/humiditySensor.xml +13 -0
  87. slm/jinja2/slm/sitelog/xsd/0.4/localEpisodicEffect.xml +10 -0
  88. slm/jinja2/slm/sitelog/xsd/0.4/moreInformation.xml +22 -0
  89. slm/jinja2/slm/sitelog/xsd/0.4/multipathSource.xml +10 -0
  90. slm/jinja2/slm/sitelog/xsd/0.4/otherInstrumentation.xml +5 -0
  91. slm/jinja2/slm/sitelog/xsd/0.4/pressureSensor.xml +12 -0
  92. slm/jinja2/slm/sitelog/xsd/0.4/radioInterference.xml +11 -0
  93. slm/jinja2/slm/sitelog/xsd/0.4/sensor.xml +16 -0
  94. slm/jinja2/slm/sitelog/xsd/0.4/signalObstruction.xml +10 -0
  95. slm/jinja2/slm/sitelog/xsd/0.4/siteIdentification.xml +22 -0
  96. slm/jinja2/slm/sitelog/xsd/0.4/siteLocation.xml +21 -0
  97. slm/jinja2/slm/sitelog/xsd/0.4/surveyedLocalTie.xml +20 -0
  98. slm/jinja2/slm/sitelog/xsd/0.4/temperatureSensor.xml +13 -0
  99. slm/jinja2/slm/sitelog/xsd/0.4/waterVaporSensor.xml +11 -0
  100. slm/jinja2/slm/sitelog/xsd/0.5/document.xml +10 -0
  101. slm/jinja2/slm/sitelog/xsd/geodesyml_0.4.xml +99 -0
  102. slm/jinja2/slm/sitelog/xsd/geodesyml_0.5.xml +112 -0
  103. slm/management/__init__.py +0 -0
  104. slm/management/commands/__init__.py +53 -0
  105. slm/management/commands/build_index.py +96 -0
  106. slm/management/commands/generate_sinex.py +675 -0
  107. slm/management/commands/head_from_index.py +541 -0
  108. slm/management/commands/import_archive.py +908 -0
  109. slm/management/commands/import_equipment.py +351 -0
  110. slm/management/commands/set_site.py +56 -0
  111. slm/management/commands/sitelog.py +144 -0
  112. slm/management/commands/synchronize.py +60 -0
  113. slm/management/commands/update_data_availability.py +167 -0
  114. slm/management/commands/validate_db.py +186 -0
  115. slm/management/commands/validate_gml.py +73 -0
  116. slm/map/__init__.py +1 -0
  117. slm/map/admin.py +5 -0
  118. slm/map/api/__init__.py +0 -0
  119. slm/map/api/edit/__init__.py +0 -0
  120. slm/map/api/edit/serializers.py +28 -0
  121. slm/map/api/edit/views.py +46 -0
  122. slm/map/api/public/__init__.py +0 -0
  123. slm/map/api/public/serializers.py +29 -0
  124. slm/map/api/public/views.py +64 -0
  125. slm/map/apps.py +7 -0
  126. slm/map/defines.py +53 -0
  127. slm/map/migrations/0001_initial.py +115 -0
  128. slm/map/migrations/__init__.py +0 -0
  129. slm/map/models.py +63 -0
  130. slm/map/static/slm/css/map.css +86 -0
  131. slm/map/static/slm/js/map.js +159 -0
  132. slm/map/templates/slm/map.html +374 -0
  133. slm/map/templates/slm/station/base.html +11 -0
  134. slm/map/templates/slm/station/edit.html +10 -0
  135. slm/map/templates/slm/top_nav.html +17 -0
  136. slm/map/templatetags/__init__.py +0 -0
  137. slm/map/templatetags/slm_map.py +18 -0
  138. slm/map/urls.py +25 -0
  139. slm/map/views.py +36 -0
  140. slm/middleware.py +29 -0
  141. slm/migrations/0001_alter_siteantenna_marker_enu_alter_sitelocation_llh_and_more.py +47 -0
  142. slm/migrations/0001_initial.py +4826 -0
  143. slm/migrations/0002_alter_dataavailability_site.py +22 -0
  144. slm/migrations/0003_remove_logentry_slm_logentr_site_lo_7a2af7_idx_and_more.py +80 -0
  145. slm/migrations/0004_alter_logentry_timestamp_and_more.py +25 -0
  146. slm/migrations/0005_alter_logentry_options_alter_logentry_section_and_more.py +46 -0
  147. slm/migrations/0006_alter_logentry_options_alter_logentry_index_together.py +24 -0
  148. slm/migrations/0007_alter_dataavailability_rate.py +23 -0
  149. slm/migrations/0008_alter_archiveindex_options_and_more.py +64 -0
  150. slm/migrations/0009_alter_archiveindex_end.py +21 -0
  151. slm/migrations/0010_alter_dataavailability_rinex_version_and_more.py +844 -0
  152. slm/migrations/0011_alter_siteidentification_fracture_spacing.py +33 -0
  153. slm/migrations/0012_alter_logentry_type.py +36 -0
  154. slm/migrations/0013_unpublishedfilesalert.py +48 -0
  155. slm/migrations/0014_sitelogpublished.py +48 -0
  156. slm/migrations/0015_alter_siteantenna_options_and_more.py +181 -0
  157. slm/migrations/0016_alter_antenna_description_alter_radome_description_and_more.py +42 -0
  158. slm/migrations/0017_alter_logentry_unique_together_and_more.py +54 -0
  159. slm/migrations/0018_afix_deleted.py +34 -0
  160. slm/migrations/0018_alter_siteantenna_options_and_more.py +244 -0
  161. slm/migrations/0019_remove_siteantenna_marker_enu_siteantenna_marker_une_and_more.py +101 -0
  162. slm/migrations/0020_alter_manufacturer_options.py +16 -0
  163. slm/migrations/0021_alter_siteform_report_type.py +23 -0
  164. slm/migrations/0022_rename_antcal_antenna_radome_slm_antcal_antenna_20827a_idx_and_more.py +297 -0
  165. slm/migrations/0023_archivedsitelog_gml_version_and_more.py +55 -0
  166. slm/migrations/0024_alter_agency_name_alter_agency_shortname.py +24 -0
  167. slm/migrations/0025_alter_archivedsitelog_log_format_and_more.py +61 -0
  168. slm/migrations/0026_alter_archivedsitelog_log_format_and_more.py +61 -0
  169. slm/migrations/0027_importalert_file_contents_importalert_findings_and_more.py +41 -0
  170. slm/migrations/0028_antenna_replaced_manufacturer_url_radome_replaced_and_more.py +46 -0
  171. slm/migrations/0029_manufacturer_full_name.py +17 -0
  172. slm/migrations/0030_alter_antenna_state_alter_radome_state_and_more.py +43 -0
  173. slm/migrations/__init__.py +0 -0
  174. slm/migrations/load_satellitesystems.py +27 -0
  175. slm/models/__init__.py +118 -0
  176. slm/models/about.py +14 -0
  177. slm/models/alerts.py +1204 -0
  178. slm/models/data.py +58 -0
  179. slm/models/equipment.py +229 -0
  180. slm/models/help.py +14 -0
  181. slm/models/index.py +428 -0
  182. slm/models/sitelog.py +4279 -0
  183. slm/models/system.py +723 -0
  184. slm/models/user.py +304 -0
  185. slm/parsing/__init__.py +786 -0
  186. slm/parsing/legacy/__init__.py +4 -0
  187. slm/parsing/legacy/binding.py +817 -0
  188. slm/parsing/legacy/parser.py +377 -0
  189. slm/parsing/xsd/__init__.py +34 -0
  190. slm/parsing/xsd/binding.py +86 -0
  191. slm/parsing/xsd/geodesyml/0.4/commonTypes.xsd +133 -0
  192. slm/parsing/xsd/geodesyml/0.4/contact.xsd +29 -0
  193. slm/parsing/xsd/geodesyml/0.4/dataStreams.xsd +129 -0
  194. slm/parsing/xsd/geodesyml/0.4/document.xsd +64 -0
  195. slm/parsing/xsd/geodesyml/0.4/equipment.xsd +427 -0
  196. slm/parsing/xsd/geodesyml/0.4/fieldMeasurement.xsd +170 -0
  197. slm/parsing/xsd/geodesyml/0.4/geodesyML.xsd +71 -0
  198. slm/parsing/xsd/geodesyml/0.4/geodeticEquipment.xsd +343 -0
  199. slm/parsing/xsd/geodesyml/0.4/geodeticMonument.xsd +147 -0
  200. slm/parsing/xsd/geodesyml/0.4/lineage.xsd +614 -0
  201. slm/parsing/xsd/geodesyml/0.4/localInterferences.xsd +131 -0
  202. slm/parsing/xsd/geodesyml/0.4/measurement.xsd +473 -0
  203. slm/parsing/xsd/geodesyml/0.4/monumentInfo.xsd +251 -0
  204. slm/parsing/xsd/geodesyml/0.4/observationSystem.xsd +429 -0
  205. slm/parsing/xsd/geodesyml/0.4/project.xsd +38 -0
  206. slm/parsing/xsd/geodesyml/0.4/quality.xsd +176 -0
  207. slm/parsing/xsd/geodesyml/0.4/referenceFrame.xsd +194 -0
  208. slm/parsing/xsd/geodesyml/0.4/siteLog.xsd +71 -0
  209. slm/parsing/xsd/geodesyml/0.5/commonTypes.xsd +133 -0
  210. slm/parsing/xsd/geodesyml/0.5/contact.xsd +29 -0
  211. slm/parsing/xsd/geodesyml/0.5/dataStreams.xsd +129 -0
  212. slm/parsing/xsd/geodesyml/0.5/document.xsd +64 -0
  213. slm/parsing/xsd/geodesyml/0.5/equipment.xsd +427 -0
  214. slm/parsing/xsd/geodesyml/0.5/fieldMeasurement.xsd +170 -0
  215. slm/parsing/xsd/geodesyml/0.5/geodesyML.xsd +71 -0
  216. slm/parsing/xsd/geodesyml/0.5/geodeticEquipment.xsd +343 -0
  217. slm/parsing/xsd/geodesyml/0.5/geodeticMonument.xsd +147 -0
  218. slm/parsing/xsd/geodesyml/0.5/lineage.xsd +614 -0
  219. slm/parsing/xsd/geodesyml/0.5/localInterferences.xsd +131 -0
  220. slm/parsing/xsd/geodesyml/0.5/measurement.xsd +473 -0
  221. slm/parsing/xsd/geodesyml/0.5/monumentInfo.xsd +306 -0
  222. slm/parsing/xsd/geodesyml/0.5/observationSystem.xsd +429 -0
  223. slm/parsing/xsd/geodesyml/0.5/project.xsd +38 -0
  224. slm/parsing/xsd/geodesyml/0.5/quality.xsd +176 -0
  225. slm/parsing/xsd/geodesyml/0.5/referenceFrame.xsd +194 -0
  226. slm/parsing/xsd/geodesyml/0.5/siteLog.xsd +73 -0
  227. slm/parsing/xsd/parser.py +116 -0
  228. slm/parsing/xsd/resolver.py +28 -0
  229. slm/receivers/__init__.py +11 -0
  230. slm/receivers/alerts.py +87 -0
  231. slm/receivers/cleanup.py +41 -0
  232. slm/receivers/event_loggers.py +175 -0
  233. slm/receivers/index.py +67 -0
  234. slm/settings/__init__.py +55 -0
  235. slm/settings/auth.py +15 -0
  236. slm/settings/ckeditor.py +14 -0
  237. slm/settings/debug.py +47 -0
  238. slm/settings/internationalization.py +12 -0
  239. slm/settings/logging.py +113 -0
  240. slm/settings/platform/__init__.py +0 -0
  241. slm/settings/platform/darwin.py +10 -0
  242. slm/settings/rest.py +21 -0
  243. slm/settings/root.py +152 -0
  244. slm/settings/routines.py +43 -0
  245. slm/settings/secrets.py +37 -0
  246. slm/settings/security.py +5 -0
  247. slm/settings/slm.py +188 -0
  248. slm/settings/static_templates.py +53 -0
  249. slm/settings/templates.py +29 -0
  250. slm/settings/uploads.py +8 -0
  251. slm/settings/urls.py +126 -0
  252. slm/settings/validation.py +196 -0
  253. slm/signals.py +250 -0
  254. slm/singleton.py +49 -0
  255. slm/static/rest_framework/css/bootstrap-tweaks.css +204 -0
  256. slm/static/rest_framework/css/bootstrap.min.css +7 -0
  257. slm/static/rest_framework/css/bootstrap.min.css.map +1 -0
  258. slm/static/rest_framework/css/default.css +82 -0
  259. slm/static/rest_framework/css/prettify.css +30 -0
  260. slm/static/rest_framework/docs/css/base.css +344 -0
  261. slm/static/rest_framework/docs/css/highlight.css +125 -0
  262. slm/static/rest_framework/docs/css/jquery.json-view.min.css +11 -0
  263. slm/static/rest_framework/docs/img/favicon.ico +0 -0
  264. slm/static/rest_framework/docs/img/grid.png +0 -0
  265. slm/static/rest_framework/docs/js/api.js +321 -0
  266. slm/static/rest_framework/docs/js/highlight.pack.js +2 -0
  267. slm/static/rest_framework/docs/js/jquery.json-view.min.js +7 -0
  268. slm/static/rest_framework/img/grid.png +0 -0
  269. slm/static/rest_framework/js/ajax-form.js +127 -0
  270. slm/static/rest_framework/js/bootstrap.bundle.min.js +7 -0
  271. slm/static/rest_framework/js/bootstrap.bundle.min.js.map +1 -0
  272. slm/static/rest_framework/js/bootstrap.min.js.map +1 -0
  273. slm/static/rest_framework/js/coreapi-0.1.1.js +2043 -0
  274. slm/static/rest_framework/js/csrf.js +52 -0
  275. slm/static/rest_framework/js/default.js +47 -0
  276. slm/static/rest_framework/js/jquery-3.5.1.min.js +2 -0
  277. slm/static/rest_framework/js/prettify-min.js +28 -0
  278. slm/static/slm/css/admin.css +3 -0
  279. slm/static/slm/css/defines.css +82 -0
  280. slm/static/slm/css/forms.css +1 -0
  281. slm/static/slm/css/style.css +1004 -0
  282. slm/static/slm/img/email-branding.png +0 -0
  283. slm/static/slm/img/favicon.ico +0 -0
  284. slm/static/slm/img/login-bg.jpg +0 -0
  285. slm/static/slm/img/slm-logo.svg +4 -0
  286. slm/static/slm/js/autocomplete.js +341 -0
  287. slm/static/slm/js/enums.js +322 -0
  288. slm/static/slm/js/fileIcons.js +30 -0
  289. slm/static/slm/js/form.js +404 -0
  290. slm/static/slm/js/formWidget.js +23 -0
  291. slm/static/slm/js/persistable.js +33 -0
  292. slm/static/slm/js/slm.js +1028 -0
  293. slm/static/slm/js/time24.js +212 -0
  294. slm/static_templates/slm/css/defines.css +26 -0
  295. slm/static_templates/slm/js/enums.js +28 -0
  296. slm/static_templates/slm/js/fileIcons.js +16 -0
  297. slm/static_templates/slm/js/urls.js +5 -0
  298. slm/templates/account/base.html +20 -0
  299. slm/templates/account/email/base.html +43 -0
  300. slm/templates/account/email/base_message.txt +7 -0
  301. slm/templates/account/email/email_confirmation_message.html +16 -0
  302. slm/templates/account/email/email_confirmation_message.txt +7 -0
  303. slm/templates/account/email/email_confirmation_signup_message.html +1 -0
  304. slm/templates/account/email/email_confirmation_signup_message.txt +1 -0
  305. slm/templates/account/email/email_confirmation_signup_subject.txt +1 -0
  306. slm/templates/account/email/email_confirmation_subject.txt +4 -0
  307. slm/templates/account/email/password_reset_key_message.html +28 -0
  308. slm/templates/account/email/password_reset_key_message.txt +9 -0
  309. slm/templates/account/email/password_reset_key_subject.txt +4 -0
  310. slm/templates/account/email/unknown_account_message.html +25 -0
  311. slm/templates/account/email/unknown_account_message.txt +12 -0
  312. slm/templates/account/email/unknown_account_subject.txt +4 -0
  313. slm/templates/account/login.html +67 -0
  314. slm/templates/account/logout.html +38 -0
  315. slm/templates/account/password_change.html +48 -0
  316. slm/templates/account/password_reset.html +51 -0
  317. slm/templates/account/password_reset_done.html +20 -0
  318. slm/templates/account/password_reset_from_key.html +52 -0
  319. slm/templates/account/password_reset_from_key_done.html +17 -0
  320. slm/templates/admin/base.html +7 -0
  321. slm/templates/messages.html +8 -0
  322. slm/templates/rest_framework/README +16 -0
  323. slm/templates/rest_framework/admin/detail.html +10 -0
  324. slm/templates/rest_framework/admin/dict_value.html +11 -0
  325. slm/templates/rest_framework/admin/list.html +32 -0
  326. slm/templates/rest_framework/admin/list_value.html +11 -0
  327. slm/templates/rest_framework/admin/simple_list_value.html +2 -0
  328. slm/templates/rest_framework/admin.html +282 -0
  329. slm/templates/rest_framework/api.html +3 -0
  330. slm/templates/rest_framework/base.html +334 -0
  331. slm/templates/rest_framework/docs/auth/basic.html +42 -0
  332. slm/templates/rest_framework/docs/auth/session.html +40 -0
  333. slm/templates/rest_framework/docs/auth/token.html +41 -0
  334. slm/templates/rest_framework/docs/document.html +37 -0
  335. slm/templates/rest_framework/docs/error.html +71 -0
  336. slm/templates/rest_framework/docs/index.html +55 -0
  337. slm/templates/rest_framework/docs/interact.html +57 -0
  338. slm/templates/rest_framework/docs/langs/javascript-intro.html +5 -0
  339. slm/templates/rest_framework/docs/langs/javascript.html +15 -0
  340. slm/templates/rest_framework/docs/langs/python-intro.html +3 -0
  341. slm/templates/rest_framework/docs/langs/python.html +13 -0
  342. slm/templates/rest_framework/docs/langs/shell-intro.html +3 -0
  343. slm/templates/rest_framework/docs/langs/shell.html +6 -0
  344. slm/templates/rest_framework/docs/link.html +113 -0
  345. slm/templates/rest_framework/docs/sidebar.html +78 -0
  346. slm/templates/rest_framework/filters/base.html +16 -0
  347. slm/templates/rest_framework/filters/ordering.html +17 -0
  348. slm/templates/rest_framework/filters/search.html +13 -0
  349. slm/templates/rest_framework/horizontal/checkbox.html +23 -0
  350. slm/templates/rest_framework/horizontal/checkbox_multiple.html +32 -0
  351. slm/templates/rest_framework/horizontal/dict_field.html +11 -0
  352. slm/templates/rest_framework/horizontal/fieldset.html +16 -0
  353. slm/templates/rest_framework/horizontal/form.html +6 -0
  354. slm/templates/rest_framework/horizontal/input.html +21 -0
  355. slm/templates/rest_framework/horizontal/list_field.html +11 -0
  356. slm/templates/rest_framework/horizontal/list_fieldset.html +13 -0
  357. slm/templates/rest_framework/horizontal/radio.html +42 -0
  358. slm/templates/rest_framework/horizontal/select.html +36 -0
  359. slm/templates/rest_framework/horizontal/select_multiple.html +38 -0
  360. slm/templates/rest_framework/horizontal/textarea.html +21 -0
  361. slm/templates/rest_framework/inline/checkbox.html +8 -0
  362. slm/templates/rest_framework/inline/checkbox_multiple.html +14 -0
  363. slm/templates/rest_framework/inline/dict_field.html +9 -0
  364. slm/templates/rest_framework/inline/fieldset.html +6 -0
  365. slm/templates/rest_framework/inline/form.html +8 -0
  366. slm/templates/rest_framework/inline/input.html +9 -0
  367. slm/templates/rest_framework/inline/list_field.html +9 -0
  368. slm/templates/rest_framework/inline/list_fieldset.html +3 -0
  369. slm/templates/rest_framework/inline/radio.html +25 -0
  370. slm/templates/rest_framework/inline/select.html +24 -0
  371. slm/templates/rest_framework/inline/select_multiple.html +25 -0
  372. slm/templates/rest_framework/inline/textarea.html +9 -0
  373. slm/templates/rest_framework/login.html +3 -0
  374. slm/templates/rest_framework/login_base.html +65 -0
  375. slm/templates/rest_framework/pagination/numbers.html +47 -0
  376. slm/templates/rest_framework/pagination/previous_and_next.html +21 -0
  377. slm/templates/rest_framework/raw_data_form.html +11 -0
  378. slm/templates/rest_framework/schema.js +3 -0
  379. slm/templates/rest_framework/vertical/checkbox.html +16 -0
  380. slm/templates/rest_framework/vertical/checkbox_multiple.html +30 -0
  381. slm/templates/rest_framework/vertical/dict_field.html +7 -0
  382. slm/templates/rest_framework/vertical/fieldset.html +13 -0
  383. slm/templates/rest_framework/vertical/form.html +6 -0
  384. slm/templates/rest_framework/vertical/input.html +17 -0
  385. slm/templates/rest_framework/vertical/list_field.html +7 -0
  386. slm/templates/rest_framework/vertical/list_fieldset.html +7 -0
  387. slm/templates/rest_framework/vertical/radio.html +40 -0
  388. slm/templates/rest_framework/vertical/select.html +34 -0
  389. slm/templates/rest_framework/vertical/select_multiple.html +31 -0
  390. slm/templates/rest_framework/vertical/textarea.html +17 -0
  391. slm/templates/slm/about.html +21 -0
  392. slm/templates/slm/alerts/alert.html +15 -0
  393. slm/templates/slm/alerts/geodesymlinvalid.html +8 -0
  394. slm/templates/slm/alerts/importalert.html +10 -0
  395. slm/templates/slm/alerts.html +18 -0
  396. slm/templates/slm/auth_menu.html +41 -0
  397. slm/templates/slm/base.html +195 -0
  398. slm/templates/slm/emails/alert_issued.html +31 -0
  399. slm/templates/slm/emails/alert_issued.txt +9 -0
  400. slm/templates/slm/emails/base.html +6 -0
  401. slm/templates/slm/emails/changes_rejected.txt +7 -0
  402. slm/templates/slm/emails/review_requested.txt +7 -0
  403. slm/templates/slm/forms/widgets/auto_complete.html +21 -0
  404. slm/templates/slm/forms/widgets/auto_complete_multiple.html +18 -0
  405. slm/templates/slm/forms/widgets/checkbox_multiple.html +6 -0
  406. slm/templates/slm/forms/widgets/inline_multi.html +1 -0
  407. slm/templates/slm/forms/widgets/splitdatetime.html +14 -0
  408. slm/templates/slm/forms/widgets/time24.html +37 -0
  409. slm/templates/slm/help.html +54 -0
  410. slm/templates/slm/messages.html +13 -0
  411. slm/templates/slm/new_site.html +88 -0
  412. slm/templates/slm/profile.html +57 -0
  413. slm/templates/slm/register.html +40 -0
  414. slm/templates/slm/reports/file_log.html +43 -0
  415. slm/templates/slm/reports/head_log.html +23 -0
  416. slm/templates/slm/reports/head_report.html +55 -0
  417. slm/templates/slm/reports/index_log.html +23 -0
  418. slm/templates/slm/reports/index_report.html +71 -0
  419. slm/templates/slm/station/alert.html +8 -0
  420. slm/templates/slm/station/alerts.html +19 -0
  421. slm/templates/slm/station/base.html +104 -0
  422. slm/templates/slm/station/download.html +87 -0
  423. slm/templates/slm/station/edit.html +283 -0
  424. slm/templates/slm/station/form.html +110 -0
  425. slm/templates/slm/station/log.html +18 -0
  426. slm/templates/slm/station/review.html +461 -0
  427. slm/templates/slm/station/upload.html +295 -0
  428. slm/templates/slm/station/uploads/attachment.html +20 -0
  429. slm/templates/slm/station/uploads/geodesyml.html +1 -0
  430. slm/templates/slm/station/uploads/image.html +27 -0
  431. slm/templates/slm/station/uploads/json.html +0 -0
  432. slm/templates/slm/station/uploads/legacy.html +77 -0
  433. slm/templates/slm/top_nav.html +14 -0
  434. slm/templates/slm/user_activity.html +16 -0
  435. slm/templates/slm/widgets/alert_scroll.html +135 -0
  436. slm/templates/slm/widgets/filelist.html +258 -0
  437. slm/templates/slm/widgets/legend.html +12 -0
  438. slm/templates/slm/widgets/log_scroll.html +88 -0
  439. slm/templates/slm/widgets/stationlist.html +233 -0
  440. slm/templatetags/__init__.py +0 -0
  441. slm/templatetags/jinja2.py +9 -0
  442. slm/templatetags/slm.py +459 -0
  443. slm/urls.py +148 -0
  444. slm/utils.py +299 -0
  445. slm/validators.py +297 -0
  446. slm/views.py +654 -0
  447. slm/widgets.py +134 -0
slm/forms.py ADDED
@@ -0,0 +1,1126 @@
1
+ """
2
+ Handles forms for site log sections, user
3
+ management.
4
+
5
+ There is a form for each sitelog section.
6
+
7
+ More info on forms:
8
+ https://docs.djangoproject.com/en/stable/topics/forms/
9
+
10
+ More info on field types:
11
+ https://docs.djangoproject.com/en/stable/ref/models/fields/
12
+ """
13
+
14
+ import json
15
+
16
+ from ckeditor.widgets import CKEditorWidget
17
+ from crispy_forms.helper import FormHelper
18
+ from crispy_forms.layout import Div, Layout
19
+ from django import forms
20
+ from django.contrib.gis.forms import PointField
21
+ from django.contrib.gis.geos import Point, Polygon
22
+ from django.core.exceptions import FieldDoesNotExist, ValidationError
23
+ from django.core.validators import MinValueValidator
24
+ from django.db import transaction
25
+ from django.db.models import Max
26
+ from django.db.models.fields import NOT_PROVIDED
27
+ from django.forms.fields import BooleanField, CharField, TypedMultipleChoiceField
28
+ from django.forms.widgets import (
29
+ CheckboxInput,
30
+ CheckboxSelectMultiple,
31
+ MultiWidget,
32
+ NullBooleanSelect,
33
+ NumberInput,
34
+ Select,
35
+ TextInput,
36
+ )
37
+ from django.urls import reverse_lazy
38
+ from django.utils.functional import Promise, SimpleLazyObject, cached_property
39
+ from django.utils.translation import gettext as _
40
+ from django_enum.choices import choices
41
+ from django_enum.forms import EnumChoiceField
42
+ from polyline import polyline
43
+
44
+ from slm.api.edit.serializers import UserProfileSerializer, UserSerializer
45
+ from slm.defines import (
46
+ AlertLevel,
47
+ CardinalDirection,
48
+ FrequencyStandardType,
49
+ ISOCountry,
50
+ SiteLogStatus,
51
+ SLMFileType,
52
+ )
53
+ from slm.models import (
54
+ Agency,
55
+ Alert,
56
+ Antenna,
57
+ Network,
58
+ Radome,
59
+ Receiver,
60
+ SatelliteSystem,
61
+ Site,
62
+ SiteAntenna,
63
+ SiteCollocation,
64
+ SiteFileUpload,
65
+ SiteForm,
66
+ SiteFrequencyStandard,
67
+ SiteHumiditySensor,
68
+ SiteIdentification,
69
+ SiteLocalEpisodicEffects,
70
+ SiteLocation,
71
+ SiteMoreInformation,
72
+ SiteMultiPathSources,
73
+ SiteOperationalContact,
74
+ SiteOtherInstrumentation,
75
+ SitePressureSensor,
76
+ SiteRadioInterferences,
77
+ SiteReceiver,
78
+ SiteResponsibleAgency,
79
+ SiteSignalObstructions,
80
+ SiteSurveyedLocalTies,
81
+ SiteTemperatureSensor,
82
+ SiteWaterVaporRadiometer,
83
+ )
84
+ from slm.validators import FieldRequired, get_validators
85
+ from slm.widgets import (
86
+ AutoComplete,
87
+ AutoCompleteEnumSelectMultiple,
88
+ AutoCompleteSelectMultiple,
89
+ DatePicker,
90
+ EnumSelectMultiple,
91
+ GraphicTextarea,
92
+ SLMCheckboxSelectMultiple,
93
+ SLMDateTimeWidget,
94
+ )
95
+
96
+
97
+ class SLMCheckboxInput(CheckboxInput):
98
+ """
99
+ Django's BooleanField/NullBooleanField does not properly handle value ==
100
+ 'on' or value == 'off' which are submitted as form data for checkboxes and
101
+ switches in certain circumstances. We Provide our own extensions here that
102
+ do this. This might change in the future and if so these fields can be
103
+ deleted and replaced by the Django internal ones.
104
+ """
105
+
106
+ def format_value(self, value):
107
+ if isinstance(value, list):
108
+ value = value[0]
109
+ return {"on": "true", "off": "false"}.get(
110
+ value.lower() if isinstance(value, str) else value,
111
+ super().format_value(value),
112
+ )
113
+
114
+ def value_from_datadict(self, data, files, name):
115
+ value = data.get(name)
116
+ if isinstance(value, list):
117
+ value = value[0]
118
+ return {"on": True, "off": False}.get(
119
+ value.lower() if isinstance(value, str) else value,
120
+ super().value_from_datadict(data, files, name),
121
+ )
122
+
123
+
124
+ class SLMBooleanField(BooleanField):
125
+ widget = SLMCheckboxInput
126
+
127
+ def to_python(self, value):
128
+ if isinstance(value, str) and value.lower() in ("false", "0", "off"):
129
+ value = False
130
+ else:
131
+ value = bool(value)
132
+ return super().to_python(value)
133
+
134
+
135
+ class SLMNullBooleanSelect(NullBooleanSelect):
136
+ def __init__(self, attrs=None):
137
+ choices = (
138
+ ("", _("Unknown")),
139
+ ("true", _("Yes")),
140
+ ("false", _("No")),
141
+ )
142
+ # skip NullBooleanSelect init
143
+ Select.__init__(self, attrs, choices)
144
+
145
+ def format_value(self, value):
146
+ if value in [None, "", "None"]:
147
+ return [""]
148
+ return [super().format_value(value)]
149
+
150
+ def value_from_datadict(self, data, files, name):
151
+ value = data.get(name)
152
+ return {None: None, "": None}.get(
153
+ value, super().value_from_datadict(data, files, name)
154
+ )
155
+
156
+
157
+ class SiteAntennaGraphicField(CharField):
158
+ widget = GraphicTextarea
159
+
160
+
161
+ class PolylineWidget(TextInput):
162
+ def value_from_datadict(self, data, files, name):
163
+ if hasattr(data, "getlist"):
164
+ return data.getlist(name, []) or None
165
+ return data.get(name, []) or None
166
+
167
+
168
+ class PolylineListField(forms.CharField):
169
+ default_error_messages = {
170
+ "invalid": _(
171
+ "Unable to decode polyline {poly} - {error}. Bounding boxes "
172
+ "should be line strings holding (longitude, latitude) tuples "
173
+ "encoded using Google's polyline algorithm"
174
+ )
175
+ }
176
+
177
+ widget = PolylineWidget
178
+
179
+ def clean(self, value):
180
+ if value is None or value == [""]:
181
+ return None
182
+ polygons = []
183
+ for poly in value:
184
+ try:
185
+ linear_ring = polyline.decode(poly)
186
+ linear_ring.append(linear_ring[0]) # close the ring
187
+ polygons.append(Polygon(linear_ring))
188
+ except Exception as exc:
189
+ raise ValidationError(
190
+ self.error_messages["invalid"].format(poly=poly, error=str(exc)),
191
+ code="invalid",
192
+ )
193
+ return polygons
194
+
195
+
196
+ class EnumMultipleChoiceField(EnumChoiceField, TypedMultipleChoiceField):
197
+ """
198
+ The default ``ChoiceField`` will only accept the base enumeration values.
199
+ Use this field on forms to accept any value mappable to an enumeration
200
+ including any labels or symmetric properties.
201
+ """
202
+
203
+ widget = EnumSelectMultiple
204
+
205
+ def __init__(self, *args, **kwargs):
206
+ coerce = self.coerce
207
+ super().__init__(*args, **kwargs)
208
+ self.coerce = coerce
209
+
210
+ def coerce(self, value):
211
+ if isinstance(value, self.enum):
212
+ return value
213
+ return [super(EnumMultipleChoiceField, self).coerce(val) for val in value]
214
+
215
+
216
+ class SLMDateField(forms.DateField):
217
+ input_type = "date"
218
+
219
+
220
+ class SLMTimeField(forms.TimeField):
221
+ input_type = "time"
222
+
223
+
224
+ class SLMDateTimeField(forms.SplitDateTimeField):
225
+ widget = SLMDateTimeWidget
226
+
227
+ def __init__(self, *args, **kwargs):
228
+ super().__init__(*args, **kwargs)
229
+ self.widget.widgets[0].input_type = "date"
230
+ self.widget.widgets[1].input_type = "time"
231
+
232
+
233
+ class PointWidget(MultiWidget):
234
+ dim = 3
235
+
236
+ template_name = "slm/forms/widgets/inline_multi.html"
237
+
238
+ def __init__(self, dim=dim, attrs=None):
239
+ self.dim = dim
240
+ super().__init__([NumberInput(attrs=attrs) for _ in range(0, self.dim)], attrs)
241
+
242
+ def get_context(self, name, value, attrs):
243
+ context = super().get_context(name, value, attrs)
244
+ # fix the names so they are all the same
245
+ for subwidget in context["widget"]["subwidgets"]:
246
+ subwidget["name"] = "_".join(subwidget["name"].split("_")[:-1])
247
+ return context
248
+
249
+ def decompress(self, values):
250
+ from django.contrib.gis.geos import Point
251
+
252
+ if values is None:
253
+ return Point(*[None for _ in range(0, self.dim)])
254
+ return Point(*values)
255
+
256
+ def value_from_datadict(self, data, files, name):
257
+ if name in data:
258
+ return [float(coord) for coord in data.getlist(name)]
259
+ return None
260
+
261
+
262
+ class SLMPointField(PointField):
263
+ widget = PointWidget
264
+
265
+ def __init__(self, *args, attrs=None, dim=None, **kwargs):
266
+ if dim is not None:
267
+ self.widget = self.widget(dim=dim)
268
+ super().__init__(*args, **kwargs)
269
+ if self.widget.attrs is None:
270
+ self.widget.attrs = {}
271
+ self.widget.attrs.update(attrs or {})
272
+
273
+ def clean(self, value):
274
+ """
275
+ Raise a ValidationError if the value cannot be
276
+ instantiated as a Point - otherwise return the Point or None.
277
+ """
278
+ if value is None:
279
+ return value
280
+
281
+ # Ensuring that the geometry is of the correct type (indicated
282
+ # using the OGC string label).
283
+ if len(value) != self.widget.dim:
284
+ raise ValidationError(
285
+ self.error_messages["invalid_geom_type"], code="invalid_geom_type"
286
+ )
287
+ return (
288
+ Point(*[None if val in ["", None] else float(val) for val in value]) or None
289
+ )
290
+
291
+
292
+ class AutoSelectMixin:
293
+ def __init__(
294
+ self,
295
+ *args,
296
+ value_param="id",
297
+ label_param=None,
298
+ render_suggestion=None,
299
+ query_params=None,
300
+ menu_class=None,
301
+ **kwargs,
302
+ ):
303
+ super().__init__(*args, **kwargs)
304
+ self.widget.attrs.update({"data-value-param": value_param})
305
+ self.widget.attrs["data-value-param"] = value_param
306
+ if label_param:
307
+ self.widget.attrs["data-label-param"] = label_param
308
+ if render_suggestion:
309
+ self.widget.attrs["data-render-suggestion"] = render_suggestion
310
+ if menu_class:
311
+ self.widget.attrs["data-menu-class"] = menu_class
312
+ if query_params:
313
+ self.widget.attrs["data-query-params"] = (
314
+ query_params
315
+ if isinstance(query_params, str)
316
+ else json.dumps(query_params)
317
+ )
318
+ self.widget.attrs["class"] = " ".join(
319
+ [*self.widget.attrs.get("class", "").split(" "), "search-input"]
320
+ )
321
+
322
+
323
+ class ModelAutoSelectMixin(AutoSelectMixin):
324
+ def __init__(self, service_url, *args, search_param="search", **kwargs):
325
+ super().__init__(*args, **kwargs)
326
+ self.widget.attrs["data-service-url"] = service_url
327
+ self.widget.attrs["data-search-param"] = search_param
328
+
329
+
330
+ class ModelAutoComplete(ModelAutoSelectMixin, forms.ModelChoiceField):
331
+ widget = AutoComplete
332
+
333
+
334
+ class ModelMultipleAutoComplete(ModelAutoSelectMixin, forms.ModelMultipleChoiceField):
335
+ widget = AutoCompleteSelectMultiple
336
+
337
+
338
+ class EnumAutoSelectMixin(AutoSelectMixin):
339
+ class PropertyEncoder(json.JSONEncoder):
340
+ def default(self, obj):
341
+ if isinstance(obj, Promise):
342
+ return str(obj)
343
+ return super().default(obj)
344
+
345
+ def __init__(self, enum, *args, properties=None, data_source=None, **kwargs):
346
+ super().__init__(
347
+ enum,
348
+ *args,
349
+ value_param=kwargs.pop("value_param", "value"),
350
+ label_param=kwargs.pop("label_param", "label"),
351
+ **kwargs,
352
+ )
353
+ properties = set(properties or [])
354
+ properties.update({"value", "label"})
355
+
356
+ def lazy_source():
357
+ """
358
+ Data sources might hit the database - so we have to evaluate them
359
+ lazily at widget render time instead of before django bootstrapping
360
+ """
361
+ return json.dumps(
362
+ sorted(
363
+ [
364
+ {prop: getattr(en, prop) for prop in properties}
365
+ for en in (data_source() if data_source else enum or [])
366
+ ],
367
+ key=lambda en: en[self.widget.attrs["data-label-param"]],
368
+ ),
369
+ cls=MultiSelectEnumAutoComplete.PropertyEncoder,
370
+ )
371
+
372
+ self.widget.attrs["data-source"] = SimpleLazyObject(lazy_source)
373
+
374
+
375
+ class MultiSelectEnumAutoComplete(EnumAutoSelectMixin, EnumMultipleChoiceField):
376
+ widget = AutoCompleteEnumSelectMultiple
377
+
378
+
379
+ class SelectEnumAutoComplete(EnumAutoSelectMixin, EnumChoiceField):
380
+ widget = AutoComplete
381
+
382
+ def __init__(self, *args, widget=AutoComplete, **kwargs):
383
+ super().__init__(*args, widget=widget, **kwargs)
384
+
385
+
386
+ class NewSiteForm(forms.ModelForm):
387
+ @property
388
+ def helper(self):
389
+ helper = FormHelper()
390
+ helper.form_id = "slm-new-site-form"
391
+ helper.layout = Layout(
392
+ Div(
393
+ Div("name", css_class="col-3"),
394
+ Div("agencies", css_class="col-9"),
395
+ css_class="row",
396
+ )
397
+ )
398
+ return helper
399
+
400
+ agencies = ModelMultipleAutoComplete(
401
+ queryset=Agency.objects.all(),
402
+ help_text=_("Enter the name or abbreviation of an Agency."),
403
+ label=_("Agency"),
404
+ required=False,
405
+ service_url=reverse_lazy("slm_edit_api:agency-list"),
406
+ search_param="search",
407
+ value_param="id",
408
+ label_param="name",
409
+ render_suggestion="return `(${obj.shortname}) ${obj.name}`;",
410
+ )
411
+
412
+ class Meta:
413
+ model = Site
414
+ fields = ["name", "agencies"]
415
+
416
+
417
+ class SectionForm(forms.ModelForm):
418
+ def __init__(self, instance=None, **kwargs):
419
+ self.diff = instance.published_diff() if instance else {}
420
+ self.flags = instance._flags if instance else {}
421
+ super().__init__(instance=instance, **kwargs)
422
+ for field in self.fields:
423
+ try:
424
+ model_field = self.Meta.model._meta.get_field(field)
425
+ for validator in get_validators(self.Meta.model, model_field.name):
426
+ if isinstance(validator, FieldRequired):
427
+ self.fields[field].required = True
428
+ break
429
+ self.fields[field].required |= not (
430
+ getattr(model_field, "default", None) != NOT_PROVIDED
431
+ and model_field.blank
432
+ )
433
+ self.fields[field].widget.attrs.setdefault("class", "")
434
+ self.fields[field].widget.attrs["class"] += " slm-form-field"
435
+ except FieldDoesNotExist:
436
+ pass
437
+
438
+ @classmethod
439
+ def section_name(cls):
440
+ return cls.Meta.model.section_name()
441
+
442
+ @classmethod
443
+ def section_slug(cls):
444
+ return cls.Meta.model.section_slug()
445
+
446
+ @property
447
+ def num_flags(self):
448
+ return len(self.flags)
449
+
450
+ @classmethod
451
+ def api(cls):
452
+ return f"slm_edit_api:{cls.Meta.model.__name__.lower()}"
453
+
454
+ @cached_property
455
+ def structured_fields(self):
456
+ # todo this is spaghetti
457
+ # arrange fields in structure to easily produce fieldsets in
458
+ # correct order in the template, reflects old site
459
+ # log order and groupings
460
+ fields = []
461
+
462
+ def flatten(structure):
463
+ flat = []
464
+ for field in structure:
465
+ if isinstance(field, tuple) or isinstance(field, list):
466
+ flat += flatten(field)
467
+ else:
468
+ flat.append(field)
469
+ return flat
470
+
471
+ def resolve_field(field_name):
472
+ fields = []
473
+ if field_name not in self.fields:
474
+ if hasattr(getattr(self.Meta.model, field_name), "field"):
475
+ field = getattr(self.Meta.model, field_name).field
476
+ if isinstance(field, tuple) or isinstance(field, list):
477
+ fields.extend([fd.name for fd in field])
478
+ else:
479
+ fields.append(field.name)
480
+ else:
481
+ fields.append(field_name)
482
+
483
+ return [
484
+ self.fields[field].get_bound_field(form=self, field_name=field)
485
+ for field in fields
486
+ ]
487
+
488
+ for structure in self.Meta.model.structure():
489
+ if isinstance(structure, tuple) or isinstance(structure, list):
490
+ group_fields = []
491
+ try:
492
+ self._meta.model._meta.get_field(structure[0])
493
+ group = None
494
+ group_fields.append(resolve_field(structure[0])[0])
495
+ except FieldDoesNotExist:
496
+ group = structure[0]
497
+
498
+ for field in flatten(structure[1]):
499
+ group_fields.extend(resolve_field(field))
500
+
501
+ fields.append((group, group_fields))
502
+ else:
503
+ fields.append((None, resolve_field(structure)))
504
+ fields += [(None, [field]) for field in self.hidden_fields()]
505
+ return fields
506
+
507
+ # todo this might be a security hole - restrict queryset to user's stations
508
+ site = forms.ModelChoiceField(
509
+ queryset=Site.objects.all(), widget=forms.HiddenInput()
510
+ )
511
+
512
+ id = forms.IntegerField(
513
+ validators=[MinValueValidator(0)], widget=forms.HiddenInput(), required=False
514
+ )
515
+
516
+ class Meta:
517
+ fields = ["site", "id"]
518
+
519
+
520
+ class SubSectionForm(SectionForm):
521
+ subsection = forms.IntegerField(
522
+ validators=[MinValueValidator(0)], widget=forms.HiddenInput(), required=False
523
+ )
524
+
525
+ def save(self, commit=True):
526
+ if self.instance.subsection is None:
527
+ with transaction.atomic():
528
+ # todo is there a race condition here?
529
+ self.instance.subsection = (
530
+ self.Meta.model.objects.select_for_update()
531
+ .filter(site=self.instance.site)
532
+ .aggregate(Max("subsection"))["subsection__max"]
533
+ or 0
534
+ ) + 1
535
+
536
+ return super().save(commit=commit)
537
+ return super().save(commit=commit)
538
+
539
+ @classmethod
540
+ def group_name(cls):
541
+ if hasattr(cls, "NAV_HEADING"):
542
+ return cls.NAV_HEADING.replace(" ", "_").replace(".", "").strip().lower()
543
+ return None
544
+
545
+ class Meta(SectionForm.Meta):
546
+ fields = [*SectionForm.Meta.fields, "subsection"]
547
+
548
+
549
+ class SiteFormForm(SectionForm):
550
+ def __init__(self, *args, **kwargs):
551
+ super().__init__(*args, **kwargs)
552
+ for field in ["report_type", "date_prepared"]:
553
+ self.fields[field].required = False
554
+ self.fields[field].widget.attrs["disabled"] = "disabled"
555
+
556
+ # we only include this for legacy purposes - this is not an editable value
557
+ previous_log = forms.CharField(
558
+ label=_("Previous Site Log"),
559
+ help_text=_(
560
+ "Previous site log in this format: ssss_CCYYMMDD.log "
561
+ "Format: (ssss = 4 character site name). If the site already has "
562
+ "a log at the IGS Central Bureau it can be found at"
563
+ "https://files.igs.org/pub/station/log/"
564
+ ),
565
+ disabled=True,
566
+ required=False,
567
+ )
568
+
569
+ class Meta(SectionForm.Meta):
570
+ model = SiteForm
571
+ fields = [*SectionForm.Meta.fields, *SiteForm.site_log_fields(), "previous_log"]
572
+ widgets = {"date_prepared": DatePicker}
573
+
574
+
575
+ class SiteIdentificationForm(SectionForm):
576
+ # we only include this for legacy purposes - this is not an editable value
577
+ nine_character_id = forms.CharField(
578
+ label=_("Nine Character ID"),
579
+ help_text=_(
580
+ "This is the 9 Character station name (XXXXMRCCC) used in RINEX 3 "
581
+ "filenames. Format: (XXXX - existing four character IGS station "
582
+ "name, M - Monument or marker number (0-9), R - Receiver number "
583
+ "(0-9), CCC - Three digit ISO 3166-1 country code)"
584
+ ),
585
+ disabled=True,
586
+ required=False,
587
+ )
588
+
589
+ class Meta(SectionForm.Meta):
590
+ model = SiteIdentification
591
+ fields = [
592
+ *SectionForm.Meta.fields,
593
+ *SiteIdentification.site_log_fields(),
594
+ "nine_character_id",
595
+ ]
596
+ field_classes = {"date_installed": SLMDateTimeField}
597
+
598
+
599
+ class SiteLocationForm(SectionForm):
600
+ country = SelectEnumAutoComplete(
601
+ ISOCountry,
602
+ help_text=SiteLocation._meta.get_field("country").help_text,
603
+ label=SiteLocation._meta.get_field("country").verbose_name,
604
+ render_suggestion=(
605
+ 'return `<span class="fi fi-${obj.value.toLowerCase()}"></span>'
606
+ '<span class="matchable">${obj.label}</span>`;'
607
+ ),
608
+ strict=False,
609
+ )
610
+
611
+ xyz = SLMPointField(
612
+ help_text=SiteLocation._meta.get_field("xyz").help_text,
613
+ label=SiteLocation._meta.get_field("xyz").verbose_name,
614
+ )
615
+
616
+ llh = SLMPointField(
617
+ help_text=SiteLocation._meta.get_field("llh").help_text,
618
+ label=SiteLocation._meta.get_field("llh").verbose_name,
619
+ attrs={"step": 0.0000001},
620
+ )
621
+
622
+ class Meta:
623
+ model = SiteLocation
624
+ fields = [*SectionForm.Meta.fields, *SiteLocation.site_log_fields()]
625
+
626
+
627
+ class SiteReceiverForm(SubSectionForm):
628
+ COPY_LAST_ON_ADD = [
629
+ field
630
+ for field in [*SubSectionForm.Meta.fields, *SiteReceiver.site_log_fields()]
631
+ if field not in {"installed", "removed", "additional_info"}
632
+ ]
633
+
634
+ satellite_system = forms.ModelChoiceField(
635
+ queryset=SatelliteSystem.objects.all(),
636
+ help_text=SiteReceiver._meta.get_field("satellite_system").help_text,
637
+ label=SiteReceiver._meta.get_field("satellite_system").verbose_name,
638
+ required=True,
639
+ widget=SLMCheckboxSelectMultiple(columns=4),
640
+ empty_label=None,
641
+ )
642
+
643
+ receiver_type = ModelAutoComplete(
644
+ queryset=Receiver.objects.all(),
645
+ service_url=reverse_lazy("slm_public_api:receiver-list"),
646
+ help_text=SiteReceiver._meta.get_field("receiver_type").help_text,
647
+ label=SiteReceiver._meta.get_field("receiver_type").verbose_name,
648
+ search_param="model",
649
+ value_param="model",
650
+ label_param="model",
651
+ to_field_name="model",
652
+ )
653
+
654
+ temp_stabilized = forms.NullBooleanField(
655
+ help_text=SiteReceiver._meta.get_field("temp_stabilized").help_text,
656
+ label=SiteReceiver._meta.get_field("temp_stabilized").verbose_name,
657
+ widget=SLMNullBooleanSelect(),
658
+ )
659
+
660
+ def __init__(self, **kwargs):
661
+ super().__init__(**kwargs)
662
+ # todo why is this not automatically done?
663
+ if "satellite_system" in self.initial:
664
+ self.initial["satellite_system"] = [
665
+ system.name for system in self.initial["satellite_system"].all()
666
+ ]
667
+
668
+ class Meta(SubSectionForm):
669
+ model = SiteReceiver
670
+ fields = [*SubSectionForm.Meta.fields, *SiteReceiver.site_log_fields()]
671
+ field_classes = {"installed": SLMDateTimeField, "removed": SLMDateTimeField}
672
+
673
+
674
+ class SiteAntennaForm(SubSectionForm):
675
+ COPY_LAST_ON_ADD = [
676
+ field
677
+ for field in [*SubSectionForm.Meta.fields, *SiteAntenna.site_log_fields()]
678
+ if field not in {"installed", "removed", "additional_info"}
679
+ ]
680
+
681
+ marker_une = SLMPointField(
682
+ help_text=SiteAntenna._meta.get_field("marker_une").help_text,
683
+ label=SiteAntenna._meta.get_field("marker_une").verbose_name,
684
+ )
685
+
686
+ alignment = forms.FloatField(
687
+ required=SiteAntenna._meta.get_field("alignment").blank,
688
+ help_text=SiteAntenna._meta.get_field("alignment").help_text,
689
+ label=SiteAntenna._meta.get_field("alignment").verbose_name,
690
+ max_value=180,
691
+ min_value=-180,
692
+ )
693
+
694
+ antenna_type = ModelAutoComplete(
695
+ queryset=Antenna.objects.all(),
696
+ service_url=reverse_lazy("slm_public_api:antenna-list"),
697
+ help_text=SiteAntenna._meta.get_field("antenna_type").help_text,
698
+ label=SiteAntenna._meta.get_field("antenna_type").verbose_name,
699
+ search_param="model",
700
+ value_param="model",
701
+ label_param="model",
702
+ to_field_name="model",
703
+ )
704
+
705
+ radome_type = ModelAutoComplete(
706
+ queryset=Radome.objects.all(),
707
+ service_url=reverse_lazy("slm_public_api:radome-list"),
708
+ help_text=SiteAntenna._meta.get_field("radome_type").help_text,
709
+ label=SiteAntenna._meta.get_field("radome_type").verbose_name,
710
+ search_param="model",
711
+ value_param="model",
712
+ label_param="model",
713
+ to_field_name="model",
714
+ )
715
+
716
+ def __init__(self, *args, **kwargs):
717
+ super().__init__(*args, **kwargs)
718
+ # Check if the instance exists (i.e., it's an edit form, not a new form)
719
+ if self.instance and self.instance.pk:
720
+ self.fields["custom_graphic"].initial = self.instance.graphic
721
+
722
+ class Meta(SubSectionForm):
723
+ model = SiteAntenna
724
+ fields = [*SubSectionForm.Meta.fields, *SiteAntenna.site_log_fields()]
725
+ field_classes = {
726
+ "installed": SLMDateTimeField,
727
+ "removed": SLMDateTimeField,
728
+ "custom_graphic": SiteAntennaGraphicField,
729
+ }
730
+
731
+
732
+ class SiteSurveyedLocalTiesForm(SubSectionForm):
733
+ diff_xyz = SLMPointField(
734
+ help_text=SiteSurveyedLocalTies._meta.get_field("diff_xyz").help_text,
735
+ label=SiteSurveyedLocalTies._meta.get_field("diff_xyz").verbose_name,
736
+ )
737
+
738
+ class Meta(SubSectionForm.Meta):
739
+ model = SiteSurveyedLocalTies
740
+ fields = [*SubSectionForm.Meta.fields, *SiteSurveyedLocalTies.site_log_fields()]
741
+ field_classes = {"measured": SLMDateTimeField, "diff_xyz": SLMPointField}
742
+
743
+
744
+ class SiteFrequencyStandardForm(SubSectionForm):
745
+ class Meta(SubSectionForm.Meta):
746
+ model = SiteFrequencyStandard
747
+ fields = [*SubSectionForm.Meta.fields, *SiteFrequencyStandard.site_log_fields()]
748
+ widgets = {"effective_start": DatePicker, "effective_end": DatePicker}
749
+
750
+
751
+ class SiteCollocationForm(SubSectionForm):
752
+ class Meta(SubSectionForm.Meta):
753
+ model = SiteCollocation
754
+ fields = [*SubSectionForm.Meta.fields, *SiteCollocation.site_log_fields()]
755
+ widgets = {"effective_start": DatePicker, "effective_end": DatePicker}
756
+
757
+
758
+ class MeteorologicalForm(SubSectionForm):
759
+ NAV_HEADING = _("Meteorological Instr.")
760
+
761
+ class Meta(SubSectionForm):
762
+ fields = SubSectionForm.Meta.fields
763
+ widgets = {
764
+ "calibration": DatePicker,
765
+ "effective_start": DatePicker,
766
+ "effective_end": DatePicker,
767
+ }
768
+
769
+
770
+ class SiteHumiditySensorForm(MeteorologicalForm):
771
+ class Meta(MeteorologicalForm.Meta):
772
+ model = SiteHumiditySensor
773
+ fields = [
774
+ *MeteorologicalForm.Meta.fields,
775
+ *SiteHumiditySensor.site_log_fields(),
776
+ ]
777
+
778
+
779
+ class SitePressureSensorForm(MeteorologicalForm):
780
+ class Meta(MeteorologicalForm.Meta):
781
+ model = SitePressureSensor
782
+ fields = [
783
+ *MeteorologicalForm.Meta.fields,
784
+ *SitePressureSensor.site_log_fields(),
785
+ ]
786
+
787
+
788
+ class SiteTemperatureSensorForm(MeteorologicalForm):
789
+ class Meta(MeteorologicalForm.Meta):
790
+ model = SiteTemperatureSensor
791
+ fields = [
792
+ *MeteorologicalForm.Meta.fields,
793
+ *SiteTemperatureSensor.site_log_fields(),
794
+ ]
795
+
796
+
797
+ class SiteWaterVaporRadiometerForm(MeteorologicalForm):
798
+ class Meta(MeteorologicalForm.Meta):
799
+ model = SiteWaterVaporRadiometer
800
+ fields = [
801
+ *MeteorologicalForm.Meta.fields,
802
+ *SiteWaterVaporRadiometer.site_log_fields(),
803
+ ]
804
+
805
+
806
+ class SiteOtherInstrumentationForm(MeteorologicalForm):
807
+ class Meta(MeteorologicalForm.Meta):
808
+ model = SiteOtherInstrumentation
809
+ fields = [
810
+ *MeteorologicalForm.Meta.fields,
811
+ *SiteOtherInstrumentation.site_log_fields(),
812
+ ]
813
+
814
+
815
+ class LocalConditionForm(SubSectionForm):
816
+ NAV_HEADING = _("Local Conditions")
817
+
818
+ class Meta(SubSectionForm.Meta):
819
+ fields = SubSectionForm.Meta.fields
820
+ widgets = {"effective_start": DatePicker, "effective_end": DatePicker}
821
+
822
+
823
+ class SiteRadioInterferencesForm(LocalConditionForm):
824
+ class Meta(LocalConditionForm.Meta):
825
+ model = SiteRadioInterferences
826
+ fields = [
827
+ *LocalConditionForm.Meta.fields,
828
+ *SiteRadioInterferences.site_log_fields(),
829
+ ]
830
+
831
+
832
+ class SiteMultiPathSourcesForm(LocalConditionForm):
833
+ class Meta(LocalConditionForm.Meta):
834
+ model = SiteMultiPathSources
835
+ fields = [
836
+ *LocalConditionForm.Meta.fields,
837
+ *SiteMultiPathSources.site_log_fields(),
838
+ ]
839
+
840
+
841
+ class SiteSignalObstructionsForm(LocalConditionForm):
842
+ class Meta(LocalConditionForm.Meta):
843
+ model = SiteSignalObstructions
844
+ fields = [
845
+ *LocalConditionForm.Meta.fields,
846
+ *SiteSignalObstructions.site_log_fields(),
847
+ ]
848
+
849
+
850
+ class SiteLocalEpisodicEffectsForm(SubSectionForm):
851
+ class Meta(SubSectionForm.Meta):
852
+ model = SiteLocalEpisodicEffects
853
+ fields = [
854
+ *SubSectionForm.Meta.fields,
855
+ *SiteLocalEpisodicEffects.site_log_fields(),
856
+ ]
857
+ widgets = {"effective_start": DatePicker, "effective_end": DatePicker}
858
+
859
+
860
+ class AgencyPOCForm(SectionForm):
861
+ class Meta(SectionForm.Meta):
862
+ fields = SectionForm.Meta.fields
863
+ widgets = {
864
+ "agency": forms.Textarea(attrs={"rows": 2}),
865
+ "mailing_address": forms.Textarea(attrs={"rows": 4}),
866
+ }
867
+
868
+
869
+ class SiteOperationalContactForm(AgencyPOCForm):
870
+ class Meta(AgencyPOCForm.Meta):
871
+ model = SiteOperationalContact
872
+ fields = [*AgencyPOCForm.Meta.fields, *SiteOperationalContact.site_log_fields()]
873
+
874
+
875
+ class SiteResponsibleAgencyForm(AgencyPOCForm):
876
+ class Meta(AgencyPOCForm.Meta):
877
+ model = SiteResponsibleAgency
878
+ fields = [*AgencyPOCForm.Meta.fields, *SiteResponsibleAgency.site_log_fields()]
879
+
880
+
881
+ class SiteMoreInformationForm(SectionForm):
882
+ class Meta(SectionForm.Meta):
883
+ model = SiteMoreInformation
884
+ fields = [*SectionForm.Meta.fields, *SiteMoreInformation.site_log_fields()]
885
+
886
+
887
+ class UserForm(forms.ModelForm):
888
+ @property
889
+ def helper(self):
890
+ helper = FormHelper()
891
+ helper.form_id = "slm-user-form"
892
+ helper.layout = Layout(
893
+ Div(Div("email"), css_class="row"),
894
+ Div(
895
+ Div("first_name", css_class="col-6"),
896
+ Div("last_name", css_class="col-6"),
897
+ css_class="row",
898
+ ),
899
+ Div(
900
+ Div("silence_alerts", css_class="col-6"),
901
+ Div("html_emails", css_class="col-6"),
902
+ css_class="row",
903
+ ),
904
+ Div(Div("agencies"), css_class="row"),
905
+ )
906
+ return helper
907
+
908
+ agencies = forms.ModelMultipleChoiceField(
909
+ queryset=Agency.objects.all(), required=False, disabled=True
910
+ )
911
+
912
+ def __init__(self, *args, instance=None, **kwargs):
913
+ super().__init__(*args, instance=instance, **kwargs)
914
+ if instance:
915
+ self.fields["agencies"].queryset = instance.agencies.all()
916
+
917
+ class Meta:
918
+ model = UserSerializer.Meta.model
919
+ fields = UserSerializer.Meta.fields
920
+ exclude = ("date_joined", "profile")
921
+
922
+
923
+ class UserProfileForm(forms.ModelForm):
924
+ @property
925
+ def helper(self):
926
+ helper = FormHelper()
927
+ helper.form_id = "slm-user-profile-form"
928
+ helper.layout = Layout(
929
+ Div(
930
+ Div("phone1", css_class="col-6"),
931
+ Div("phone2", css_class="col-6"),
932
+ css_class="row",
933
+ ),
934
+ Div(Div("address1"), css_class="row"),
935
+ Div(Div("address2"), css_class="row"),
936
+ Div(Div("address3"), css_class="row"),
937
+ Div(
938
+ Div("city", css_class="col-6"),
939
+ Div("state_province", css_class="col-6"),
940
+ css_class="row",
941
+ ),
942
+ Div(
943
+ Div("country", css_class="col-6"),
944
+ Div("postal_code", css_class="col-6"),
945
+ css_class="row",
946
+ ),
947
+ )
948
+ return helper
949
+
950
+ class Meta:
951
+ model = UserProfileSerializer.Meta.model
952
+ fields = UserProfileSerializer.Meta.fields
953
+ exclude = ("registration_agency",)
954
+
955
+
956
+ class SiteFileForm(forms.ModelForm):
957
+ name = forms.SlugField(max_length=255, help_text=_("The name of the file."))
958
+
959
+ direction = EnumChoiceField(
960
+ CardinalDirection,
961
+ help_text=SiteFileUpload._meta.get_field("direction").help_text,
962
+ label=SiteFileUpload._meta.get_field("direction").verbose_name,
963
+ choices=[("", "-" * 10), *choices(CardinalDirection)],
964
+ )
965
+
966
+ def __init__(self, *args, instance=None, **kwargs):
967
+ super().__init__(*args, instance=instance, **kwargs)
968
+ if instance and instance.file_type != SLMFileType.SITE_IMAGE:
969
+ self.fields["direction"].widget = forms.HiddenInput()
970
+ self.fields["direction"].disabled = True
971
+
972
+ class Meta:
973
+ model = SiteFileUpload
974
+ fields = ["name", "description", "direction"]
975
+
976
+
977
+ class RichTextForm(forms.Form):
978
+ text = forms.CharField(widget=CKEditorWidget(config_name="richtextinput"))
979
+
980
+
981
+ class StationFilterForm(forms.Form):
982
+ name = forms.CharField(required=False)
983
+
984
+ status = EnumMultipleChoiceField(
985
+ SiteLogStatus,
986
+ # help_text=_('Include stations with these statuses.'),
987
+ label=_("Site Status"),
988
+ required=False,
989
+ )
990
+
991
+ alert = forms.MultipleChoiceField(
992
+ choices=(
993
+ (alert.__name__, alert._meta.verbose_name)
994
+ for alert in Alert.objects.site_alerts()
995
+ ),
996
+ widget=forms.CheckboxSelectMultiple(),
997
+ required=False,
998
+ # help_text=_('Include stations with the following alert types.'),
999
+ )
1000
+
1001
+ alert_level = EnumMultipleChoiceField(
1002
+ AlertLevel,
1003
+ # help_text=_('Include stations with alerts at this level.'),
1004
+ label=_("Alert Level"),
1005
+ required=False,
1006
+ )
1007
+
1008
+ station = ModelMultipleAutoComplete(
1009
+ queryset=Site.objects.public(),
1010
+ service_url=reverse_lazy("slm_public_api:name-list"),
1011
+ # help_text=Site._meta.get_field('name').help_text,
1012
+ # label=Site._meta.get_field('name').verbose_name,
1013
+ search_param="name",
1014
+ value_param="name",
1015
+ label_param="name",
1016
+ menu_class="station-names",
1017
+ to_field_name="name",
1018
+ required=False,
1019
+ )
1020
+
1021
+ satellite_system = forms.ModelMultipleChoiceField(
1022
+ queryset=SatelliteSystem.objects.all(),
1023
+ label=SiteReceiver._meta.get_field("satellite_system").verbose_name,
1024
+ required=False,
1025
+ widget=CheckboxSelectMultiple(),
1026
+ )
1027
+
1028
+ frequency_standard = EnumMultipleChoiceField(
1029
+ FrequencyStandardType, required=False, label=_("Frequency Standard")
1030
+ )
1031
+
1032
+ current = SLMBooleanField(
1033
+ label=_("Current"),
1034
+ help_text=_("Only include sites that currently have this equipment."),
1035
+ initial=True,
1036
+ required=False,
1037
+ )
1038
+
1039
+ receiver = ModelMultipleAutoComplete(
1040
+ queryset=Receiver.objects.all(),
1041
+ # help_text=_('Enter the name or abbreviation of an Agency.'),
1042
+ label=_("Receiver"),
1043
+ required=False,
1044
+ service_url=reverse_lazy("slm_public_api:receiver-list"),
1045
+ search_param="model",
1046
+ value_param="id",
1047
+ label_param="model",
1048
+ query_params={"in_use": True},
1049
+ )
1050
+
1051
+ antenna = ModelMultipleAutoComplete(
1052
+ queryset=Antenna.objects.all(),
1053
+ # help_text=_('Enter the name or abbreviation of an Agency.'),
1054
+ label=_("Antenna"),
1055
+ required=False,
1056
+ service_url=reverse_lazy("slm_public_api:antenna-list"),
1057
+ search_param="model",
1058
+ value_param="id",
1059
+ label_param="model",
1060
+ query_params={"in_use": True},
1061
+ )
1062
+
1063
+ radome = ModelMultipleAutoComplete(
1064
+ queryset=Radome.objects.all(),
1065
+ # help_text=_('Enter the name or abbreviation of an Agency.'),
1066
+ label=_("Radome"),
1067
+ required=False,
1068
+ service_url=reverse_lazy("slm_public_api:radome-list"),
1069
+ search_param="model",
1070
+ value_param="id",
1071
+ label_param="model",
1072
+ query_params={"in_use": True},
1073
+ )
1074
+
1075
+ agency = ModelMultipleAutoComplete(
1076
+ queryset=Agency.objects.all(),
1077
+ # help_text=_('Enter the name or abbreviation of an Agency.'),
1078
+ label=_("Agency"),
1079
+ required=False,
1080
+ service_url=reverse_lazy("slm_edit_api:agency-list"),
1081
+ search_param="search",
1082
+ value_param="id",
1083
+ label_param="name",
1084
+ render_suggestion=(
1085
+ 'return `<span class="matchable">(${obj.shortname})</span>'
1086
+ '<span class="matchable">${obj.name}</span>`;'
1087
+ ),
1088
+ )
1089
+
1090
+ network = ModelMultipleAutoComplete(
1091
+ queryset=Network.objects.all(),
1092
+ # help_text=_('Enter the name of an IGS Network.'),
1093
+ label=_("Network"),
1094
+ required=False,
1095
+ service_url=reverse_lazy("slm_edit_api:network-list"),
1096
+ search_param="name",
1097
+ value_param="id",
1098
+ label_param="name",
1099
+ )
1100
+
1101
+ country = MultiSelectEnumAutoComplete(
1102
+ # help_text=_('Enter the name of a country or region.'),
1103
+ ISOCountry,
1104
+ label=_("Country/Region"),
1105
+ required=False,
1106
+ render_suggestion=(
1107
+ 'return `<span class="fi fi-${obj.value.toLowerCase()}"></span>'
1108
+ '<span class="matchable">${obj.label}</span>`;'
1109
+ ),
1110
+ data_source=ISOCountry.with_stations,
1111
+ )
1112
+
1113
+ geography = PolylineListField(
1114
+ required=False,
1115
+ label=_("Geographic Bounds"),
1116
+ help_text=_(
1117
+ "Bounding boxes should be line strings holding (longitude, "
1118
+ "latitude) tuples encoded using Google's polyline algorithm."
1119
+ ),
1120
+ )
1121
+
1122
+ def clean_current(self):
1123
+ # todo mixin that does this
1124
+ if self["current"].html_name not in self.data:
1125
+ return self.fields["current"].initial
1126
+ return self.cleaned_data["current"]