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/models/sitelog.py ADDED
@@ -0,0 +1,4279 @@
1
+ import json
2
+ from collections import namedtuple
3
+ from datetime import datetime, timezone
4
+ from enum import Enum
5
+ from functools import lru_cache
6
+
7
+ from django.conf import settings
8
+ from django.contrib.auth import get_user_model
9
+ from django.contrib.auth.models import Permission
10
+ from django.contrib.gis.db import models as gis_models
11
+ from django.core.exceptions import FieldDoesNotExist, ValidationError
12
+ from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
13
+ from django.db import models, transaction
14
+ from django.db.models import (
15
+ CheckConstraint,
16
+ ExpressionWrapper,
17
+ F,
18
+ Max,
19
+ OuterRef,
20
+ Q,
21
+ Subquery,
22
+ Value,
23
+ )
24
+ from django.db.models.functions import (
25
+ Cast,
26
+ Coalesce,
27
+ Concat,
28
+ ExtractDay,
29
+ ExtractMonth,
30
+ ExtractYear,
31
+ Greatest,
32
+ Lower,
33
+ LPad,
34
+ Now,
35
+ Substr,
36
+ )
37
+ from django.utils.functional import cached_property, classproperty
38
+ from django.utils.timezone import now
39
+ from django.utils.translation import gettext as _
40
+ from django_enum import EnumField
41
+
42
+ from slm import signals as slm_signals
43
+ from slm.defines import (
44
+ AlertLevel,
45
+ AntennaReferencePoint,
46
+ Aspiration,
47
+ CollocationStatus,
48
+ FractureSpacing,
49
+ FrequencyStandardType,
50
+ ISOCountry,
51
+ RinexVersion,
52
+ SiteLogFormat,
53
+ SiteLogStatus,
54
+ TectonicPlates,
55
+ )
56
+ from slm.utils import date_to_str
57
+ from slm.validators import get_validators
58
+
59
+
60
+ def utc_now_date():
61
+ return datetime.now(timezone.utc).date()
62
+
63
+
64
+ # a named tuple used as meta information to dynamically determine what the
65
+ # section models are and how to access them from the Site model
66
+ Section = namedtuple(
67
+ "Section",
68
+ [
69
+ "field", # the name of the section for database queries
70
+ "accessor", # the section manager attribute on Site instances
71
+ "cls", # the section's python model class
72
+ "subsection", # true if this is a subsection (i.e. multiple instances)
73
+ ],
74
+ )
75
+
76
+
77
+ class SubquerySum(Subquery):
78
+ """
79
+ The django ORM is still really clunky around aggregating subqueries.
80
+ https://code.djangoproject.com/ticket/28296
81
+ """
82
+
83
+ output_field = models.IntegerField()
84
+
85
+ def __init__(self, *args, **kwargs):
86
+ self.template = (
87
+ f'(SELECT SUM({kwargs.pop("field")}) ' f"FROM (%(subquery)s) _sum)"
88
+ )
89
+ super().__init__(*args, **kwargs)
90
+
91
+
92
+ def bool_condition(*args, **kwargs):
93
+ return ExpressionWrapper(Q(*args, **kwargs), output_field=models.BooleanField())
94
+
95
+
96
+ class DefaultToStrEncoder(json.JSONEncoder):
97
+ def __init__(self, *args, **kwargs):
98
+ super().__init__(*args, **kwargs)
99
+
100
+ def default(self, obj):
101
+ return str(obj)
102
+
103
+
104
+ class SiteManager(models.Manager):
105
+ pass
106
+
107
+
108
+ class SiteQuerySet(models.QuerySet):
109
+ """
110
+ A custom queryset for the Site model that adds some useful methods for
111
+ common annotations. Information about sites is spread across a large number
112
+ of tables, and it is often useful to annotate a queryset with information
113
+ from those tables. This queryset provides a few methods to do that.
114
+ """
115
+
116
+ def annotate_files(self, log_format=SiteLogFormat.LEGACY, prefix=None):
117
+ from slm.models import ArchivedSiteLog
118
+
119
+ latest_archive = ArchivedSiteLog.objects.filter(
120
+ Q(site=OuterRef("pk")) & Q(log_format=log_format)
121
+ ).order_by("-index__begin")
122
+ size_field = f"{log_format.ext if prefix is None else prefix}_size"
123
+ file_field = f"{log_format.ext if prefix is None else prefix}_file"
124
+ return self.annotate(
125
+ **{
126
+ size_field: Subquery(latest_archive.values("size")[:1]),
127
+ file_field: Subquery(latest_archive.values("pk")[:1]),
128
+ }
129
+ )
130
+
131
+ def annotate_filenames(
132
+ self, published=True, name_len=None, field_name="filename", lower_case=False
133
+ ):
134
+ """
135
+ Add the log names (w/o) extension as a property called filename to
136
+ each site.
137
+ :param published: If true (default) annotate with the filename for the
138
+ most recently published version of the log. If false, will generate
139
+ a filename for the HEAD version of the log whether published or in
140
+ moderation.
141
+ :param name_len: If given a number, the filename will start with only
142
+ the first name_len characters of the site name.
143
+ :param field_name: Change the name of the annotated field.
144
+ :param lower_case: Filenames will be lowercase if true.
145
+ :return: A queryset with the filename annotation added.
146
+ """
147
+ name_str = F("name")
148
+ if name_len:
149
+ name_str = Cast(Substr("name", 1, length=name_len), models.CharField())
150
+
151
+ if lower_case:
152
+ name_str = Lower(name_str)
153
+
154
+ form = SiteForm.objects.filter(Q(site=OuterRef("pk")) & Q(published=published))
155
+
156
+ return self.annotate(
157
+ date_prepared=Subquery(form.values("date_prepared")[:1]),
158
+ **{
159
+ field_name: Concat(
160
+ name_str,
161
+ Value("_"),
162
+ Cast(ExtractYear("date_prepared"), models.CharField()),
163
+ LPad(
164
+ Cast(ExtractMonth("date_prepared"), models.CharField()),
165
+ 2,
166
+ fill_text=Value("0"),
167
+ ),
168
+ LPad(
169
+ Cast(ExtractDay("date_prepared"), models.CharField()),
170
+ 2,
171
+ fill_text=Value("0"),
172
+ ),
173
+ )
174
+ },
175
+ )
176
+
177
+ def active(self):
178
+ """
179
+ Active stations include all stations that are public and not former or
180
+ suspended.
181
+
182
+ :return:
183
+ """
184
+ return self.public().filter(
185
+ ~Q(
186
+ status__in=[
187
+ SiteLogStatus.PROPOSED,
188
+ SiteLogStatus.FORMER,
189
+ SiteLogStatus.SUSPENDED,
190
+ ]
191
+ )
192
+ )
193
+
194
+ def public(self):
195
+ """
196
+ Return all publicly visible sites. This includes sites that are
197
+ in non-active states (i.e. proposed, former, suspended).
198
+ :return:
199
+ """
200
+ from slm.models import Agency, Network
201
+
202
+ public = Site.objects.filter(
203
+ (
204
+ Q(agencies__in=Agency.objects.filter(public=True))
205
+ | Q(agencies__isnull=True)
206
+ )
207
+ & (
208
+ Q(networks__in=Network.objects.filter(public=True))
209
+ | Q(networks__isnull=True)
210
+ )
211
+ &
212
+ # must have been published at least once! - even if in proposed
213
+ # state
214
+ Q(last_publish__isnull=False)
215
+ ).distinct()
216
+ return self.filter(pk__in=public)
217
+
218
+ def editable_by(self, user):
219
+ """
220
+ Return the list of sites that should be visible in the editor to the
221
+ given user.
222
+
223
+ :param user: The user model.
224
+ :return: A queryset with all sites un-editable by the user filtered
225
+ out.
226
+ """
227
+ if user.is_authenticated:
228
+ if user.is_superuser:
229
+ return self
230
+ return self.filter(agencies__in=user.agencies.all())
231
+ return self.none()
232
+
233
+ def moderated(self, user):
234
+ if user.is_authenticated:
235
+ if user.is_superuser:
236
+ return self.all()
237
+ elif user.is_moderator():
238
+ return self.filter(agencies__in=user.agencies.all()).distinct()
239
+ return self.none()
240
+
241
+ def update_alert_levels(self):
242
+ """
243
+ Update the denormalized max alert level for sites in this queryset to
244
+ reflect their active alerts.
245
+
246
+ return: calling queryset for chaining
247
+ """
248
+ from slm.models import Alert
249
+
250
+ max_alert = Alert.objects.for_site(OuterRef("pk")).order_by("-level")
251
+ self.annotate(_max_alert=Subquery(max_alert.values("level")[:1])).update(
252
+ max_alert=F("_max_alert")
253
+ )
254
+ return self
255
+
256
+ def needs_publish(self):
257
+ qry = self
258
+ mod_q = Q()
259
+ for idx, section in enumerate(self.model.sections()):
260
+ mod_qry = section.cls.objects._current(
261
+ published=False, filter=Q(site=OuterRef("pk"))
262
+ )
263
+ qry = qry.annotate(**{f"_mod{idx}": Subquery(mod_qry.values("pk")[:1])})
264
+ mod_q |= Q(**{f"_mod{idx}__isnull": False})
265
+ return qry.filter(mod_q).exists()
266
+
267
+ def synchronize_denormalized_state(self, skip_form_updates=False):
268
+ """
269
+ Some state is denormalized and cached onto site records to speed up
270
+ reads. This ensures this denormalized state
271
+ (max_alert, num_flags, status, and some site form fields) accurately
272
+ reflect the normal data.
273
+ :param skip_form_updates: If true do not update the forms section
274
+ with modified section info.
275
+ :return:
276
+ """
277
+ self.update_alert_levels()
278
+
279
+ aggregate = None
280
+ qry = self
281
+ mod_q = Q()
282
+ for idx, section in enumerate(self.model.sections()):
283
+ # head query - exclude deleted
284
+ head_qry = section.cls.objects._current(
285
+ published=None, include_deleted=False, filter=Q(site=OuterRef("pk"))
286
+ )
287
+
288
+ mod_qry = section.cls.objects._current(
289
+ published=False, filter=Q(site=OuterRef("pk"))
290
+ )
291
+ qry = qry.annotate(
292
+ **{
293
+ f"_num_flags{idx}": SubquerySum(
294
+ head_qry.values("num_flags"), field="num_flags"
295
+ ),
296
+ f"_mod{idx}": Subquery(mod_qry.values("pk")[:1]),
297
+ }
298
+ )
299
+ mod_q |= Q(**{f"_mod{idx}__isnull": False})
300
+ if aggregate is None:
301
+ aggregate = Coalesce(f"_num_flags{idx}", 0)
302
+ else:
303
+ aggregate += Coalesce(f"_num_flags{idx}", 0)
304
+
305
+ qry.order_by().annotate(_num_flags=aggregate).update(
306
+ num_flags=Coalesce("_num_flags", 0)
307
+ )
308
+
309
+ qry.filter(
310
+ Q(status__in=[SiteLogStatus.UPDATED, SiteLogStatus.PUBLISHED])
311
+ ).filter(mod_q).update(status=SiteLogStatus.UPDATED)
312
+
313
+ exists_q = Q()
314
+ for required_section in getattr(
315
+ settings, "SLM_REQUIRED_SECTIONS_TO_PUBLISH", []
316
+ ):
317
+ exists_q &= Q(**{f"{required_section}__isnull": False})
318
+ qry.filter(
319
+ Q(
320
+ status__in=[
321
+ SiteLogStatus.UPDATED,
322
+ SiteLogStatus.PUBLISHED,
323
+ # SiteLogStatus.PROPOSED,
324
+ ]
325
+ ) # do not allow PROPOSED to be transitioned to PUBLISHED without explicit top level change
326
+ & exists_q
327
+ ).filter(~mod_q).update(status=SiteLogStatus.PUBLISHED)
328
+
329
+ # this is the longest operation - there might be a way to squash it
330
+ # into a single query
331
+ if not skip_form_updates:
332
+ for site in qry.filter(mod_q):
333
+ form = site.siteform_set.head()
334
+ if form.published:
335
+ form.pk = None
336
+ form.published = False
337
+ form.save()
338
+
339
+ form.modified_section = ", ".join(site.modified_sections)
340
+ if site.last_publish and form.report_type == "NEW":
341
+ form.report_type = "UPDATE"
342
+ form.save()
343
+
344
+ def availability(self):
345
+ from slm.models import DataAvailability
346
+
347
+ last_data_avail = DataAvailability.objects.filter(site=OuterRef("pk")).order_by(
348
+ "-last"
349
+ )
350
+ return self.annotate(
351
+ last_data_time=Subquery(last_data_avail.values("last")[:1]),
352
+ last_data=Now() - F("last_data_time"),
353
+ last_rinex2=Subquery(
354
+ last_data_avail.filter(RinexVersion(2).major_q()).values("last")[:1]
355
+ ),
356
+ last_rinex3=Subquery(
357
+ last_data_avail.filter(RinexVersion(3).major_q()).values("last")[:1]
358
+ ),
359
+ last_rinex4=Subquery(
360
+ last_data_avail.filter(RinexVersion(4).major_q()).values("last")[:1]
361
+ ),
362
+ )
363
+
364
+ def with_last_info(self):
365
+ """
366
+ Adds a datetime field called last_info which should reflect the last
367
+ time there was any notable public information update for the site. For
368
+ instance this is useful for search engine indexing.
369
+ :return: queryset with a last_info field added
370
+ """
371
+ from slm.models import DataAvailability
372
+
373
+ last_data_avail = DataAvailability.objects.filter(site=OuterRef("pk")).order_by(
374
+ F("last").desc(nulls_last=True)
375
+ )
376
+ return self.annotate(
377
+ last_info=Greatest(
378
+ Subquery(last_data_avail.values("last")[:1]),
379
+ "last_publish",
380
+ output_field=models.DateTimeField(),
381
+ )
382
+ )
383
+
384
+ def with_identification_fields(self, *fields, **renamed_fields):
385
+ """
386
+ Annotate the given identification fields valid now onto the site
387
+ objects in this queryset.
388
+
389
+ :param fields: The names of the fields to annotate, by default these
390
+ will include: iers_domes_number, cdp_number
391
+ :param renamed_fields: Named arguments where the names of the arguments
392
+ are the field accessors for the fields to annotate and the values
393
+ are the names to use for the annotated fields.
394
+ :return: The queryset with the annotations made
395
+ """
396
+ fields = {
397
+ **{field: field for field in fields},
398
+ **{field: name for field, name in renamed_fields.items()},
399
+ }
400
+
401
+ fields = fields or {
402
+ **{field: field for field in ["iers_domes_number", "cdp_number"]}
403
+ }
404
+
405
+ identification = SiteIdentification.objects.filter(
406
+ Q(site=OuterRef("pk")) & Q(published=True)
407
+ )
408
+ return self.annotate(
409
+ **{
410
+ name: Subquery(identification.values(field)[:1])
411
+ for field, name in fields.items()
412
+ }
413
+ )
414
+
415
+ def with_location_fields(self, *fields, **renamed_fields):
416
+ """
417
+ Annotate the given location fields valid now or at the given epoch
418
+ onto the site objects in this queryset.
419
+
420
+ :param fields: The names of the fields to annotate, by default these
421
+ will include: XYZ, LLH, city, state, and
422
+ country
423
+ :param renamed_fields: Named arguments where the names of the arguments
424
+ are the field accessors for the fields to annotate and the values
425
+ are the names to use for the annotated fields.
426
+ :return: The queryset with the annotations made
427
+ """
428
+ fields = {
429
+ **{field: field for field in fields},
430
+ **{field: name for field, name in renamed_fields.items()},
431
+ }
432
+
433
+ fields = fields or {
434
+ **{field: field for field in ["xyz", "llh", "city", "state", "country"]}
435
+ }
436
+
437
+ location = SiteLocation.objects.filter(
438
+ Q(site=OuterRef("pk")) & Q(published=True)
439
+ )
440
+ return self.annotate(
441
+ **{
442
+ name: Subquery(location.values(field)[:1])
443
+ for field, name in fields.items()
444
+ }
445
+ )
446
+
447
+ def with_receiver_fields(self, *fields, epoch=None, **renamed_fields):
448
+ """
449
+ Annotate the given receiver fields valid now or at the given epoch
450
+ onto the site objects in this queryset. The field names should be
451
+ the Django ORM field accessor names. For instance the receiver model
452
+ type would be: receiver_type__model. To use a different name than the
453
+ accessor for the annotated field you may pass the fields renamed as
454
+ keyword arguments. For instance to rename serial_number to
455
+ receiver_serial_number you would pass:
456
+
457
+ with_receiver_fields(serial_number='receiver_serial_number')
458
+
459
+ :param fields: The names of the fields to annotate, by default these
460
+ will include:
461
+ receiver_type__model -> receiver,
462
+ serial_number -> receiver_sn
463
+ firmware -> receiver_firmware
464
+ :param epoch: The point in time at which the receiver information
465
+ should be valid for, default is now
466
+ :param renamed_fields: Named arguments where the names of the arguments
467
+ are the field accessors for the fields to annotate and the values
468
+ are the names to use for the annotated fields.
469
+ :return: The queryset with the annotations made
470
+ """
471
+ fields = {
472
+ **{field: field for field in fields},
473
+ **{field: name for field, name in renamed_fields.items()},
474
+ }
475
+
476
+ fields = fields or {
477
+ "receiver_type__model": "receiver",
478
+ "serial_number": "receiver_sn",
479
+ "firmware": "receiver_firmware",
480
+ }
481
+
482
+ epoch_q = (
483
+ Q()
484
+ if epoch is None
485
+ else (
486
+ (Q(installed__lte=epoch) | Q(installed__isnull=True))
487
+ & (Q(removed__gt=epoch) | Q(removed__isnull=True))
488
+ )
489
+ )
490
+ receiver = SiteReceiver.objects.filter(
491
+ Q(site=OuterRef("pk")) & Q(published=True) & epoch_q
492
+ ).order_by("-installed")
493
+
494
+ return self.annotate(
495
+ **{
496
+ name: Subquery(receiver.values(field)[:1])
497
+ for field, name in fields.items()
498
+ }
499
+ )
500
+
501
+ def with_antenna_fields(self, *fields, epoch=None, **renamed_fields):
502
+ """
503
+ Annotate the given antenna fields valid now or at the given epoch
504
+ onto the site objects in this queryset. The field names should be
505
+ the Django ORM field accessor names. For instance the receiver model
506
+ type would be: receiver_type__model. To use a different name than the
507
+ accessor for the annotated field you may pass the fields renamed as
508
+ keyword arguments. For instance to rename serial_number to
509
+ antenna_sn you would pass:
510
+
511
+ with_antenna_fields(serial_number='antenna_sn')
512
+
513
+ The antenna calibration method is available as the field 'antcal'.
514
+
515
+ :param fields: The names of the fields to annotate, by default these
516
+ will include:
517
+ antenna_type__model -> antenna,
518
+ radome_type__model -> radome,
519
+ antcal -> antcal
520
+
521
+ :param epoch: The point in time at which the receiver information
522
+ should be valid for, default is now
523
+ :param renamed_fields: Named arguments where the names of the arguments
524
+ are the field accessors for the fields to annotate and the values
525
+ are the names to use for the annotated fields.
526
+ :return: The queryset with the annotations made
527
+ """
528
+ from slm.models import AntCal
529
+
530
+ fields = {
531
+ **{field: field for field in fields},
532
+ **{field: name for field, name in renamed_fields.items()},
533
+ }
534
+
535
+ fields = fields or {
536
+ "antenna_type__model": "antenna",
537
+ "radome_type__model": "radome",
538
+ "antcal": "antcal",
539
+ }
540
+
541
+ epoch_q = (
542
+ Q()
543
+ if epoch is None
544
+ else (
545
+ (Q(installed__lte=epoch) | Q(installed__isnull=True))
546
+ & (Q(removed__gt=epoch) | Q(removed__isnull=True))
547
+ )
548
+ )
549
+
550
+ antenna = SiteAntenna.objects.filter(
551
+ Q(site=OuterRef("pk")) & Q(published=True) & epoch_q
552
+ ).order_by("-installed")
553
+ if "antcal" in fields:
554
+ antenna = antenna.annotate(
555
+ antcal=Subquery(
556
+ AntCal.objects.filter(
557
+ Q(antenna=OuterRef("antenna_type"))
558
+ & Q(radome=OuterRef("radome_type"))
559
+ ).values("method")[:1]
560
+ )
561
+ )
562
+
563
+ return self.annotate(
564
+ **{
565
+ name: Subquery(antenna.values(field)[:1])
566
+ for field, name in fields.items()
567
+ }
568
+ )
569
+
570
+ def with_frequency_standard_fields(self, *fields, epoch=None, **renamed_fields):
571
+ """
572
+ Annotate the given frequency standard fields valid now or at the given
573
+ epoch onto the site objects in this queryset. The field names should be
574
+ the Django ORM field accessor names.
575
+
576
+ :param fields: The names of the fields to annotate, by default these
577
+ will include:
578
+ standard_type -> clock
579
+ :param epoch: The point in time at which the receiver information
580
+ should be valid for, default is now
581
+ :param renamed_fields: Named arguments where the names of the arguments
582
+ are the field accessors for the fields to annotate and the values
583
+ are the names to use for the annotated fields.
584
+ :return: The queryset with the annotations made
585
+ """
586
+ fields = {
587
+ **{field: field for field in fields},
588
+ **{field: name for field, name in renamed_fields.items()},
589
+ }
590
+
591
+ fields = fields or {"standard_type": "clock"}
592
+
593
+ epoch_q = (
594
+ Q()
595
+ if epoch is None
596
+ else (
597
+ (Q(effective_start__lte=epoch) | Q(effective_start__isnull=True))
598
+ & (Q(effective_end__gt=epoch) | Q(effective_end__isnull=True))
599
+ )
600
+ )
601
+ freq = SiteFrequencyStandard.objects.filter(
602
+ Q(site=OuterRef("pk")) & Q(published=True) & epoch_q
603
+ ).order_by("-effective_start")
604
+
605
+ return self.annotate(
606
+ **{name: Subquery(freq.values(field)[:1]) for field, name in fields.items()}
607
+ )
608
+
609
+ def with_info_fields(self, *fields, **renamed_fields):
610
+ """
611
+ Annotate the given identification fields valid now onto the site
612
+ objects in this queryset.
613
+
614
+ :param fields: The names of the fields to annotate, by default these
615
+ will include: iers_domes_number, cdp_number
616
+ :param renamed_fields: Named arguments where the names of the arguments
617
+ are the field accessors for the fields to annotate and the values
618
+ are the names to use for the annotated fields.
619
+ :return: The queryset with the annotations made
620
+ """
621
+ fields = {
622
+ **{field: field for field in fields},
623
+ **{field: name for field, name in renamed_fields.items()},
624
+ }
625
+
626
+ fields = fields or {
627
+ "primary": "primary_datacenter",
628
+ "secondary": "secondary_datacenter",
629
+ }
630
+
631
+ more_info = SiteMoreInformation.objects.filter(
632
+ Q(site=OuterRef("pk")) & Q(published=True)
633
+ )
634
+ return self.annotate(
635
+ **{
636
+ name: Subquery(more_info.values(field)[:1])
637
+ for field, name in fields.items()
638
+ }
639
+ )
640
+
641
+
642
+ class Site(models.Model):
643
+ """
644
+ XXXX Site Information Form (site log)
645
+ International GNSS Service
646
+ See Instructions at:
647
+ https://files.igs.org/pub/station/general/sitelog_instr.txt
648
+ """
649
+
650
+ # API_RELATED_FIELD = 'name'
651
+
652
+ objects = SiteManager.from_queryset(SiteQuerySet)()
653
+
654
+ name = models.CharField(
655
+ max_length=9,
656
+ unique=True,
657
+ help_text=_(
658
+ "This is the 9 Character station name (XXXXMRCCC) used in RINEX 3 "
659
+ "filenames Format: (XXXX - existing four character IGS station "
660
+ "name, M - Monument or marker number (0-9), R - Receiver number "
661
+ "(0-9), CCC - Three digit ISO 3166-1 country code)"
662
+ ),
663
+ db_index=True,
664
+ validators=[RegexValidator(r"[\w]{4}[\d]{2}[\w]{3}")],
665
+ )
666
+
667
+ # todo can site exist without agency?
668
+ agencies = models.ManyToManyField("slm.Agency", related_name="sites")
669
+
670
+ # dormant is now deduplicated into status field
671
+ status = EnumField(
672
+ SiteLogStatus,
673
+ default=SiteLogStatus.PROPOSED,
674
+ blank=True,
675
+ help_text=_("The current status of the site."),
676
+ db_index=True,
677
+ )
678
+
679
+ owner = models.ForeignKey(
680
+ "slm.User", null=True, default=None, blank=True, on_delete=models.SET_NULL
681
+ )
682
+
683
+ # Denormalized data ###########################
684
+ # These fields are cached onto the site table to speed up lookups, issues
685
+ # can arise if they get out of synch with the data
686
+ num_flags = models.PositiveSmallIntegerField(
687
+ default=0,
688
+ blank=True,
689
+ help_text=_("The number of flags the most recent site log version has."),
690
+ db_index=True,
691
+ )
692
+
693
+ max_alert = EnumField(
694
+ AlertLevel,
695
+ default=None,
696
+ blank=True,
697
+ null=True,
698
+ help_text=_("The number of flags the most recent site log version has."),
699
+ db_index=True,
700
+ )
701
+ ##############################################
702
+
703
+ # todo deprecated
704
+ preferred = models.IntegerField(default=0, blank=True)
705
+ modified_user = models.IntegerField(default=0, blank=True)
706
+ #######
707
+
708
+ created = models.DateTimeField(
709
+ auto_now_add=True,
710
+ blank=True,
711
+ null=True,
712
+ help_text=_("The time this site was first registered."),
713
+ db_index=True,
714
+ )
715
+
716
+ join_date = models.DateField(
717
+ blank=True,
718
+ null=True,
719
+ help_text=_("The date this site was first published."),
720
+ db_index=True,
721
+ )
722
+
723
+ # todo, normalize onto log join
724
+ last_user = models.ForeignKey(
725
+ "slm.User",
726
+ null=True,
727
+ default=None,
728
+ blank=True,
729
+ on_delete=models.SET_NULL,
730
+ related_name="recent_sites",
731
+ help_text=_("The last user to make edits to the site log."),
732
+ )
733
+ ######################################
734
+
735
+ last_publish = models.DateTimeField(
736
+ null=True,
737
+ blank=True,
738
+ default=None,
739
+ help_text=_("The publish date of the current log file."),
740
+ db_index=True,
741
+ )
742
+
743
+ last_update = models.DateTimeField(
744
+ null=True,
745
+ blank=True,
746
+ default=None,
747
+ help_text=_("The time of the most recent update to the site log."),
748
+ db_index=True,
749
+ )
750
+
751
+ def needs_publish(self):
752
+ if self.status in [SiteLogStatus.PROPOSED, SiteLogStatus.UPDATED]:
753
+ return True
754
+ elif self.status == SiteLogStatus.PUBLISHED:
755
+ return False
756
+ return self.__class__.objects.filter(pk=self.pk).needs_publish()
757
+
758
+ @lru_cache(maxsize=32)
759
+ def is_moderator(self, user):
760
+ if user:
761
+ if user.is_superuser:
762
+ return True
763
+ return self.moderators.filter(pk=user.pk).exists()
764
+ return False
765
+
766
+ def get_filename(self, log_format, epoch=None, name_len=None, lower_case=False):
767
+ """
768
+ Get the filename (including extension) to be used for the rendered
769
+ site log given the parameters.
770
+
771
+ :param log_format: The SiteLogFormat of the rendered log
772
+ :param epoch: The date (or datetime) when the site log was valid. If
773
+ not given the last published date will be used, then the time of the
774
+ last_update, and ultimately if the first two are null the created time
775
+ will be used.
776
+ :param name_len: The number of characters from the site log name to use
777
+ as the prefix. (default: 9 - all of them)
778
+ :param lower_case: True if the filename should be lower case.
779
+ (default False)
780
+ :return: The filename including extension.
781
+ """
782
+ if epoch is None:
783
+ epoch = self.last_publish or self.last_update or self.created
784
+ if name_len is None and log_format is SiteLogFormat.LEGACY:
785
+ name_len = 4
786
+ name = self.name[:name_len]
787
+ return (
788
+ f"{name.lower() if lower_case else name.upper()}_"
789
+ f"{epoch.year}{epoch.month:02}"
790
+ f"{epoch.day:02}.{log_format.ext}"
791
+ )
792
+
793
+ def refresh_from_db(self, **kwargs):
794
+ if hasattr(self, "_max_alert"):
795
+ del self._max_alert
796
+ return super().refresh_from_db(**kwargs)
797
+
798
+ @classproperty
799
+ def alert_fields(cls):
800
+ from slm.models import Alert
801
+
802
+ return [
803
+ field.get_accessor_name()
804
+ for field in cls._meta.related_objects
805
+ if issubclass(field.related_model, Alert)
806
+ ]
807
+
808
+ @cached_property
809
+ def moderators(self):
810
+ """
811
+ Get the users who have moderate permission for this site. Moderators
812
+ are also editors, but are not listed in editors
813
+
814
+ :return: A queryset containing users with moderate permission for the
815
+ site
816
+ """
817
+ perm = Permission.objects.get_by_natural_key("moderate_sites", "slm", "user")
818
+ return (
819
+ get_user_model()
820
+ .objects.filter(
821
+ Q(is_superuser=True)
822
+ | (
823
+ Q(agencies__in=self.agencies.all())
824
+ & (Q(groups__permissions=perm) | Q(user_permissions=perm))
825
+ )
826
+ )
827
+ .distinct()
828
+ )
829
+
830
+ @cached_property
831
+ def editors(self):
832
+ """
833
+ Get the users who have edit permission for this site. This may include
834
+ moderators who are part of the same agency.
835
+
836
+ :return: A queryset containing users with edit permissions for the site
837
+ """
838
+ qry = Q(agencies__in=self.agencies.all())
839
+ if self.owner:
840
+ qry |= Q(pk=self.owner.pk)
841
+ return get_user_model().objects.filter(qry)
842
+
843
+ @classmethod
844
+ def sections(cls):
845
+ if hasattr(cls, "sections_"):
846
+ return cls.sections_
847
+
848
+ cls.sections_ = [
849
+ Section(
850
+ field=section.name,
851
+ accessor=section.get_accessor_name(),
852
+ cls=section.related_model,
853
+ subsection=SiteSubSection in section.related_model.__mro__,
854
+ )
855
+ for section in Site._meta.get_fields()
856
+ if section.related_model and (SiteSection in section.related_model.__mro__)
857
+ ]
858
+ return cls.sections_
859
+
860
+ def is_publishable(self):
861
+ has_required_sections = Q(id=self.id)
862
+ for required_section in getattr(
863
+ settings, "SLM_REQUIRED_SECTIONS_TO_PUBLISH", []
864
+ ):
865
+ has_required_sections &= Q(**{f"{required_section}__isnull": False})
866
+ return bool(Site.objects.filter(has_required_sections).count())
867
+
868
+ def can_publish(self, user):
869
+ """
870
+ This is a future hook to use for instances where non-moderators are
871
+ allowed to publish a site log under certain conditions.
872
+
873
+ :param user:
874
+ :return:
875
+ """
876
+ if user:
877
+ # cannot publish without these minimum sectons
878
+ has_required_sections = Q(id=self.id)
879
+ for required_section in getattr(
880
+ settings, "SLM_REQUIRED_SECTIONS_TO_PUBLISH", []
881
+ ):
882
+ has_required_sections &= Q(**{f"{required_section}__isnull": False})
883
+ return self.is_moderator(user) and self.is_publishable()
884
+ return False
885
+
886
+ def can_edit(self, user):
887
+ if user and user.is_authenticated:
888
+ if self.is_moderator(user) or self.owner == user:
889
+ return True
890
+ return user.agencies.filter(pk__in=self.agencies.all()).count() > 0
891
+ return False
892
+
893
+ def update_status(self, save=True, user=None, timestamp=None, first_publish=False):
894
+ """
895
+ Update the denormalized data that is too expensive to query on the
896
+ fly. This includes flag count, moderation status and DateTimes. Also
897
+ check for and delete any review requests if a publish was done.
898
+
899
+ :param save:
900
+ :param user: The user responsible for a status update check
901
+ :param timestamp: The time at which the status update is triggered
902
+ :return:
903
+ """
904
+ if not timestamp:
905
+ timestamp = now()
906
+
907
+ self.last_update = timestamp
908
+ if first_publish:
909
+ self.last_publish = timestamp
910
+
911
+ if user:
912
+ self.last_user = user
913
+
914
+ status = self.status
915
+ self.synchronize()
916
+
917
+ # if in either of these two states - status update must come from
918
+ # a global publish of the site log, not from this which can be
919
+ # triggered by a section publish
920
+ if first_publish or (
921
+ status != self.status and self.status == SiteLogStatus.PUBLISHED
922
+ ):
923
+ self.last_publish = timestamp
924
+ self.status = SiteLogStatus.PUBLISHED
925
+ if hasattr(self, "review_request"):
926
+ self.review_request.delete()
927
+
928
+ if save:
929
+ self.save()
930
+
931
+ def revert(self):
932
+ reverted = False
933
+ for section in self.sections():
934
+ reverted |= getattr(self, section.accessor).revert()
935
+ if reverted:
936
+ self.update_status()
937
+ return reverted
938
+
939
+ def published(self, epoch=None):
940
+ return self._current(epoch=epoch, published=True)
941
+
942
+ def head(self, epoch=None, include_deleted=False):
943
+ return self._current(
944
+ epoch=epoch, published=None, include_deleted=include_deleted
945
+ )
946
+
947
+ def _current(self, epoch=None, published=None, filter=None, include_deleted=False):
948
+ for section in self.sections():
949
+ setattr(
950
+ self,
951
+ section.field,
952
+ (
953
+ getattr(self, section.accessor)._current(
954
+ epoch=epoch,
955
+ published=published,
956
+ filter=filter,
957
+ include_deleted=include_deleted,
958
+ )
959
+ if section.subsection
960
+ else getattr(self, section.accessor)
961
+ ._current(
962
+ epoch=epoch,
963
+ published=published,
964
+ filter=filter,
965
+ include_deleted=include_deleted,
966
+ )
967
+ .first()
968
+ ),
969
+ )
970
+
971
+ @cached_property
972
+ def modified_sections(self):
973
+ modified_sections = []
974
+ for section in self.sections():
975
+ if section.cls is SiteForm:
976
+ continue
977
+ if section.subsection:
978
+ idx = 0
979
+ for subsection in getattr(self, section.accessor).head().sort():
980
+ idx += 1
981
+ if not subsection.published:
982
+ dot_index = f"{subsection.section_number()}"
983
+ if subsection.subsection_number():
984
+ dot_index += f".{subsection.subsection_number()}"
985
+ dot_index += f".{idx}"
986
+ modified_sections.append(dot_index)
987
+ else:
988
+ modified = getattr(self, section.accessor).head()
989
+ if modified and not modified.published:
990
+ modified_sections.append(str(modified.section_number()))
991
+ return modified_sections
992
+
993
+ @property
994
+ def four_id(self):
995
+ return self.name[:4]
996
+
997
+ def publish(self, request=None, silent=False, timestamp=None):
998
+ """
999
+ Publish the current HEAD edits on this SiteLog.
1000
+
1001
+ :param request: The request that triggered the publish (optional)
1002
+ :param silent: If True, no publish signal will be sent.
1003
+ :param timestamp: Timestamp to use for the publish, if none - will
1004
+ be the time of this call
1005
+ :return: The number of sections and subsections that had changes
1006
+ that were published or 0 if no changes were at HEAD to publish.
1007
+ """
1008
+ if timestamp is None:
1009
+ timestamp = now()
1010
+
1011
+ form = self.siteform_set.head()
1012
+ if form is None:
1013
+ SiteForm.objects.create(site=self, published=False, report_type="NEW")
1014
+ elif form.published:
1015
+ form.pk = None
1016
+ form.published = False
1017
+ form.save()
1018
+
1019
+ sections_published = 0
1020
+ for section in self.sections():
1021
+ if section.subsection:
1022
+ for subsection in getattr(self, section.accessor).head(
1023
+ include_deleted=True
1024
+ ):
1025
+ sections_published += int(
1026
+ subsection.publish(
1027
+ request=request,
1028
+ silent=True,
1029
+ timestamp=timestamp,
1030
+ update_site=False,
1031
+ )
1032
+ )
1033
+ else:
1034
+ current = getattr(self, section.accessor).head(include_deleted=True)
1035
+ if current:
1036
+ sections_published += int(
1037
+ current.publish(
1038
+ request=request,
1039
+ silent=True,
1040
+ timestamp=timestamp,
1041
+ update_site=False,
1042
+ )
1043
+ )
1044
+
1045
+ # this might be an initial PUBLISH when we're in PROPOSED or FORMER
1046
+ if sections_published or self.status != SiteLogStatus.PUBLISHED:
1047
+ self.last_publish = timestamp
1048
+ # self.save()
1049
+ self.update_status(
1050
+ save=True,
1051
+ user=request.user if request else None,
1052
+ timestamp=timestamp,
1053
+ first_publish=(self.status is SiteLogStatus.PROPOSED),
1054
+ )
1055
+ if hasattr(self, "review_request"):
1056
+ self.review_request.delete()
1057
+ if not silent:
1058
+ slm_signals.site_published.send(
1059
+ sender=self,
1060
+ site=self,
1061
+ user=request.user if request else None,
1062
+ timestamp=timestamp,
1063
+ request=request,
1064
+ section=None,
1065
+ )
1066
+ return sections_published
1067
+
1068
+ @cached_property
1069
+ def equipment_list(self):
1070
+ """
1071
+ Returns a list of published equipment pairings in date order:
1072
+
1073
+ [
1074
+ (date, {receiver: <receiver>, antenna: <antenna>}),
1075
+ (date, {receiver: <receiver>, antenna: <antenna>}),
1076
+ (date, {receiver: <receiver>, antenna: <antenna>})
1077
+ ]
1078
+ """
1079
+ equipment = {}
1080
+ self.published()
1081
+ for receiver in self.sitereceiver_set.all():
1082
+ equipment.setdefault(receiver.installed.date(), {})
1083
+ equipment[receiver.installed.date()]["receiver"] = receiver
1084
+ for antenna in self.siteantenna_set.all():
1085
+ equipment.setdefault(antenna.installed.date(), {})
1086
+ equipment[antenna.installed.date()]["antenna"] = antenna
1087
+
1088
+ # build the time ordered list of equipment pairings
1089
+ time_ordered = []
1090
+ dates = sorted(equipment.keys())
1091
+ for idx, eq_date in enumerate(dates):
1092
+ time_ordered.append(
1093
+ (
1094
+ eq_date,
1095
+ {
1096
+ "receiver": equipment[eq_date].get(
1097
+ "receiver",
1098
+ None if idx == 0 else time_ordered[idx - 1][1]["receiver"],
1099
+ ),
1100
+ "antenna": equipment[eq_date].get(
1101
+ "antenna",
1102
+ None if idx == 0 else time_ordered[idx - 1][1]["antenna"],
1103
+ ),
1104
+ },
1105
+ )
1106
+ )
1107
+
1108
+ # remove any gaps at the front without full equipment
1109
+ start = 0
1110
+ for idx, eq in enumerate(time_ordered):
1111
+ if eq[1]["receiver"] and eq[1]["antenna"]:
1112
+ break
1113
+ start = idx + 1
1114
+
1115
+ return list(reversed(time_ordered[start:]))
1116
+
1117
+ def __str__(self):
1118
+ return self.name
1119
+
1120
+ def synchronize(self, refresh=True, skip_form_updates=False):
1121
+ Site.objects.filter(pk=self.pk).synchronize_denormalized_state(
1122
+ skip_form_updates=skip_form_updates
1123
+ )
1124
+ if refresh:
1125
+ self.refresh_from_db()
1126
+
1127
+
1128
+ class SiteSectionManager(gis_models.Manager):
1129
+ is_head = False
1130
+
1131
+ def get_queryset(self):
1132
+ return super().get_queryset().select_related("site")
1133
+
1134
+ def revert(self):
1135
+ return bool(self.get_queryset().filter(published=False).delete()[0])
1136
+
1137
+ def published(self, epoch=None):
1138
+ return self.get_queryset().published(epoch=epoch)
1139
+
1140
+ def head(self, epoch=None, include_deleted=False):
1141
+ self.is_head = True
1142
+ return self.get_queryset().head(epoch=epoch, include_deleted=include_deleted)
1143
+
1144
+ def _current(self, epoch=None, published=None, filter=None, include_deleted=False):
1145
+ self.is_head = published is None
1146
+ return self.get_queryset()._current(
1147
+ epoch=epoch,
1148
+ published=published,
1149
+ filter=filter,
1150
+ include_deleted=include_deleted,
1151
+ )
1152
+
1153
+
1154
+ class SiteSectionQueryset(gis_models.QuerySet):
1155
+ is_head = False
1156
+
1157
+ def editable_by(self, user):
1158
+ if user.is_superuser:
1159
+ return self
1160
+ return self.filter(site__agencies__in=user.agencies.all())
1161
+
1162
+ def station(self, station):
1163
+ if isinstance(station, str):
1164
+ return self.filter(site__name=station)
1165
+ return self.filter(site=station)
1166
+
1167
+ def published(self, epoch=None):
1168
+ return self._current(epoch=epoch, published=True).first()
1169
+
1170
+ def head(self, epoch=None, include_deleted=False):
1171
+ self.is_head = True
1172
+ return self._current(
1173
+ epoch=epoch, published=None, include_deleted=include_deleted
1174
+ ).first()
1175
+
1176
+ def _current(self, epoch=None, published=None, filter=None, include_deleted=False):
1177
+ self.is_head = published is None
1178
+ pub_q = filter or Q()
1179
+ if published is not None:
1180
+ pub_q &= Q(published=published)
1181
+ if epoch and getattr(self.model, "valid_time", ""):
1182
+ # todo does epoch make sense for non-subsections??
1183
+ ret = (
1184
+ self.filter(pub_q)
1185
+ .order_by("published")
1186
+ .filter({f"{self.model.valid_time}__lte": epoch})[0:1]
1187
+ )
1188
+ else:
1189
+ ret = self.filter(pub_q).order_by("published")[0:1]
1190
+
1191
+ ret.is_head = self.is_head
1192
+ return ret
1193
+
1194
+ def sort(self, reverse=False):
1195
+ return self
1196
+
1197
+
1198
+ class SiteLocationManager(SiteSectionManager):
1199
+ pass
1200
+
1201
+
1202
+ class SiteLocationQueryset(SiteSectionQueryset):
1203
+ def countries(self):
1204
+ """
1205
+ Return the list of unique countries that this queryset of SiteLocations
1206
+ is in.
1207
+
1208
+ .. note::
1209
+
1210
+ Site locations with invalid ISO-3166 ountry codes will not be
1211
+ included.
1212
+
1213
+ :return: A list of ISOCountry enumerations.
1214
+ """
1215
+ return list(
1216
+ set(
1217
+ [
1218
+ country
1219
+ for country in self.values_list("country", flat=True)
1220
+ .distinct()
1221
+ .order_by("country")
1222
+ if isinstance(country, ISOCountry)
1223
+ ]
1224
+ )
1225
+ )
1226
+
1227
+
1228
+ class SiteSection(gis_models.Model):
1229
+ site = models.ForeignKey("slm.Site", on_delete=models.CASCADE)
1230
+
1231
+ edited = models.DateTimeField(auto_now_add=True, db_index=True, null=False)
1232
+
1233
+ published = models.BooleanField(default=False, db_index=True)
1234
+
1235
+ _flags = models.JSONField(
1236
+ null=False, blank=True, default=dict, encoder=DefaultToStrEncoder
1237
+ )
1238
+
1239
+ num_flags = models.PositiveSmallIntegerField(default=0, null=False, db_index=True)
1240
+
1241
+ objects = SiteSectionManager.from_queryset(SiteSectionQueryset)()
1242
+
1243
+ def publishable(self):
1244
+ return not self.published or (getattr(self, "is_deleted", False))
1245
+
1246
+ @cached_property
1247
+ def has_published(self):
1248
+ return self.__class__.objects.filter(
1249
+ Q(site=self.site) & Q(published=True)
1250
+ ).exists()
1251
+
1252
+ def save(self, *args, **kwargs):
1253
+ self.num_flags = len(self._flags) if self._flags else 0
1254
+ super().save(*args, **kwargs)
1255
+
1256
+ def revert(self):
1257
+ reverted = bool(
1258
+ self.__class__.objects.filter(
1259
+ Q(site=self.site) & Q(published=False)
1260
+ ).delete()[0]
1261
+ )
1262
+ if reverted:
1263
+ self.site.update_status()
1264
+ return reverted
1265
+
1266
+ @property
1267
+ def dot_index(self):
1268
+ return self.section_number()
1269
+
1270
+ def publish(self, request=None, silent=False, timestamp=None, update_site=True):
1271
+ """
1272
+ Publish the current HEAD edits on this section - this will delete the
1273
+ last published section instance.
1274
+
1275
+ :param request: The request that triggered the publish (optional)
1276
+ :param silent: If True, no publish signal will be sent.
1277
+ :param timestamp: Timestamp to use for the publish, if none - will
1278
+ be the time of this call
1279
+ :param update_site: If True, site will be updated with publish
1280
+ information (i.e. pass False if this section is being published as
1281
+ part of a larger site publish)
1282
+ :return: True if a change was published, False otherwise.
1283
+ """
1284
+ if not self.publishable():
1285
+ return False
1286
+
1287
+ if timestamp is None:
1288
+ timestamp = now()
1289
+
1290
+ with transaction.atomic():
1291
+ if getattr(self, "is_deleted", False):
1292
+ self.delete()
1293
+ else:
1294
+ # delete the previously published row if it exists
1295
+ kwargs = {"site": self.site, "published": True}
1296
+ if hasattr(self, "subsection"):
1297
+ kwargs["subsection"] = self.subsection
1298
+
1299
+ self.__class__.objects.filter(**kwargs).delete()
1300
+
1301
+ self.published = True
1302
+ if isinstance(self, SiteForm):
1303
+ self.save(skip_update=True)
1304
+ else:
1305
+ self.save()
1306
+
1307
+ if update_site:
1308
+ self.site.last_publish = timestamp
1309
+ self.site.save()
1310
+ self.site.update_status(save=True, timestamp=timestamp)
1311
+
1312
+ if not silent and self.site.last_publish:
1313
+ # only send this signal if publishing this section results
1314
+ # in a newly published site log. It will not if this section
1315
+ # publish is part of a large site log publish or if the site
1316
+ # log has never been published before.
1317
+ slm_signals.site_published.send(
1318
+ sender=self.site,
1319
+ site=self.site,
1320
+ user=request.user if request else None,
1321
+ timestamp=timestamp,
1322
+ request=request,
1323
+ section=self,
1324
+ )
1325
+ return True
1326
+
1327
+ def can_publish(self, user):
1328
+ """
1329
+ This is a future hook to use for instances where non-moderators are
1330
+ allowed to publish a site log section under certain conditions.
1331
+
1332
+ :param user:
1333
+ :return:
1334
+ """
1335
+ return self.site.is_moderator(user)
1336
+
1337
+ def can_edit(self, user):
1338
+ return self.site.can_edit(user)
1339
+
1340
+ def clean(self):
1341
+ """
1342
+ Run configured validation routines. Routines are configured in
1343
+ the SLM_DATA_VALIDATORS setting. This setting maps model fields to
1344
+ validation logic. We run through those routines here and depending
1345
+ on severity we either add flags or throw an error.
1346
+
1347
+ :except ValidationError: If a save-blocking validation error has
1348
+ occurred.
1349
+ """
1350
+ errors = {}
1351
+ for field in [*self._meta.fields, *self._meta.many_to_many]:
1352
+ for validator in get_validators(self._meta.label, field.name):
1353
+ try:
1354
+ validator(self, field, getattr(self, field.name, None))
1355
+ except ValidationError as val_err:
1356
+ errors[field.name] = val_err.error_list
1357
+ if errors:
1358
+ raise ValidationError(errors)
1359
+
1360
+ @property
1361
+ def mod_status(self):
1362
+ if getattr(self, "is_deleted", False):
1363
+ return SiteLogStatus.UPDATED
1364
+ if self.published:
1365
+ return SiteLogStatus.PUBLISHED
1366
+ return SiteLogStatus.UPDATED
1367
+
1368
+ def published_diff(self, epoch=None):
1369
+ """
1370
+ Get a dictionary representing the diff with the current published HEAD
1371
+ """
1372
+ diff = {}
1373
+ if getattr(self, "is_deleted", None):
1374
+ return {}
1375
+
1376
+ if isinstance(self, SiteSubSection):
1377
+ published = self.__class__.objects.filter(site=self.site).published(
1378
+ subsection=self.subsection, epoch=epoch
1379
+ )
1380
+ else:
1381
+ published = self.__class__.objects.filter(site=self.site).published(
1382
+ epoch=epoch
1383
+ )
1384
+
1385
+ if published and published.id == self.id:
1386
+ return diff
1387
+
1388
+ def transform(value, field_name):
1389
+ if isinstance(value, models.Model):
1390
+ return str(value)
1391
+ elif isinstance(self._meta.get_field(field), models.ManyToManyField):
1392
+ if value:
1393
+ return "+".join([str(val) for val in value.all()])
1394
+ elif isinstance(self._meta.get_field(field), gis_models.PointField):
1395
+ if value:
1396
+ return value.coords
1397
+ return value
1398
+
1399
+ def differ(value1, value2):
1400
+ if isinstance(value1, str) and isinstance(value2, str):
1401
+ return value1.strip() != value2.strip()
1402
+ return value1 != value2
1403
+
1404
+ for field in self.site_log_fields():
1405
+ pub = transform(getattr(published, field, None), field)
1406
+ head = transform(getattr(self, field), field)
1407
+ if differ(head, pub) and head not in [None, ""] and pub not in [None, ""]:
1408
+ diff[field] = {"pub": pub, "head": head}
1409
+ return diff
1410
+
1411
+ @classmethod
1412
+ def section_number(cls):
1413
+ raise NotImplementedError("SiteSection models must implement section_number()")
1414
+
1415
+ @classmethod
1416
+ def section_name(cls):
1417
+ return cls._meta.verbose_name.replace("site", "").strip().title()
1418
+
1419
+ @classmethod
1420
+ def section_slug(cls):
1421
+ return cls.__name__.lower().replace("site", "").strip()
1422
+
1423
+ @classmethod
1424
+ def site_log_fields(cls):
1425
+ """
1426
+ Return the editable fields for the given sitelog section
1427
+ """
1428
+ return [
1429
+ field.name
1430
+ for field in cls._meta.fields
1431
+ if field.name
1432
+ not in {
1433
+ "id",
1434
+ "site",
1435
+ "edited",
1436
+ "published",
1437
+ "error",
1438
+ "subsection",
1439
+ "is_deleted",
1440
+ "deleted",
1441
+ "_flags",
1442
+ "inserted",
1443
+ "num_flags",
1444
+ }
1445
+ ]
1446
+
1447
+ @classmethod
1448
+ def structure(cls):
1449
+ """
1450
+ todo remove?
1451
+ Return the structure of the legacy site log section in the form:
1452
+ [
1453
+ 'field name0',
1454
+ ('section name1', ('field name1', 'field name2', ...),
1455
+ 'field name3',
1456
+ ...
1457
+ ]
1458
+
1459
+ The field name is the name of the field on the class, it may be a
1460
+ database field or a callable that returns an object coercible to a
1461
+ string.
1462
+ """
1463
+ # raise NotImplementedError(f'SiteSections must implement structure
1464
+ # classmethod!')
1465
+ return cls.site_log_fields()
1466
+
1467
+ @classmethod
1468
+ def legacy_name(cls, field):
1469
+ if callable(getattr(cls, field, None)):
1470
+ return getattr(cls, field).verbose_name
1471
+ return cls._meta.get_field(field).verbose_name
1472
+
1473
+ def __init__(self, *args, **kwargs):
1474
+ """
1475
+ After our model is initialized we cache the values of the fields. This
1476
+ is used by get_initial_value to eliminate the need for another database
1477
+ round trip.
1478
+
1479
+ :param args:
1480
+ :param kwargs:
1481
+ """
1482
+ super().__init__(*args, **kwargs)
1483
+ self._init_values_ = {}
1484
+ deferred = self.get_deferred_fields()
1485
+ for field in self.site_log_fields():
1486
+ if field not in deferred and not isinstance(
1487
+ self._meta.get_field(field), (models.ManyToManyField, models.ForeignKey)
1488
+ ):
1489
+ self._init_values_[field] = getattr(self, field)
1490
+
1491
+ def get_initial_value(self, field):
1492
+ """
1493
+ Get the current value of the field at the time of initialization. This
1494
+ call may result in a database lookup if the field was deferred on
1495
+ initialization. The field must be in the model's site_log_fields().
1496
+
1497
+ :param field:
1498
+ :return:
1499
+ """
1500
+ if field not in self.site_log_fields():
1501
+ raise ValueError(
1502
+ f"Field {field} is not a site log field for {self.__class__}"
1503
+ )
1504
+ if field not in self._init_values_:
1505
+ current = self.__class__.objects.filter(pk=self.pk).first()
1506
+ for field in self.site_log_fields():
1507
+ self._init_values_[field] = getattr(current, field)
1508
+ return self._init_values_[field]
1509
+
1510
+ def sort(self):
1511
+ """
1512
+ This is a kludge that's in place until the data model refactor can
1513
+ separate the edit and state tables
1514
+ """
1515
+ return self
1516
+
1517
+ class Meta:
1518
+ abstract = True
1519
+ ordering = ("-edited",)
1520
+ unique_together = (
1521
+ "site",
1522
+ "published",
1523
+ )
1524
+ indexes = [
1525
+ models.Index(fields=("edited", "published")),
1526
+ models.Index(fields=("site", "edited")),
1527
+ models.Index(fields=("site", "edited", "published")),
1528
+ ]
1529
+
1530
+
1531
+ class SiteSubSectionManager(SiteSectionManager):
1532
+ def revert(self):
1533
+ reverted = super().revert()
1534
+ reverted |= bool(
1535
+ self.get_queryset()
1536
+ .filter(Q(published=True) & Q(is_deleted=True))
1537
+ .update(is_deleted=False)
1538
+ )
1539
+ return reverted
1540
+
1541
+ def create(self, *args, **kwargs):
1542
+ # some DBs only support one auto field per table, so we have to
1543
+ # manually increment the subsection identifier for new subsections
1544
+ # using select_for_update to avoid race conditions
1545
+ if "subsection" not in kwargs:
1546
+ last = (
1547
+ self.model.objects.select_for_update()
1548
+ .filter(site=kwargs.get("site"))
1549
+ .aggregate(Max("subsection"))["subsection__max"]
1550
+ )
1551
+ kwargs["subsection"] = last + 1 if last is not None else 0
1552
+ return super().create(*args, **kwargs)
1553
+
1554
+
1555
+ class SiteSubSectionQuerySet(SiteSectionQueryset):
1556
+ is_head = False
1557
+
1558
+ def last(self):
1559
+ """
1560
+ The technique we use to restrict head() is to order by unpublished vs
1561
+ published and then use DISTINCT on subsection to pick the first
1562
+ row for each subsection - this unfortunately breaks when last() is used
1563
+ because it will pull out the published version instead of one exists.
1564
+ We work around that here by pulling out the last from the original
1565
+ query rather than asking the database to do it. first() is unaffected
1566
+ by this problem.
1567
+ """
1568
+ if self.is_head:
1569
+ for sct in reversed(self):
1570
+ return sct
1571
+ return super().last()
1572
+
1573
+ def published(self, subsection=None, epoch=None):
1574
+ return self._current(subsection=subsection, epoch=epoch, published=True)
1575
+
1576
+ def head(self, subsection=None, epoch=None, include_deleted=False):
1577
+ self.is_head = True
1578
+ return self._current(
1579
+ subsection=subsection,
1580
+ epoch=epoch,
1581
+ published=None,
1582
+ include_deleted=include_deleted,
1583
+ )
1584
+
1585
+ def _current(
1586
+ self,
1587
+ subsection=None,
1588
+ epoch=None,
1589
+ published=None,
1590
+ filter=None,
1591
+ include_deleted=False,
1592
+ ):
1593
+ """
1594
+ Fetch the subsection stack that matches the parameters.
1595
+
1596
+ :param subsection: A subsection identifier to fetch a specific
1597
+ subsection
1598
+ :param epoch: A point in time at which to fetch the subsection stack.
1599
+ :param published: If None (Default) fetch the latest HEAD version of
1600
+ the subsection stack, if True fetch only the latest published
1601
+ versions of the subsection stack. If False, fetch only unpublished
1602
+ members of the subsection stack.
1603
+ :param filter: An additional Q object to filter the subsection stack
1604
+ by.
1605
+ :param include_deleted: Include deleted sections if True, this param is
1606
+ meaningless if published is True.
1607
+ :return:
1608
+ """
1609
+ self.is_head = published is None
1610
+ section_q = filter or Q()
1611
+
1612
+ if epoch and self.model.valid_time is not None:
1613
+ section_q &= Q(**{f"{self.model.valid_time}__lte": epoch})
1614
+
1615
+ if published:
1616
+ section_q &= Q(published=True)
1617
+ elif published is None:
1618
+ if not include_deleted:
1619
+ section_q &= Q(is_deleted=False)
1620
+ else:
1621
+ section_q &= Q(published=False) | Q(is_deleted=True)
1622
+
1623
+ if subsection is not None:
1624
+ return self.filter(Q(subsection=subsection) & section_q).first()
1625
+
1626
+ elif published is not None:
1627
+ qry = self.filter(section_q).order_by(self.model.order_field, "subsection")
1628
+ else:
1629
+ ordering = ["subsection", "published"]
1630
+
1631
+ qry = self.filter(section_q).order_by(*ordering).distinct("subsection")
1632
+
1633
+ qry.is_head = self.is_head
1634
+ return qry
1635
+
1636
+ def sort(self, reverse=False):
1637
+ """
1638
+ When fetching head() lists - we must sort in memory because changes to
1639
+ the sort field can screw up the head() selection. In short - if
1640
+ you call head() on a subsection and ordering matters, call sort next.
1641
+ This does not alter the query but instead runs the query and returns
1642
+ an in-memory list that is sorted. It should therefore not be called
1643
+ on large querysets on that pull from more than one site.
1644
+
1645
+ This will be fixed in the data model architectural refactor when the
1646
+ edit tables are separated from the published tables.
1647
+
1648
+ :param reverse: Reverse the sorted order
1649
+ :return: An iterable of sorted objects
1650
+ """
1651
+
1652
+ class OrderTuple:
1653
+ def __init__(self, field, subsection):
1654
+ self.field = field
1655
+ self.subsection = subsection
1656
+
1657
+ def __lt__(self, other):
1658
+ """
1659
+ Custom < operator allows for None values of field to be
1660
+ ignored - there's some old/bad data we have to allow
1661
+ """
1662
+ if other.field is not None and self.field is not None:
1663
+ if self.field < other.field:
1664
+ return True
1665
+ return self.subsection < other.subsection
1666
+
1667
+ sorted_sections = sorted(
1668
+ (obj for obj in self),
1669
+ key=lambda o: OrderTuple(getattr(o, o.order_field), o.subsection)
1670
+ if getattr(self.model, "order_field", None)
1671
+ else lambda o: o.subsection,
1672
+ )
1673
+ if reverse:
1674
+ return list(reversed(sorted_sections))
1675
+ return list(sorted_sections)
1676
+
1677
+
1678
+ class SiteSubSection(SiteSection):
1679
+ subsection = models.PositiveSmallIntegerField(blank=True, db_index=True)
1680
+
1681
+ is_deleted = models.BooleanField(default=False, null=False, blank=True)
1682
+
1683
+ inserted = models.DateTimeField(default=now, db_index=True)
1684
+
1685
+ objects = SiteSubSectionManager.from_queryset(SiteSubSectionQuerySet)()
1686
+
1687
+ def revert(self):
1688
+ reverted = self.__class__.objects.filter(
1689
+ Q(site=self.site) & Q(published=False) & Q(subsection=self.subsection)
1690
+ ).delete()[0] | self.__class__.objects.filter(
1691
+ Q(site=self.site)
1692
+ & Q(published=True)
1693
+ & Q(is_deleted=True)
1694
+ & Q(subsection=self.subsection)
1695
+ ).update(is_deleted=False)
1696
+ if reverted:
1697
+ self.site.update_status()
1698
+ return reverted
1699
+
1700
+ @cached_property
1701
+ def has_published(self):
1702
+ return self.__class__.objects.filter(
1703
+ Q(site=self.site) & Q(published=True) & Q(subsection=self.subsection)
1704
+ ).exists()
1705
+
1706
+ @property
1707
+ def dot_index(self, published=False):
1708
+ """
1709
+ Get the published dot index of this subsection. (e.g. 8.1.2). This
1710
+ performs a query - it's usually best to compute these indexes during
1711
+ serialization or inline when subsections are iterated over.
1712
+ """
1713
+ dot_index = f"{self.section_number()}"
1714
+ if self.subsection_number():
1715
+ dot_index += f".{self.subsection_number()}"
1716
+
1717
+ # ordering gets tricky because some legacy data might have nulls in the
1718
+ # expected order field
1719
+ ordering = (self.order_field, getattr(self, self.order_field))
1720
+ if ordering[1] is None:
1721
+ ordering = ("subsection", getattr(self, "subsection"))
1722
+
1723
+ list_idx = (
1724
+ self.__class__.objects.filter(site=self.site)
1725
+ .published()
1726
+ .filter(**{f"{ordering[0]}__lt": ordering[1]})
1727
+ .count()
1728
+ + 1
1729
+ )
1730
+ dot_index += f".{list_idx}"
1731
+ return dot_index
1732
+
1733
+ @classproperty
1734
+ def valid_time(cls):
1735
+ """
1736
+ The field that defines when this subsection became valid. All
1737
+ subsections should have time ranges of validity.
1738
+ :return:
1739
+ """
1740
+ for field in ["installed", "effective_start"]:
1741
+ try:
1742
+ return field if cls._meta.get_field(field) else None
1743
+ except FieldDoesNotExist:
1744
+ continue
1745
+ raise NotImplementedError(f"{cls} must implement valid_time()")
1746
+
1747
+ @classproperty
1748
+ def order_field(cls):
1749
+ return cls.valid_time if cls.valid_time else "subsection"
1750
+
1751
+ @property
1752
+ def heading(self):
1753
+ """
1754
+ A brief name for this instance useful for UI display.
1755
+ """
1756
+ raise NotImplementedError("Site subsection models should implement heading().")
1757
+
1758
+ @cached_property
1759
+ def subsection_prefix(self):
1760
+ idx = f"{self.section_number()}"
1761
+ if self.subsection_number():
1762
+ idx += f".{self.subsection_number()}"
1763
+ return idx
1764
+
1765
+ @classmethod
1766
+ def subsection_number(cls):
1767
+ raise NotImplementedError(
1768
+ "SiteSubSection models must implement subsection_number()"
1769
+ )
1770
+
1771
+ @classmethod
1772
+ def subsection_name(cls):
1773
+ return cls._meta.verbose_name.replace("site", "").strip().title()
1774
+
1775
+ """
1776
+ @cached_property
1777
+ def subsection_id(self):
1778
+ # This cached property remaps section identifiers onto a monotonic
1779
+ # counter, (i.e. the x in 8.1.x)
1780
+ #if hasattr(self, 'subsection_id_'):
1781
+ # return self.subsection_id_
1782
+ #if not self.published:
1783
+ # return None
1784
+ return {
1785
+ # MySQL backend doesnt support distinct on field so we hav to use
1786
+ # a set to deduplicate, sigh
1787
+ sub: idx for idx, sub in enumerate({
1788
+ sub[0] for sub in self.__class__.objects.filter(
1789
+ published=True,
1790
+ site=self.site
1791
+ ).order_by('subsection').values_list('subsection')
1792
+ })
1793
+ }[self.subsection] + 1
1794
+ """
1795
+
1796
+ class Meta:
1797
+ abstract = True
1798
+ ordering = ("-edited",)
1799
+ unique_together = ("site", "published", "subsection")
1800
+ indexes = [
1801
+ models.Index(fields=("site", "edited")),
1802
+ models.Index(fields=("site", "edited", "published")),
1803
+ models.Index(fields=("site", "edited", "subsection")),
1804
+ models.Index(fields=("site", "edited", "published", "subsection")),
1805
+ models.Index(fields=("site", "subsection", "published")),
1806
+ models.Index(fields=("subsection", "published")),
1807
+ ]
1808
+ constraints = [
1809
+ CheckConstraint(
1810
+ check=~(Q(published=False) & Q(is_deleted=True)),
1811
+ name="%(app_label)s_%(class)s_no_mod_deleted",
1812
+ )
1813
+ ]
1814
+
1815
+
1816
+ class SiteForm(SiteSection):
1817
+ """
1818
+ 0. Form
1819
+
1820
+ Prepared by (full name) :
1821
+ Date Prepared : (CCYY-MM-DD)
1822
+ Report Type : (NEW/UPDATE)
1823
+ If Update:
1824
+ Previous Site Log : (ssss_ccyymmdd.log)
1825
+ Modified/Added Sections : (n.n,n.n,...)
1826
+ """
1827
+
1828
+ @classmethod
1829
+ def structure(cls):
1830
+ return [
1831
+ "prepared_by",
1832
+ "date_prepared",
1833
+ "report_type",
1834
+ (_("If Update"), ("previous_log", "modified_section")),
1835
+ ]
1836
+
1837
+ @classmethod
1838
+ def section_number(cls):
1839
+ return 0
1840
+
1841
+ @classmethod
1842
+ def section_header(cls):
1843
+ return "Form"
1844
+
1845
+ prepared_by = models.CharField(
1846
+ max_length=50,
1847
+ blank=True,
1848
+ verbose_name=_("Prepared by (full name)"),
1849
+ help_text=_("Enter the name of who prepared this site log"),
1850
+ )
1851
+ date_prepared = models.DateField(
1852
+ null=True,
1853
+ blank=True,
1854
+ verbose_name=_("Date Prepared"),
1855
+ help_text=_("Enter the date the site log was prepared (CCYY-MM-DD)."),
1856
+ db_index=True,
1857
+ validators=[MaxValueValidator(utc_now_date)],
1858
+ )
1859
+
1860
+ report_type = models.CharField(
1861
+ max_length=50,
1862
+ blank=True,
1863
+ default="NEW",
1864
+ verbose_name=_("Report Type"),
1865
+ help_text=_("Enter type of report. Example: (UPDATE)."),
1866
+ )
1867
+
1868
+ previous = models.ForeignKey(
1869
+ "slm.ArchiveIndex",
1870
+ on_delete=models.SET_NULL,
1871
+ null=True,
1872
+ blank=True,
1873
+ default=None,
1874
+ )
1875
+
1876
+ @property
1877
+ def previous_log(self):
1878
+ return self.site.get_filename(
1879
+ log_format=SiteLogFormat.LEGACY,
1880
+ lower_case=True,
1881
+ epoch=self.previous.begin,
1882
+ )
1883
+
1884
+ @property
1885
+ def previous_log_9char(self):
1886
+ if self.previous.files.filter(log_format=SiteLogFormat.ASCII_9CHAR).exists():
1887
+ return self.site.get_filename(
1888
+ log_format=SiteLogFormat.ASCII_9CHAR,
1889
+ lower_case=True,
1890
+ epoch=self.previous.begin,
1891
+ )
1892
+ return self.previous_log
1893
+
1894
+ @property
1895
+ def previous_xml(self):
1896
+ return self.site.get_filename(
1897
+ log_format=SiteLogFormat.GEODESY_ML,
1898
+ lower_case=True,
1899
+ epoch=self.previous.begin,
1900
+ )
1901
+
1902
+ @property
1903
+ def previous_json(self):
1904
+ return self.site.get_filename(
1905
+ log_format=SiteLogFormat.JSON,
1906
+ name_len=4,
1907
+ lower_case=True,
1908
+ epoch=self.previous.begin,
1909
+ )
1910
+
1911
+ modified_section = models.TextField(
1912
+ blank=True,
1913
+ default="",
1914
+ verbose_name=_("Modified/Added Sections"),
1915
+ help_text=_(
1916
+ "Enter the sections which have changed from the previous version "
1917
+ "of the log. Example: (3.2, 4.2)"
1918
+ ),
1919
+ )
1920
+
1921
+ def save(self, *args, skip_update=False, set_previous=True, **kwargs):
1922
+ from slm.models import ArchiveIndex
1923
+
1924
+ if set_previous:
1925
+ self.previous = ArchiveIndex.objects.filter(site=self.site).first()
1926
+ if self.previous:
1927
+ self.report_type = "UPDATE"
1928
+ if not skip_update:
1929
+ self.modified_section = kwargs.pop(
1930
+ "modified_section", ", ".join(self.site.modified_sections)
1931
+ )
1932
+ self.date_prepared = datetime.now(timezone.utc).date()
1933
+ self.full_clean()
1934
+ super().save(*args, **kwargs)
1935
+
1936
+ def clean(self):
1937
+ from slm.models import ArchiveIndex
1938
+
1939
+ super().clean()
1940
+ head = ArchiveIndex.objects.filter(
1941
+ Q(site=self.site) & Q(end__isnull=True)
1942
+ ).first()
1943
+ if head and self.date_prepared < head.begin.date():
1944
+ raise ValidationError(
1945
+ {
1946
+ "date_prepared": [
1947
+ _("Date prepared cannot be before the previous log.")
1948
+ ]
1949
+ }
1950
+ )
1951
+
1952
+ def publish(self, request=None, silent=False, timestamp=None, update_site=True):
1953
+ self.date_prepared = datetime.now(timezone.utc).date()
1954
+ return super().publish(
1955
+ request=request, silent=silent, timestamp=timestamp, update_site=update_site
1956
+ )
1957
+
1958
+
1959
+ class SiteIdentification(SiteSection):
1960
+ """
1961
+ Old Table(s):
1962
+ 'SiteLog_Identification',
1963
+ 'SiteLog_IdentificationGeologic',
1964
+ 'SiteLog_IdentificationMonument'
1965
+
1966
+ -----------------------------
1967
+
1968
+ 1. Site Identification of the GNSS Monument
1969
+
1970
+ Site Name :
1971
+ Four Character ID : (A4)
1972
+ Monument Inscription :
1973
+ IERS DOMES Number : (A9)
1974
+ CDP Number : (A4)
1975
+ Monument Description : (PILLAR/BRASS PLATE/STEEL MAST/etc)
1976
+ Height of the Monument : (m)
1977
+ Monument Foundation : (STEEL RODS, CONCRETE BLOCK, ROOF, etc)
1978
+ Foundation Depth : (m)
1979
+ Marker Description : (CHISELLED CROSS/DIVOT/BRASS NAIL/etc)
1980
+ Date Installed : (CCYY-MM-DDThh:mmZ)
1981
+ Geologic Characteristic : (BEDROCK/CLAY/CONGLOMERATE/GRAVEL/SAND/etc)
1982
+ Bedrock Type : (IGNEOUS/METAMORPHIC/SEDIMENTARY)
1983
+ Bedrock Condition : (FRESH/JOINTED/WEATHERED)
1984
+ Fracture Spacing : (1-10 cm/11-50 cm/51-200 cm/over 200 cm)
1985
+ Fault Zones Nearby : (YES/NO/Name of the zone)
1986
+ Distance/Activity : (multiple lines)
1987
+ Additional Information : (multiple lines)
1988
+ """
1989
+
1990
+ @classmethod
1991
+ def structure(cls):
1992
+ return [
1993
+ "site_name",
1994
+ # "four_character_id",
1995
+ "nine_character_id",
1996
+ "monument_inscription",
1997
+ "iers_domes_number",
1998
+ "cdp_number",
1999
+ (
2000
+ "monument_description",
2001
+ ("monument_height", "monument_foundation", "foundation_depth"),
2002
+ ),
2003
+ "marker_description",
2004
+ "date_installed",
2005
+ (
2006
+ "geologic_characteristic",
2007
+ (
2008
+ "bedrock_type",
2009
+ "bedrock_condition",
2010
+ "fracture_spacing",
2011
+ ("fault_zones", ("distance",)),
2012
+ ),
2013
+ ),
2014
+ "additional_information",
2015
+ ]
2016
+
2017
+ @classmethod
2018
+ def section_number(cls):
2019
+ return 1
2020
+
2021
+ @classmethod
2022
+ def section_header(cls):
2023
+ return "Site Identification of the GNSS Monument"
2024
+
2025
+ site_name = models.CharField(
2026
+ max_length=255,
2027
+ blank=True,
2028
+ default="",
2029
+ verbose_name=_("Site Name"),
2030
+ help_text=_("Enter the name of the site."),
2031
+ db_index=True,
2032
+ )
2033
+
2034
+ @cached_property
2035
+ def four_character_id(self):
2036
+ return self.site.name[0:4].upper()
2037
+
2038
+ @cached_property
2039
+ def nine_character_id(self):
2040
+ return self.site.name.upper()
2041
+
2042
+ monument_inscription = models.CharField(
2043
+ max_length=50,
2044
+ default="",
2045
+ blank=True,
2046
+ verbose_name=_("Monument Inscription"),
2047
+ help_text=_("Enter what is stamped on the monument"),
2048
+ db_index=True,
2049
+ )
2050
+
2051
+ iers_domes_number = models.CharField(
2052
+ max_length=9,
2053
+ blank=True,
2054
+ default="",
2055
+ verbose_name=_("IERS DOMES Number"),
2056
+ help_text=_(
2057
+ "This is strictly required. "
2058
+ "See https://itrf.ign.fr/en/network/domes/request to obtain one. "
2059
+ "Format: 9 character alphanumeric (A9)"
2060
+ ),
2061
+ db_index=True,
2062
+ )
2063
+
2064
+ cdp_number = models.CharField(
2065
+ max_length=50,
2066
+ default="",
2067
+ blank=True,
2068
+ verbose_name=_("CDP Number"),
2069
+ help_text=_(
2070
+ "Enter the NASA CDP identifier if available. "
2071
+ "Format: 4 character alphanumeric (A4)"
2072
+ ),
2073
+ db_index=True,
2074
+ )
2075
+
2076
+ date_installed = models.DateTimeField(
2077
+ null=True,
2078
+ blank=True,
2079
+ verbose_name=_("Date Installed (UTC)"),
2080
+ help_text=_(
2081
+ "Enter the original date that this site was included in the IGS. "
2082
+ "Format: (CCYY-MM-DDThh:mmZ)"
2083
+ ),
2084
+ db_index=True,
2085
+ )
2086
+
2087
+ # Monument fields
2088
+ monument_description = models.CharField(
2089
+ max_length=50,
2090
+ default="",
2091
+ blank=True,
2092
+ verbose_name=_("Monument Description"),
2093
+ help_text=_(
2094
+ "Provide a general description of the GNSS monument. "
2095
+ "Format: (PILLAR/BRASS PLATE/STEEL MAST/etc)"
2096
+ ),
2097
+ db_index=True,
2098
+ )
2099
+
2100
+ monument_height = models.FloatField(
2101
+ null=True,
2102
+ default=None,
2103
+ blank=True,
2104
+ verbose_name=_("Height of the Monument (m)"),
2105
+ help_text=_(
2106
+ "Enter the height of the monument above the ground surface in "
2107
+ "meters. Units: (m)"
2108
+ ),
2109
+ db_index=True,
2110
+ )
2111
+ monument_foundation = models.CharField(
2112
+ max_length=50,
2113
+ default="",
2114
+ blank=True,
2115
+ verbose_name=_("Monument Foundation"),
2116
+ help_text=_(
2117
+ "Describe how the GNSS monument is attached to the ground. "
2118
+ "Format: (STEEL RODS, CONCRETE BLOCK, ROOF, etc)"
2119
+ ),
2120
+ db_index=True,
2121
+ )
2122
+ foundation_depth = models.FloatField(
2123
+ null=True,
2124
+ default=None,
2125
+ blank=True,
2126
+ verbose_name=_("Foundation Depth (m)"),
2127
+ help_text=_(
2128
+ "Enter the depth of the monument foundation below the ground "
2129
+ "surface in meters. Format: (m)"
2130
+ ),
2131
+ db_index=True,
2132
+ )
2133
+
2134
+ marker_description = models.CharField(
2135
+ max_length=50,
2136
+ default="",
2137
+ blank=True,
2138
+ verbose_name=_("Marker Description"),
2139
+ help_text=_(
2140
+ "Describe the actual physical marker reference point. "
2141
+ "Format: (CHISELLED CROSS/DIVOT/BRASS NAIL/etc)"
2142
+ ),
2143
+ db_index=True,
2144
+ )
2145
+
2146
+ geologic_characteristic = models.CharField(
2147
+ max_length=50,
2148
+ default="",
2149
+ blank=True,
2150
+ verbose_name=_("Geologic Characteristic"),
2151
+ help_text=_(
2152
+ "Describe the general geologic characteristics of the GNSS site. "
2153
+ "Format: (BEDROCK/CLAY/CONGLOMERATE/GRAVEL/SAND/etc)"
2154
+ ),
2155
+ db_index=True,
2156
+ )
2157
+
2158
+ bedrock_type = models.CharField(
2159
+ max_length=50,
2160
+ default="",
2161
+ blank=True,
2162
+ verbose_name=_("Bedrock Type"),
2163
+ help_text=_(
2164
+ "If the site is located on bedrock, describe the nature of that "
2165
+ "bedrock. Format: (IGNEOUS/METAMORPHIC/SEDIMENTARY)"
2166
+ ),
2167
+ db_index=True,
2168
+ )
2169
+
2170
+ bedrock_condition = models.CharField(
2171
+ max_length=50,
2172
+ default="",
2173
+ blank=True,
2174
+ verbose_name=_("Bedrock Condition"),
2175
+ help_text=_(
2176
+ "If the site is located on bedrock, describe the condition of "
2177
+ "that bedrock. Format: (FRESH/JOINTED/WEATHERED)"
2178
+ ),
2179
+ db_index=True,
2180
+ )
2181
+
2182
+ fracture_spacing = EnumField(
2183
+ FractureSpacing,
2184
+ strict=False,
2185
+ max_length=50,
2186
+ default=None,
2187
+ null=True,
2188
+ blank=True,
2189
+ verbose_name=_("Fracture Spacing"),
2190
+ help_text=_(
2191
+ "If known, describe the fracture spacing of the bedrock. "
2192
+ "Format: (1-10 cm/11-50 cm/51-200 cm/over 200 cm)"
2193
+ ),
2194
+ db_index=True,
2195
+ )
2196
+
2197
+ fault_zones = models.CharField(
2198
+ max_length=50,
2199
+ default="",
2200
+ blank=True,
2201
+ verbose_name=_("Fault Zones Nearby"),
2202
+ help_text=_(
2203
+ "Enter the name of any known faults near the site. "
2204
+ "Format: (YES/NO/Name of the zone)"
2205
+ ),
2206
+ db_index=True,
2207
+ )
2208
+
2209
+ distance = models.TextField(
2210
+ default="",
2211
+ blank=True,
2212
+ verbose_name=_("Distance/activity"),
2213
+ help_text=_(
2214
+ "Describe proximity of the site to any known faults. "
2215
+ "Format: (multiple lines)"
2216
+ ),
2217
+ )
2218
+
2219
+ additional_information = models.TextField(
2220
+ default="",
2221
+ blank=True,
2222
+ verbose_name=_("Additional Information"),
2223
+ help_text=_(
2224
+ "Enter any additional information about the geologic "
2225
+ "characteristics of the GNSS site. Format: (multiple lines)"
2226
+ ),
2227
+ )
2228
+
2229
+
2230
+ class SiteLocation(SiteSection):
2231
+ """
2232
+ Old Table(s):
2233
+ 'SiteLog_Location'
2234
+ -----------------------------
2235
+
2236
+ 2. Site Location Information
2237
+
2238
+ City or Town :
2239
+ State or Province :
2240
+ Country :
2241
+ Tectonic Plate :
2242
+ Approximate Position (ITRF)
2243
+ X Coordinate (m) :
2244
+ Y Coordinate (m) :
2245
+ Z Coordinate (m) :
2246
+ Latitude (N is +) : (+/-DDMMSS.SS)
2247
+ Longitude (E is +) : (+/-DDDMMSS.SS)
2248
+ Elevation (m,ellips.) : (F7.1)
2249
+ Additional Information : (multiple lines)
2250
+ """
2251
+
2252
+ objects = SiteLocationManager.from_queryset(SiteLocationQueryset)()
2253
+
2254
+ @classmethod
2255
+ def structure(cls):
2256
+ return [
2257
+ "city",
2258
+ "state",
2259
+ "country",
2260
+ "tectonic",
2261
+ (
2262
+ _("Approximate Position (ITRF)"),
2263
+ (
2264
+ "xyz",
2265
+ "llh",
2266
+ ),
2267
+ ),
2268
+ "additional_information",
2269
+ ]
2270
+
2271
+ @classmethod
2272
+ def section_number(cls):
2273
+ return 2
2274
+
2275
+ @classmethod
2276
+ def section_header(cls):
2277
+ return "Site Location Information"
2278
+
2279
+ city = models.CharField(
2280
+ max_length=50,
2281
+ blank=True,
2282
+ default="",
2283
+ verbose_name=_("City or Town"),
2284
+ help_text=_("Enter the city or town the site is located in"),
2285
+ db_index=True,
2286
+ )
2287
+ state = models.CharField(
2288
+ max_length=50,
2289
+ default="",
2290
+ blank=True,
2291
+ verbose_name=_("State or Province"),
2292
+ help_text=_("Enter the state or province the site is located in"),
2293
+ db_index=True,
2294
+ )
2295
+
2296
+ country = EnumField(
2297
+ ISOCountry,
2298
+ strict=False,
2299
+ max_length=100,
2300
+ blank=True,
2301
+ null=True,
2302
+ default=None,
2303
+ verbose_name=_("Country or Region"),
2304
+ help_text=_("Enter the country/region the site is located in"),
2305
+ db_index=True,
2306
+ )
2307
+
2308
+ tectonic = EnumField(
2309
+ TectonicPlates,
2310
+ strict=False,
2311
+ max_length=50,
2312
+ null=True,
2313
+ default=None,
2314
+ blank=True,
2315
+ verbose_name=_("Tectonic Plate"),
2316
+ help_text=_("Select the primary tectonic plate that the GNSS site occupies"),
2317
+ db_index=True,
2318
+ )
2319
+
2320
+ # https://epsg.io/7789
2321
+ xyz = gis_models.PointField(
2322
+ srid=7789,
2323
+ dim=3,
2324
+ null=True,
2325
+ blank=True,
2326
+ db_index=True,
2327
+ help_text=_("Enter the ITRF position to a one meter precision. Format (m)"),
2328
+ verbose_name=_("Position (X, Y, Z) (m)"),
2329
+ )
2330
+
2331
+ # https://epsg.io/4979
2332
+ llh = gis_models.PointField(
2333
+ srid=4979,
2334
+ dim=3,
2335
+ null=True,
2336
+ blank=False,
2337
+ verbose_name=_("Position (Lat, Lon, Elev (m))"),
2338
+ help_text=_(
2339
+ "Enter the ITRF latitude and longitude in decimal degrees and "
2340
+ "elevation in meters to one meter precision. Note, legacy site "
2341
+ "log format is (+/-DDMMSS.SS) and elevation may be given to more "
2342
+ "decimal places than F7.1. F7.1 is the minimum for the SINEX "
2343
+ "format."
2344
+ ),
2345
+ db_index=True,
2346
+ )
2347
+
2348
+ additional_information = models.TextField(
2349
+ blank=True,
2350
+ default="",
2351
+ verbose_name=_("Additional Information"),
2352
+ help_text=_(
2353
+ "Describe the source of these coordinates or any other relevant "
2354
+ "information. Format: (multiple lines)"
2355
+ ),
2356
+ )
2357
+
2358
+
2359
+ class SiteReceiverManager(SiteSubSectionManager):
2360
+ def get_queryset(self):
2361
+ return (
2362
+ super()
2363
+ .get_queryset()
2364
+ .select_related("receiver_type")
2365
+ .prefetch_related("satellite_system")
2366
+ )
2367
+
2368
+
2369
+ class SiteReceiverQueryset(SiteSubSectionQuerySet):
2370
+ pass
2371
+
2372
+
2373
+ class SiteReceiver(SiteSubSection):
2374
+ """
2375
+ 3. GNSS Receiver Information
2376
+
2377
+ 3.x Receiver Type : (A20, from rcvr_ant.tab; see instructions)
2378
+ Satellite System : (GPS+GLO+GAL+BDS+QZSS+SBAS)
2379
+ Serial Number : (A20, but note the first A5 is used in SINEX)
2380
+ Firmware Version : (A11)
2381
+ Elevation Cutoff Setting : (deg)
2382
+ Date Installed : (CCYY-MM-DDThh:mmZ)
2383
+ Date Removed : (CCYY-MM-DDThh:mmZ)
2384
+ Temperature Stabiliz. : (none or tolerance in degrees C)
2385
+ Additional Information : (multiple lines)
2386
+ """
2387
+
2388
+ @classmethod
2389
+ def site_log_fields(cls):
2390
+ # satellite_system is not picked up by the super site_log_fields
2391
+ # because its many to many - just establish this list here manually
2392
+ # instead
2393
+ return [
2394
+ "receiver_type",
2395
+ "satellite_system",
2396
+ "serial_number",
2397
+ "firmware",
2398
+ "elevation_cutoff",
2399
+ "installed",
2400
+ "removed",
2401
+ "temp_stabilized",
2402
+ "temp_nominal",
2403
+ "temp_deviation",
2404
+ "additional_info",
2405
+ ]
2406
+
2407
+ # objects = SiteReceiverManager.from_queryset(SiteReceiverQueryset)()
2408
+
2409
+ @classmethod
2410
+ def section_number(cls):
2411
+ return 3
2412
+
2413
+ @classmethod
2414
+ def section_header(cls):
2415
+ return "GNSS Receiver Information"
2416
+
2417
+ @classmethod
2418
+ def subsection_number(cls):
2419
+ return None
2420
+
2421
+ @property
2422
+ def heading(self):
2423
+ return self.receiver_type.model
2424
+
2425
+ @property
2426
+ def effective(self):
2427
+ if self.installed and self.removed:
2428
+ return f"{date_to_str(self.installed)}/{date_to_str(self.removed)}"
2429
+ elif self.installed:
2430
+ return f"{date_to_str(self.installed)}"
2431
+ return ""
2432
+
2433
+ receiver_type = models.ForeignKey(
2434
+ "slm.Receiver",
2435
+ blank=False,
2436
+ verbose_name=_("Receiver Type"),
2437
+ help_text=_(
2438
+ "Please find your receiver in "
2439
+ "https://files.igs.org/pub/station/general/rcvr_ant.tab and use "
2440
+ "the official name, taking care to get capital letters, hyphens, "
2441
+ "etc. exactly correct. If you do not find a listing for your "
2442
+ "receiver, please notify the IGS Central Bureau. "
2443
+ "Format: (A20, from rcvr_ant.tab; see instructions)"
2444
+ ),
2445
+ on_delete=models.PROTECT,
2446
+ related_name="site_receivers",
2447
+ )
2448
+
2449
+ satellite_system = models.ManyToManyField(
2450
+ "slm.SatelliteSystem",
2451
+ verbose_name=_("Satellite System"),
2452
+ blank=True,
2453
+ help_text=_("Check all GNSS systems that apply"),
2454
+ )
2455
+
2456
+ serial_number = models.CharField(
2457
+ max_length=50,
2458
+ blank=True,
2459
+ default="",
2460
+ verbose_name=_("Serial Number"),
2461
+ help_text=_(
2462
+ "Enter the receiver serial number. "
2463
+ "Format: (A20, but note the first A5 is used in SINEX)"
2464
+ ),
2465
+ db_index=True,
2466
+ )
2467
+
2468
+ firmware = models.CharField(
2469
+ max_length=50,
2470
+ blank=True,
2471
+ default="",
2472
+ verbose_name=_("Firmware Version"),
2473
+ help_text=_("Enter the receiver firmware version. Format: (A11)"),
2474
+ db_index=True,
2475
+ )
2476
+
2477
+ elevation_cutoff = models.FloatField(
2478
+ default=None,
2479
+ null=True,
2480
+ blank=True,
2481
+ verbose_name=_("Elevation Cutoff Setting (°)"),
2482
+ help_text=_(
2483
+ "Please respond with the tracking cutoff as set in the receiver, "
2484
+ "regardless of terrain or obstructions in the area. Format: (deg)"
2485
+ ),
2486
+ validators=[MinValueValidator(-5), MaxValueValidator(15)],
2487
+ db_index=True,
2488
+ )
2489
+
2490
+ installed = models.DateTimeField(
2491
+ null=True,
2492
+ blank=False,
2493
+ verbose_name=_("Date Installed (UTC)"),
2494
+ help_text=_(
2495
+ "Enter the date and time the receiver was installed. "
2496
+ "Format: (CCYY-MM-DDThh:mmZ)"
2497
+ ),
2498
+ db_index=True,
2499
+ )
2500
+
2501
+ removed = models.DateTimeField(
2502
+ null=True,
2503
+ default=None,
2504
+ blank=True,
2505
+ verbose_name=_("Date Removed (UTC)"),
2506
+ help_text=_(
2507
+ "Enter the date and time the receiver was removed. It is important"
2508
+ " that the date removed is entered BEFORE the addition of a new "
2509
+ "receiver. Format: (CCYY-MM-DDThh:mmZ)"
2510
+ ),
2511
+ db_index=True,
2512
+ )
2513
+
2514
+ temp_stabilized = models.BooleanField(
2515
+ null=True,
2516
+ default=None,
2517
+ blank=True,
2518
+ verbose_name=_("Temperature Stabilized"),
2519
+ help_text=_(
2520
+ "If null (default) the temperature stabilization status is "
2521
+ "unknown. If true the receiver is in a temperature stabilized "
2522
+ "environment, if false the receiver is not in a temperature "
2523
+ "stabilized environment."
2524
+ ),
2525
+ )
2526
+
2527
+ temp_nominal = models.FloatField(
2528
+ default=None,
2529
+ null=True,
2530
+ blank=True,
2531
+ verbose_name=_("Nominal Temperature (°C)"),
2532
+ help_text=_(
2533
+ "If the receiver is in a temperature controlled environment, "
2534
+ "please enter the approximate temperature of that environment. "
2535
+ "Format: (°C)"
2536
+ ),
2537
+ db_index=True,
2538
+ )
2539
+ # this field is a string in GeodesyML - therefore leaving it as character
2540
+ temp_deviation = models.FloatField(
2541
+ default=None,
2542
+ null=True,
2543
+ blank=True,
2544
+ verbose_name=_("Temperature Deviation (± °C)"),
2545
+ help_text=_(
2546
+ "If the receiver is in a temperature controlled environment, "
2547
+ "please enter the expected temperature deviation from nominal of "
2548
+ "that environment. Format: (± °C)"
2549
+ ),
2550
+ db_index=True,
2551
+ )
2552
+
2553
+ additional_info = models.TextField(
2554
+ default="",
2555
+ blank=True,
2556
+ verbose_name=_("Additional Information"),
2557
+ help_text=_(
2558
+ "Enter any additional relevant information about the receiver. "
2559
+ "Format: (multiple lines)"
2560
+ ),
2561
+ )
2562
+
2563
+ def __str__(self):
2564
+ return str(self.receiver_type)
2565
+
2566
+ class Meta(SiteSubSection.Meta):
2567
+ indexes = [
2568
+ models.Index(fields=("site", "subsection", "published", "installed")),
2569
+ models.Index(fields=("subsection", "published", "installed")),
2570
+ ]
2571
+
2572
+
2573
+ class SiteAntennaManager(SiteSubSectionManager):
2574
+ def get_queryset(self):
2575
+ return super().get_queryset().select_related("antenna_type", "radome_type")
2576
+
2577
+
2578
+ class SiteAntennaQueryset(SiteSubSectionQuerySet):
2579
+ pass
2580
+
2581
+
2582
+ class SiteAntenna(SiteSubSection):
2583
+ """
2584
+ 4. GNSS Antenna Information
2585
+
2586
+ 4.x Antenna Type : (A20, from rcvr_ant.tab; see instructions)
2587
+ Serial Number : (A*, but note the first A5 is used in SINEX)
2588
+ Antenna Reference Point : (BPA/BCR/XXX from "antenna.gra"; see instr.)
2589
+ Marker->ARP Up Ecc. (m) : (F8.4)
2590
+ Marker->ARP North Ecc(m) : (F8.4)
2591
+ Marker->ARP East Ecc(m) : (F8.4)
2592
+ Alignment from True N : (deg; + is clockwise/east)
2593
+ Antenna Radome Type : (A4 from rcvr_ant.tab; see instructions)
2594
+ Radome Serial Number :
2595
+ Antenna Cable Type : (vendor & type number)
2596
+ Antenna Cable Length : (m)
2597
+ Date Installed : (CCYY-MM-DDThh:mmZ)
2598
+ Date Removed : (CCYY-MM-DDThh:mmZ)
2599
+ Additional Information : (multiple lines)
2600
+ """
2601
+
2602
+ # objects = SiteAntennaManager.from_queryset(SiteAntennaQueryset)()
2603
+
2604
+ @property
2605
+ def heading(self):
2606
+ return self.antenna_type.model
2607
+
2608
+ @property
2609
+ def effective(self):
2610
+ if self.installed and self.removed:
2611
+ return f"{date_to_str(self.installed)}/{date_to_str(self.removed)}"
2612
+ elif self.installed:
2613
+ return f"{date_to_str(self.installed)}"
2614
+ return ""
2615
+
2616
+ @classmethod
2617
+ def section_number(cls):
2618
+ return 4
2619
+
2620
+ @classmethod
2621
+ def section_header(cls):
2622
+ return "GNSS Antenna Information"
2623
+
2624
+ @classmethod
2625
+ def subsection_number(cls):
2626
+ return None
2627
+
2628
+ antenna_type = models.ForeignKey(
2629
+ "slm.Antenna",
2630
+ on_delete=models.PROTECT,
2631
+ blank=False,
2632
+ verbose_name=_("Antenna Type"),
2633
+ help_text=_(
2634
+ "Please find your antenna radome type in "
2635
+ "https://files.igs.org/pub/station/general/rcvr_ant.tab and use "
2636
+ "the official name, taking care to get capital letters, hyphens, "
2637
+ "etc. exactly correct. The radome code from rcvr_ant.tab must be "
2638
+ 'indicated in columns 17-20 of the Antenna Type, use "NONE" if no '
2639
+ "radome is installed. The antenna+radome pair must have an entry "
2640
+ "in https://files.igs.org/pub/station/general/igs05.atx with "
2641
+ "zenith- and azimuth-dependent calibration values down to the "
2642
+ "horizon. If not, notify the CB. Format: (A20, from rcvr_ant.tab; "
2643
+ "see instructions)"
2644
+ ),
2645
+ related_name="site_antennas",
2646
+ )
2647
+
2648
+ serial_number = models.CharField(
2649
+ max_length=128,
2650
+ blank=True,
2651
+ verbose_name=_("Serial Number"),
2652
+ help_text=_("Only Alpha Numeric Chars and - . Symbols allowed"),
2653
+ db_index=True,
2654
+ )
2655
+
2656
+ # todo remove this b/c it belongs solely on antenna type?
2657
+ reference_point = EnumField(
2658
+ AntennaReferencePoint,
2659
+ blank=True,
2660
+ default=None,
2661
+ verbose_name=_("Antenna Reference Point"),
2662
+ null=True,
2663
+ help_text=_(
2664
+ "Locate your antenna in the file "
2665
+ "https://files.igs.org/pub/station/general/antenna.gra. Indicate "
2666
+ "the three-letter abbreviation for the point which is indicated "
2667
+ "equivalent to ARP for your antenna. Contact the Central Bureau if"
2668
+ " your antenna does not appear. Format: (BPA/BCR/XXX from "
2669
+ "antenna.gra; see instr.)"
2670
+ ),
2671
+ db_index=True,
2672
+ )
2673
+
2674
+ marker_une = gis_models.PointField(
2675
+ srid=0, # env is a local reference frame
2676
+ dim=3,
2677
+ verbose_name=_("Marker->ARP UNE Ecc (m)"),
2678
+ default=None,
2679
+ null=True,
2680
+ blank=True,
2681
+ help_text=_(
2682
+ "Up-North-East eccentricity is the offset between the ARP and "
2683
+ "marker described in section 1 measured to an accuracy of 1mm. "
2684
+ "Format: (F8.4) Value 0 is OK"
2685
+ ),
2686
+ )
2687
+
2688
+ alignment = models.FloatField(
2689
+ blank=True,
2690
+ null=True,
2691
+ default=None,
2692
+ verbose_name=_("Alignment from True N (°)"),
2693
+ help_text=_(
2694
+ "Enter the clockwise offset from true north in degrees. The "
2695
+ "positive direction is clockwise, so that due east would be "
2696
+ 'equivalent to a response of "+90". '
2697
+ "Format: (deg; + is clockwise/east)"
2698
+ ),
2699
+ validators=[MinValueValidator(-180), MaxValueValidator(180)],
2700
+ db_index=True,
2701
+ )
2702
+
2703
+ radome_type = models.ForeignKey(
2704
+ "slm.Radome",
2705
+ blank=False,
2706
+ verbose_name=_("Antenna Radome Type"),
2707
+ help_text=_(
2708
+ "Please find your antenna radome type in "
2709
+ "https://files.igs.org/pub/station/general/rcvr_ant.tab and use "
2710
+ "the official name, taking care to get capital letters, hyphens, "
2711
+ "etc. exactly correct. The radome code from rcvr_ant.tab must be "
2712
+ 'indicated in columns 17-20 of the Antenna Type, use "NONE" if no '
2713
+ "radome is installed. The antenna+radome pair must have an entry "
2714
+ "in https://files.igs.org/pub/station/general/igs05.atx with "
2715
+ "zenith- and azimuth-dependent calibration values down to the "
2716
+ "horizon. If not, notify the CB. Format: (A20, from rcvr_ant.tab; "
2717
+ "see instructions)"
2718
+ ),
2719
+ on_delete=models.PROTECT,
2720
+ related_name="site_radomes",
2721
+ )
2722
+
2723
+ radome_serial_number = models.CharField(
2724
+ max_length=50,
2725
+ blank=True,
2726
+ default="",
2727
+ verbose_name=_("Radome Serial Number"),
2728
+ help_text=_("Enter the serial number of the radome if available"),
2729
+ db_index=True,
2730
+ )
2731
+
2732
+ cable_type = models.CharField(
2733
+ max_length=50,
2734
+ default="",
2735
+ blank=True,
2736
+ verbose_name=_("Antenna Cable Type"),
2737
+ help_text=_(
2738
+ "Enter the antenna cable specification if know. "
2739
+ "Format: (vendor & type number)"
2740
+ ),
2741
+ db_index=True,
2742
+ )
2743
+
2744
+ cable_length = models.FloatField(
2745
+ null=True,
2746
+ default=None,
2747
+ blank=True,
2748
+ verbose_name=_("Antenna Cable Length"),
2749
+ help_text=_("Enter the antenna cable length in meters. Format: (m)"),
2750
+ db_index=True,
2751
+ )
2752
+
2753
+ installed = models.DateTimeField(
2754
+ blank=False,
2755
+ verbose_name=_("Date Installed (UTC)"),
2756
+ help_text=_(
2757
+ "Enter the date the receiver was installed. " "Format: (CCYY-MM-DDThh:mmZ)"
2758
+ ),
2759
+ db_index=True,
2760
+ )
2761
+
2762
+ removed = models.DateTimeField(
2763
+ default=None,
2764
+ blank=True,
2765
+ null=True,
2766
+ verbose_name=_("Date Removed (UTC)"),
2767
+ help_text=_(
2768
+ "Enter the date the receiver was removed. It is important that "
2769
+ "the date removed is entered before the addition of a new "
2770
+ "receiver. Format: (CCYY-MM-DDThh:mmZ)"
2771
+ ),
2772
+ db_index=True,
2773
+ )
2774
+
2775
+ additional_information = models.TextField(
2776
+ blank=True,
2777
+ default="",
2778
+ verbose_name=_("Additional Information"),
2779
+ help_text=_(
2780
+ "Enter additional relevant information about the antenna, cable "
2781
+ "and radome. Indicate if a signal splitter has been used. "
2782
+ "Format: (multiple lines)"
2783
+ ),
2784
+ )
2785
+
2786
+ @property
2787
+ def graphic(self):
2788
+ if self.custom_graphic:
2789
+ return self.custom_graphic
2790
+ return self.antenna_type.graphic
2791
+
2792
+ custom_graphic = models.TextField(
2793
+ default="",
2794
+ blank=True,
2795
+ verbose_name=_("Antenna Graphic"),
2796
+ help_text=_(
2797
+ "A custom graphic may be provided, otherwise the default graphic "
2798
+ "for the antenna type will be used."
2799
+ ),
2800
+ )
2801
+
2802
+ def __str__(self):
2803
+ return str(self.antenna_type)
2804
+
2805
+ class Meta(SiteSubSection.Meta):
2806
+ indexes = [
2807
+ models.Index(fields=("site", "subsection", "published", "installed")),
2808
+ models.Index(fields=("subsection", "published", "installed")),
2809
+ ]
2810
+
2811
+
2812
+ class SiteSurveyedLocalTies(SiteSubSection):
2813
+ """
2814
+ 5. Surveyed Local Ties
2815
+
2816
+ 5.x Tied Marker Name :
2817
+ Tied Marker Usage : (SLR/VLBI/LOCAL CONTROL/FOOTPRINT/etc)
2818
+ Tied Marker CDP Number : (A4)
2819
+ Tied Marker DOMES Number : (A9)
2820
+ Differential Components from GNSS Marker to the tied monument (ITRS)
2821
+ dx (m) : (m)
2822
+ dy (m) : (m)
2823
+ dz (m) : (m)
2824
+ Accuracy (mm) : (mm)
2825
+ Survey method : (GPS CAMPAIGN/TRILATERATION/TRIANGULATION/etc)
2826
+ Date Measured : (CCYY-MM-DDThh:mmZ)
2827
+ Additional Information : (multiple lines)
2828
+ """
2829
+
2830
+ @classproperty
2831
+ def valid_time(cls):
2832
+ """
2833
+ surveyed local ties are always valid -
2834
+ todo is this correct even if date measured is not null??
2835
+ """
2836
+ return None
2837
+
2838
+ @classmethod
2839
+ def structure(cls):
2840
+ return [
2841
+ "name",
2842
+ "usage",
2843
+ "cdp_number",
2844
+ "domes_number",
2845
+ (
2846
+ _(
2847
+ "Differential Components from GNSS Marker to the tied "
2848
+ "monument (ITRS)"
2849
+ ),
2850
+ ("diff_xyz",),
2851
+ ),
2852
+ "accuracy",
2853
+ "survey_method",
2854
+ "measured",
2855
+ "additional_information",
2856
+ ]
2857
+
2858
+ @property
2859
+ def heading(self):
2860
+ return self.name
2861
+
2862
+ @property
2863
+ def effective(self):
2864
+ if self.measured:
2865
+ return f"{date_to_str(self.measured)}"
2866
+ return ""
2867
+
2868
+ @classmethod
2869
+ def section_number(cls):
2870
+ return 5
2871
+
2872
+ @classmethod
2873
+ def section_header(cls):
2874
+ return "Surveyed Local Ties"
2875
+
2876
+ @classmethod
2877
+ def subsection_number(cls):
2878
+ return None
2879
+
2880
+ name = models.CharField(
2881
+ max_length=50,
2882
+ default="",
2883
+ blank=True,
2884
+ verbose_name=_("Tied Marker Name"),
2885
+ help_text=_("Enter name of Tied Marker"),
2886
+ db_index=True,
2887
+ )
2888
+ usage = models.CharField(
2889
+ max_length=50,
2890
+ default="",
2891
+ blank=True,
2892
+ verbose_name=_("Tied Marker Usage"),
2893
+ help_text=_(
2894
+ "Enter the purpose of the tied marker such as SLR, VLBI, DORIS, "
2895
+ "or other. Format: (SLR/VLBI/LOCAL CONTROL/FOOTPRINT/etc)"
2896
+ ),
2897
+ db_index=True,
2898
+ )
2899
+ cdp_number = models.CharField(
2900
+ max_length=50,
2901
+ default="",
2902
+ blank=True,
2903
+ verbose_name=_("Tied Marker CDP Number"),
2904
+ help_text=_("Enter the NASA CDP identifier if available. Format: (A4)"),
2905
+ db_index=True,
2906
+ )
2907
+ domes_number = models.CharField(
2908
+ max_length=50,
2909
+ default="",
2910
+ blank=True,
2911
+ verbose_name=_("Tied Marker DOMES Number"),
2912
+ help_text=_("Enter the tied marker DOMES number if available. Format: (A9)"),
2913
+ db_index=True,
2914
+ )
2915
+
2916
+ diff_xyz = gis_models.PointField(
2917
+ srid=7789,
2918
+ dim=3,
2919
+ null=True,
2920
+ blank=True,
2921
+ default=None,
2922
+ db_index=True,
2923
+ help_text=_(
2924
+ "Enter the differential ITRF coordinates to one millimeter "
2925
+ "precision. Format: dx, dy, dz (m)"
2926
+ ),
2927
+ verbose_name=_("Δ XYZ (m)"),
2928
+ )
2929
+
2930
+ accuracy = models.FloatField(
2931
+ default=None,
2932
+ null=True,
2933
+ blank=True,
2934
+ verbose_name=_("Accuracy (mm)"),
2935
+ help_text=_("Enter the accuracy of the tied survey. Format: (mm)."),
2936
+ db_index=True,
2937
+ )
2938
+
2939
+ survey_method = models.CharField(
2940
+ max_length=50,
2941
+ default="",
2942
+ blank=True,
2943
+ verbose_name=_("Survey method"),
2944
+ help_text=_(
2945
+ "Enter the source or the survey method used to determine the "
2946
+ "differential coordinates, such as GNSS survey, conventional "
2947
+ "survey, or other. "
2948
+ "Format: (GPS CAMPAIGN/TRILATERATION/TRIANGULATION/etc)"
2949
+ ),
2950
+ db_index=True,
2951
+ )
2952
+
2953
+ measured = models.DateTimeField(
2954
+ null=True,
2955
+ blank=True,
2956
+ default=None,
2957
+ verbose_name=_("Date Measured (UTC)"),
2958
+ help_text=_(
2959
+ "Enter the date of the survey local ties measurement. "
2960
+ "Format: (CCYY-MM-DDThh:mmZ)"
2961
+ ),
2962
+ db_index=True,
2963
+ )
2964
+
2965
+ additional_information = models.TextField(
2966
+ default="",
2967
+ blank=True,
2968
+ verbose_name=_("Additional Information"),
2969
+ help_text=_(
2970
+ "Enter any additional information relevant to surveyed local ties."
2971
+ " Format: (multiple lines)"
2972
+ ),
2973
+ )
2974
+
2975
+
2976
+ class SiteFrequencyStandard(SiteSubSection):
2977
+ """
2978
+ 6. Frequency Standard
2979
+
2980
+ 6.x Standard Type : (INTERNAL or EXTERNAL H-MASER/CESIUM/etc)
2981
+ Input Frequency : (if external)
2982
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
2983
+ Notes : (multiple lines)
2984
+ """
2985
+
2986
+ @classmethod
2987
+ def structure(cls):
2988
+ return [("standard_type", ("input_frequency", "effective_dates", "notes"))]
2989
+
2990
+ @property
2991
+ def heading(self):
2992
+ return (
2993
+ self.standard_type.label
2994
+ if isinstance(self.standard_type, Enum)
2995
+ else str(self.standard_type)
2996
+ )
2997
+
2998
+ @property
2999
+ def effective(self):
3000
+ if self.effective_start and self.effective_end:
3001
+ return (
3002
+ f"{date_to_str(self.effective_start)}/"
3003
+ f"{date_to_str(self.effective_end)}"
3004
+ )
3005
+ elif self.effective_start:
3006
+ return f"{date_to_str(self.effective_start)}"
3007
+ return ""
3008
+
3009
+ @classmethod
3010
+ def section_number(cls):
3011
+ return 6
3012
+
3013
+ @classmethod
3014
+ def section_header(cls):
3015
+ return "Frequency Standard"
3016
+
3017
+ @classmethod
3018
+ def subsection_number(cls):
3019
+ return None
3020
+
3021
+ standard_type = EnumField(
3022
+ FrequencyStandardType,
3023
+ max_length=50,
3024
+ strict=False,
3025
+ blank=True,
3026
+ null=True,
3027
+ default=None,
3028
+ verbose_name=_("Standard Type"),
3029
+ help_text=_(
3030
+ "Select whether the frequency standard is INTERNAL or EXTERNAL "
3031
+ "and describe the oscillator type. "
3032
+ "Format: (INTERNAL or EXTERNAL H-MASER/CESIUM/etc)"
3033
+ ),
3034
+ db_index=True,
3035
+ )
3036
+
3037
+ input_frequency = models.FloatField(
3038
+ null=True,
3039
+ blank=True,
3040
+ default=None,
3041
+ verbose_name=_("Input Frequency (MHz)"),
3042
+ help_text=_("Enter the input frequency in MHz if known."),
3043
+ db_index=True,
3044
+ )
3045
+
3046
+ notes = models.TextField(
3047
+ blank=True,
3048
+ default="",
3049
+ verbose_name=_("Notes"),
3050
+ help_text=_(
3051
+ "Enter any additional information relevant to frequency standard. "
3052
+ "Format: (multiple lines)"
3053
+ ),
3054
+ )
3055
+
3056
+ effective_start = models.DateField(
3057
+ blank=True,
3058
+ null=True,
3059
+ default=None,
3060
+ help_text=_(
3061
+ "Enter the effective start date for the frequency standard. "
3062
+ "Format: (CCYY-MM-DD)"
3063
+ ),
3064
+ db_index=True,
3065
+ )
3066
+
3067
+ effective_end = models.DateField(
3068
+ blank=True,
3069
+ null=True,
3070
+ default=None,
3071
+ help_text=_(
3072
+ "Enter the effective end date for the frequency standard. "
3073
+ "Format: (CCYY-MM-DD)"
3074
+ ),
3075
+ db_index=True,
3076
+ )
3077
+
3078
+ def effective_dates(self):
3079
+ return self.effective
3080
+
3081
+ effective_dates.field = (effective_start, effective_end)
3082
+ effective_dates.verbose_name = _("Effective Dates")
3083
+
3084
+ class Meta(SiteSubSection.Meta):
3085
+ indexes = [
3086
+ models.Index(fields=("site", "subsection", "published", "effective_start")),
3087
+ models.Index(fields=("subsection", "published", "effective_start")),
3088
+ ]
3089
+
3090
+
3091
+ class SiteCollocation(SiteSubSection):
3092
+ """
3093
+ 7. Collocation Information
3094
+
3095
+ 7.1 Instrumentation Type : (GPS/GLONASS/DORIS/PRARE/SLR/VLBI/TIME/etc)
3096
+ Status : (PERMANENT/MOBILE)
3097
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3098
+ Notes : (multiple lines)
3099
+
3100
+ 7.x Instrumentation Type : (GPS/GLONASS/DORIS/PRARE/SLR/VLBI/TIME/etc)
3101
+ Status : (PERMANENT/MOBILE)
3102
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3103
+ Notes : (multiple lines)
3104
+ """
3105
+
3106
+ @classmethod
3107
+ def structure(cls):
3108
+ return [("instrument_type", ("status", "effective_dates", "notes"))]
3109
+
3110
+ @property
3111
+ def heading(self):
3112
+ return self.instrument_type
3113
+
3114
+ @property
3115
+ def effective(self):
3116
+ if self.effective_start and self.effective_end:
3117
+ return (
3118
+ f"{date_to_str(self.effective_start)}/"
3119
+ f"{date_to_str(self.effective_end)}"
3120
+ )
3121
+ elif self.effective_start:
3122
+ return f"{date_to_str(self.effective_start)}"
3123
+ return ""
3124
+
3125
+ @classmethod
3126
+ def section_number(cls):
3127
+ return 7
3128
+
3129
+ @classmethod
3130
+ def subsection_number(cls):
3131
+ return None
3132
+
3133
+ @classmethod
3134
+ def section_header(cls):
3135
+ return "Collocation Information"
3136
+
3137
+ instrument_type = models.CharField(
3138
+ max_length=50,
3139
+ blank=True,
3140
+ default="",
3141
+ verbose_name=_("Instrumentation Type"),
3142
+ help_text=_("Select all collocated instrument types that apply"),
3143
+ db_index=True,
3144
+ )
3145
+
3146
+ status = EnumField(
3147
+ CollocationStatus,
3148
+ max_length=50,
3149
+ strict=False,
3150
+ blank=True,
3151
+ null=True,
3152
+ default=None,
3153
+ verbose_name=_("Status"),
3154
+ help_text=_("Select appropriate status"),
3155
+ db_index=True,
3156
+ )
3157
+
3158
+ notes = models.TextField(
3159
+ blank=True,
3160
+ default="",
3161
+ verbose_name=_("Notes"),
3162
+ help_text=_(
3163
+ "Enter any additional information relevant to collocation. "
3164
+ "Format: (multiple lines)"
3165
+ ),
3166
+ )
3167
+
3168
+ effective_start = models.DateField(
3169
+ max_length=50,
3170
+ blank=True,
3171
+ null=True,
3172
+ default=None,
3173
+ help_text=_(
3174
+ "Enter the effective start date of the collocated instrument. "
3175
+ "Format: (CCYY-MM-DD)"
3176
+ ),
3177
+ db_index=True,
3178
+ )
3179
+ effective_end = models.DateField(
3180
+ max_length=50,
3181
+ blank=True,
3182
+ null=True,
3183
+ default=None,
3184
+ help_text=_(
3185
+ "Enter the effective end date of the collocated instrument. "
3186
+ "Format: (CCYY-MM-DD)"
3187
+ ),
3188
+ db_index=True,
3189
+ )
3190
+
3191
+ def effective_dates(self):
3192
+ return self.effective
3193
+
3194
+ effective_dates.field = (effective_start, effective_end)
3195
+ effective_dates.verbose_name = _("Effective Dates")
3196
+
3197
+ class Meta(SiteSubSection.Meta):
3198
+ indexes = [
3199
+ models.Index(fields=("site", "subsection", "published", "effective_start")),
3200
+ models.Index(fields=("subsection", "published", "effective_start")),
3201
+ ]
3202
+
3203
+
3204
+ class MeteorologicalInstrumentation(SiteSubSection):
3205
+ """
3206
+ 8. Meteorological Instrumentation
3207
+
3208
+ 8.x.x ...
3209
+ Manufacturer :
3210
+ Serial Number :
3211
+ Height Diff to Ant : (m)
3212
+ Calibration date : (CCYY-MM-DD)
3213
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3214
+ Notes : (multiple lines)
3215
+ """
3216
+
3217
+ @classmethod
3218
+ def structure(cls):
3219
+ return [
3220
+ "manufacturer",
3221
+ "serial_number",
3222
+ "height_diff",
3223
+ "calibration",
3224
+ "effective_dates",
3225
+ "notes",
3226
+ ]
3227
+
3228
+ @property
3229
+ def effective(self):
3230
+ if self.effective_start and self.effective_end:
3231
+ return (
3232
+ f"{date_to_str(self.effective_start)}/"
3233
+ f"{date_to_str(self.effective_end)}"
3234
+ )
3235
+ elif self.effective_start:
3236
+ return f"{date_to_str(self.effective_start)}"
3237
+ return ""
3238
+
3239
+ @classmethod
3240
+ def section_number(cls):
3241
+ return 8
3242
+
3243
+ @classmethod
3244
+ def section_header(cls):
3245
+ return "Meteorological Instrumentation"
3246
+
3247
+ manufacturer = models.CharField(
3248
+ max_length=255,
3249
+ blank=True,
3250
+ default="",
3251
+ verbose_name=_("Manufacturer"),
3252
+ help_text=_("Enter manufacturer's name"),
3253
+ db_index=True,
3254
+ )
3255
+ serial_number = models.CharField(
3256
+ max_length=50,
3257
+ blank=True,
3258
+ default="",
3259
+ verbose_name=_("Serial Number"),
3260
+ help_text=_("Enter the serial number of the sensor"),
3261
+ db_index=True,
3262
+ )
3263
+
3264
+ height_diff = models.FloatField(
3265
+ null=True,
3266
+ blank=True,
3267
+ default=None,
3268
+ verbose_name=_("Height Diff to Ant (m)"),
3269
+ help_text=_(
3270
+ "In meters, enter the difference in height between the sensor and "
3271
+ "the GNSS antenna. Positive number indicates the sensor is above "
3272
+ "the GNSS antenna. Decimeter accuracy preferred. Format: (m)"
3273
+ ),
3274
+ db_index=True,
3275
+ )
3276
+
3277
+ calibration = models.DateField(
3278
+ null=True,
3279
+ blank=True,
3280
+ default=None,
3281
+ verbose_name=_("Calibration Date"),
3282
+ help_text=_("Enter the date the sensor was calibrated. Format: (CCYY-MM-DD)"),
3283
+ db_index=True,
3284
+ )
3285
+
3286
+ def calibration_date(self):
3287
+ return date_to_str(self.calibration)
3288
+
3289
+ calibration_date.verbose_name = _("Calibration Date")
3290
+ calibration_date.field = calibration
3291
+
3292
+ effective_start = models.DateField(
3293
+ blank=False,
3294
+ null=True,
3295
+ help_text=_(
3296
+ "Enter the effective start date for the sensor. " "Format: (CCYY-MM-DD)"
3297
+ ),
3298
+ db_index=True,
3299
+ )
3300
+ effective_end = models.DateField(
3301
+ null=True,
3302
+ blank=True,
3303
+ default=None,
3304
+ help_text=_(
3305
+ "Enter the effective end date for the sensor. " "Format: (CCYY-MM-DD)"
3306
+ ),
3307
+ db_index=True,
3308
+ )
3309
+
3310
+ notes = models.TextField(
3311
+ blank=True,
3312
+ default="",
3313
+ verbose_name=_("Notes"),
3314
+ help_text=_(
3315
+ "Enter any additional information relevant to the sensor."
3316
+ " Format: (multiple lines)"
3317
+ ),
3318
+ )
3319
+
3320
+ def effective_dates(self):
3321
+ return self.effective
3322
+
3323
+ effective_dates.field = (effective_start, effective_end)
3324
+ effective_dates.verbose_name = _("Effective Dates")
3325
+
3326
+ class Meta(SiteSubSection.Meta):
3327
+ abstract = True
3328
+ indexes = [
3329
+ models.Index(fields=("site", "subsection", "published", "effective_start")),
3330
+ models.Index(fields=("subsection", "published", "effective_start")),
3331
+ ]
3332
+
3333
+
3334
+ class SiteHumiditySensor(MeteorologicalInstrumentation):
3335
+ """
3336
+ 8.1.1 Humidity Sensor Model :
3337
+ Manufacturer :
3338
+ Serial Number :
3339
+ Data Sampling Interval : (sec)
3340
+ Accuracy (% rel h) : (% rel h)
3341
+ Aspiration : (UNASPIRATED/NATURAL/FAN/etc)
3342
+ Height Diff to Ant : (m)
3343
+ Calibration date : (CCYY-MM-DD)
3344
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3345
+ Notes : (multiple lines)
3346
+ """
3347
+
3348
+ @classmethod
3349
+ def structure(cls):
3350
+ return [
3351
+ "model",
3352
+ "manufacturer",
3353
+ "serial_number",
3354
+ "sampling_interval",
3355
+ "accuracy",
3356
+ "aspiration",
3357
+ "height_diff",
3358
+ "calibration_date",
3359
+ "effective_dates",
3360
+ "notes",
3361
+ ]
3362
+
3363
+ @property
3364
+ def heading(self):
3365
+ return self.model
3366
+
3367
+ @classmethod
3368
+ def subsection_number(cls):
3369
+ return 1
3370
+
3371
+ model = models.CharField(
3372
+ max_length=255,
3373
+ blank=True,
3374
+ default="",
3375
+ verbose_name=_("Humidity Sensor Model"),
3376
+ help_text=_("Enter humidity sensor model"),
3377
+ db_index=True,
3378
+ )
3379
+
3380
+ sampling_interval = models.PositiveSmallIntegerField(
3381
+ default=None,
3382
+ null=True,
3383
+ blank=True,
3384
+ verbose_name=_("Data Sampling Interval (sec)"),
3385
+ help_text=_("Enter the sample interval in seconds. Format: (sec)"),
3386
+ db_index=True,
3387
+ )
3388
+
3389
+ accuracy = models.FloatField(
3390
+ default=None,
3391
+ blank=True,
3392
+ null=True,
3393
+ verbose_name=_("Accuracy (% rel h)"),
3394
+ help_text=_("Enter the accuracy in % relative humidity. Format: (% rel h)"),
3395
+ db_index=True,
3396
+ )
3397
+
3398
+ aspiration = EnumField(
3399
+ Aspiration,
3400
+ null=True,
3401
+ default=None,
3402
+ blank=True,
3403
+ strict=False,
3404
+ max_length=50,
3405
+ verbose_name=_("Aspiration"),
3406
+ help_text=_(
3407
+ "Enter the aspiration type if known. "
3408
+ "Format: (UNASPIRATED/NATURAL/FAN/etc)"
3409
+ ),
3410
+ db_index=True,
3411
+ )
3412
+
3413
+
3414
+ class SitePressureSensor(MeteorologicalInstrumentation):
3415
+ """
3416
+ 8.2.x Pressure Sensor Model :
3417
+ Manufacturer :
3418
+ Serial Number :
3419
+ Data Sampling Interval : (sec)
3420
+ Accuracy : (hPa)
3421
+ Height Diff to Ant : (m)
3422
+ Calibration date : (CCYY-MM-DD)
3423
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3424
+ Notes : (multiple lines)
3425
+ """
3426
+
3427
+ @classmethod
3428
+ def structure(cls):
3429
+ return [
3430
+ "model",
3431
+ "manufacturer",
3432
+ "serial_number",
3433
+ "sampling_interval",
3434
+ "accuracy",
3435
+ "height_diff",
3436
+ "calibration_date",
3437
+ "effective_dates",
3438
+ "notes",
3439
+ ]
3440
+
3441
+ @property
3442
+ def heading(self):
3443
+ return self.model
3444
+
3445
+ @classmethod
3446
+ def subsection_number(cls):
3447
+ return 2
3448
+
3449
+ model = models.CharField(
3450
+ max_length=255,
3451
+ blank=False,
3452
+ verbose_name=_("Pressure Sensor Model"),
3453
+ help_text=_("Enter pressure sensor model"),
3454
+ db_index=True,
3455
+ )
3456
+
3457
+ sampling_interval = models.PositiveSmallIntegerField(
3458
+ default=None,
3459
+ null=True,
3460
+ blank=True,
3461
+ verbose_name=_("Data Sampling Interval"),
3462
+ help_text=_("Enter the sample interval in seconds. Format: (sec)"),
3463
+ db_index=True,
3464
+ )
3465
+
3466
+ accuracy = models.FloatField(
3467
+ default=None,
3468
+ null=True,
3469
+ blank=True,
3470
+ verbose_name=_("Accuracy (hPa)"),
3471
+ help_text=_("Enter the accuracy in hectopascal. Format: (hPa)"),
3472
+ db_index=True,
3473
+ )
3474
+
3475
+
3476
+ class SiteTemperatureSensor(MeteorologicalInstrumentation):
3477
+ """
3478
+ 8.3.x Temp. Sensor Model :
3479
+ Manufacturer :
3480
+ Serial Number :
3481
+ Data Sampling Interval : (sec)
3482
+ Accuracy : (deg C)
3483
+ Aspiration : (UNASPIRATED/NATURAL/FAN/etc)
3484
+ Height Diff to Ant : (m)
3485
+ Calibration date : (CCYY-MM-DD)
3486
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3487
+ Notes : (multiple lines)
3488
+ """
3489
+
3490
+ @classmethod
3491
+ def structure(cls):
3492
+ return [
3493
+ "model",
3494
+ "manufacturer",
3495
+ "serial_number",
3496
+ "sampling_interval",
3497
+ "accuracy",
3498
+ "aspiration",
3499
+ "height_diff",
3500
+ "calibration_date",
3501
+ "effective_dates",
3502
+ "notes",
3503
+ ]
3504
+
3505
+ @property
3506
+ def heading(self):
3507
+ return self.model
3508
+
3509
+ @classmethod
3510
+ def subsection_number(cls):
3511
+ return 3
3512
+
3513
+ model = models.CharField(
3514
+ max_length=255,
3515
+ blank=True,
3516
+ default="",
3517
+ verbose_name=_("Temp. Sensor Model"),
3518
+ help_text=_("Enter temperature sensor model"),
3519
+ db_index=True,
3520
+ )
3521
+
3522
+ sampling_interval = models.PositiveSmallIntegerField(
3523
+ default=None,
3524
+ null=True,
3525
+ blank=True,
3526
+ verbose_name=_("Data Sampling Interval"),
3527
+ help_text=_("Enter the sample interval in seconds. Format: (sec)"),
3528
+ db_index=True,
3529
+ )
3530
+
3531
+ accuracy = models.FloatField(
3532
+ default=None,
3533
+ null=True,
3534
+ blank=True,
3535
+ verbose_name=_("Accuracy (deg C)"),
3536
+ help_text=_("Enter the accuracy in degrees Centigrade. Format: (deg C)"),
3537
+ db_index=True,
3538
+ )
3539
+
3540
+ aspiration = EnumField(
3541
+ Aspiration,
3542
+ null=True,
3543
+ default=None,
3544
+ blank=True,
3545
+ strict=False,
3546
+ max_length=50,
3547
+ verbose_name=_("Aspiration"),
3548
+ help_text=_(
3549
+ "Enter the aspiration type if known. "
3550
+ "Format: (UNASPIRATED/NATURAL/FAN/etc)"
3551
+ ),
3552
+ db_index=True,
3553
+ )
3554
+
3555
+
3556
+ class SiteWaterVaporRadiometer(MeteorologicalInstrumentation):
3557
+ """
3558
+ 8.4.x Water Vapor Radiometer :
3559
+ Manufacturer :
3560
+ Serial Number :
3561
+ Distance to Antenna : (m)
3562
+ Height Diff to Ant : (m)
3563
+ Calibration date : (CCYY-MM-DD)
3564
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3565
+ Notes : (multiple lines)
3566
+ """
3567
+
3568
+ @classmethod
3569
+ def structure(cls):
3570
+ return [
3571
+ "model",
3572
+ "manufacturer",
3573
+ "serial_number",
3574
+ "distance_to_antenna",
3575
+ "height_diff",
3576
+ "calibration_date",
3577
+ "effective_dates",
3578
+ "notes",
3579
+ ]
3580
+
3581
+ @property
3582
+ def heading(self):
3583
+ return self.model
3584
+
3585
+ @classmethod
3586
+ def subsection_number(cls):
3587
+ return 4
3588
+
3589
+ model = models.CharField(
3590
+ max_length=255,
3591
+ blank=True,
3592
+ default="",
3593
+ verbose_name=_("Water Vapor Radiometer"),
3594
+ help_text=_("Enter water vapor radiometer"),
3595
+ db_index=True,
3596
+ )
3597
+
3598
+ distance_to_antenna = models.FloatField(
3599
+ default=None,
3600
+ blank=True,
3601
+ null=True,
3602
+ verbose_name=_("Distance to Antenna (m)"),
3603
+ help_text=_(
3604
+ "Enter the horizontal distance between the WVR and the GNSS "
3605
+ "antenna to the nearest meter. Format: (m)"
3606
+ ),
3607
+ db_index=True,
3608
+ )
3609
+
3610
+
3611
+ class SiteOtherInstrumentation(SiteSubSection):
3612
+ """
3613
+ 8.5.x Other Instrumentation : (multiple lines)
3614
+ """
3615
+
3616
+ @classproperty
3617
+ def valid_time(cls):
3618
+ return None
3619
+
3620
+ @classmethod
3621
+ def structure(cls):
3622
+ return ["instrumentation"]
3623
+
3624
+ @property
3625
+ def heading(self):
3626
+ return self.instrumentation
3627
+
3628
+ @property
3629
+ def effective(self):
3630
+ return ""
3631
+
3632
+ @classmethod
3633
+ def section_number(cls):
3634
+ return 8
3635
+
3636
+ @classmethod
3637
+ def subsection_number(cls):
3638
+ return 5
3639
+
3640
+ @classmethod
3641
+ def section_header(cls):
3642
+ return None
3643
+
3644
+ instrumentation = models.TextField(
3645
+ blank=True,
3646
+ default="",
3647
+ verbose_name=_("Other Instrumentation"),
3648
+ help_text=_(
3649
+ "Enter any other relevant information regarding meteorological "
3650
+ "instrumentation near the site. Format: (multiple lines)"
3651
+ ),
3652
+ )
3653
+
3654
+
3655
+ class Condition(SiteSubSection):
3656
+ """
3657
+ 9. Local Ongoing Conditions Possibly Affecting Computed Position
3658
+
3659
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3660
+ Additional Information : (multiple lines)
3661
+ """
3662
+
3663
+ @property
3664
+ def effective(self):
3665
+ if self.effective_start and self.effective_end:
3666
+ return (
3667
+ f"{date_to_str(self.effective_start)}/"
3668
+ f"{date_to_str(self.effective_end)}"
3669
+ )
3670
+ elif self.effective_start:
3671
+ return f"{date_to_str(self.effective_start)}"
3672
+ return ""
3673
+
3674
+ @classmethod
3675
+ def section_number(cls):
3676
+ return 9
3677
+
3678
+ @classmethod
3679
+ def section_header(cls):
3680
+ return "Local Ongoing Conditions Possibly Affecting Computed Position"
3681
+
3682
+ effective_start = models.DateField(
3683
+ blank=True,
3684
+ null=True,
3685
+ default=None,
3686
+ help_text=_(
3687
+ "Enter the effective start date for the condition. " "Format: (CCYY-MM-DD)"
3688
+ ),
3689
+ db_index=True,
3690
+ )
3691
+
3692
+ effective_end = models.DateField(
3693
+ blank=True,
3694
+ null=True,
3695
+ default=None,
3696
+ help_text=_(
3697
+ "Enter the effective end date for the condition. " "Format: (CCYY-MM-DD)"
3698
+ ),
3699
+ db_index=True,
3700
+ )
3701
+
3702
+ additional_information = models.TextField(
3703
+ default="",
3704
+ blank=True,
3705
+ verbose_name=_("Additional Information"),
3706
+ help_text=_(
3707
+ "Enter additional relevant information about any radio "
3708
+ "interferences. Format: (multiple lines)"
3709
+ ),
3710
+ )
3711
+
3712
+ def effective_dates(self):
3713
+ return self.effective
3714
+
3715
+ effective_dates.field = (effective_start, effective_end)
3716
+ effective_dates.verbose_name = _("Effective Dates")
3717
+
3718
+ class Meta(SiteSubSection.Meta):
3719
+ abstract = True
3720
+ indexes = [
3721
+ models.Index(fields=("site", "subsection", "published", "effective_start")),
3722
+ models.Index(fields=("subsection", "published", "effective_start")),
3723
+ ]
3724
+
3725
+
3726
+ class SiteRadioInterferences(Condition):
3727
+ """
3728
+ 9. Local Ongoing Conditions Possibly Affecting Computed Position
3729
+
3730
+ 9.1.x Radio Interferences : (TV/CELL PHONE ANTENNA/RADAR/etc)
3731
+ Observed Degradations : (SN RATIO/DATA GAPS/etc)
3732
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3733
+ Additional Information : (multiple lines)
3734
+ """
3735
+
3736
+ @classmethod
3737
+ def structure(cls):
3738
+ return [
3739
+ "interferences",
3740
+ "degradations",
3741
+ "effective_dates",
3742
+ "additional_information",
3743
+ ]
3744
+
3745
+ @property
3746
+ def heading(self):
3747
+ return self.interferences
3748
+
3749
+ @classmethod
3750
+ def subsection_number(cls):
3751
+ return 1
3752
+
3753
+ interferences = models.CharField(
3754
+ max_length=50,
3755
+ default="",
3756
+ blank=True,
3757
+ verbose_name=_("Radio Interferences"),
3758
+ help_text=_(
3759
+ "Enter all sources of radio interference near the GNSS station. "
3760
+ "Format: (TV/CELL PHONE ANTENNA/RADAR/etc)"
3761
+ ),
3762
+ db_index=True,
3763
+ )
3764
+ degradations = models.CharField(
3765
+ max_length=50,
3766
+ default="",
3767
+ blank=True,
3768
+ verbose_name=_("Observed Degradations"),
3769
+ help_text=_(
3770
+ "Describe any observed degradations in the GNSS data that are "
3771
+ "presumed to result from radio interference. "
3772
+ "Format: (SN RATIO/DATA GAPS/etc)"
3773
+ ),
3774
+ db_index=True,
3775
+ )
3776
+
3777
+
3778
+ class SiteMultiPathSources(Condition):
3779
+ """
3780
+ 9.2.x Multipath Sources : (METAL ROOF/DOME/VLBI ANTENNA/etc)
3781
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3782
+ Additional Information : (multiple lines)
3783
+ """
3784
+
3785
+ @classmethod
3786
+ def structure(cls):
3787
+ return ["sources", "effective_dates", "additional_information"]
3788
+
3789
+ @property
3790
+ def heading(self):
3791
+ return self.sources
3792
+
3793
+ @classmethod
3794
+ def subsection_number(cls):
3795
+ return 2
3796
+
3797
+ sources = models.CharField(
3798
+ max_length=50,
3799
+ default="",
3800
+ blank=True,
3801
+ verbose_name=_("Multipath Sources"),
3802
+ help_text=_(
3803
+ "Describe any potential multipath sources near the GNSS station. "
3804
+ "Format: .(METAL ROOF/DOME/VLBI ANTENNA/etc)"
3805
+ ),
3806
+ db_index=True,
3807
+ )
3808
+
3809
+
3810
+ class SiteSignalObstructions(Condition):
3811
+ """
3812
+ 9.3.x Signal Obstructions : (TREES/BUILDINGS/etc)
3813
+ Effective Dates : (CCYY-MM-DD/CCYY-MM-DD)
3814
+ Additional Information : (multiple lines)
3815
+ """
3816
+
3817
+ @classmethod
3818
+ def structure(cls):
3819
+ return ["obstructions", "effective_dates", "additional_information"]
3820
+
3821
+ @property
3822
+ def heading(self):
3823
+ return self.obstructions
3824
+
3825
+ @classmethod
3826
+ def subsection_number(cls):
3827
+ return 3
3828
+
3829
+ obstructions = models.CharField(
3830
+ max_length=50,
3831
+ default="",
3832
+ blank=True,
3833
+ verbose_name=_("Signal Obstructions"),
3834
+ help_text=_(
3835
+ "Describe any potential signal obstructions near the GNSS station."
3836
+ " Format: (TREES/BUILDLINGS/etc)"
3837
+ ),
3838
+ db_index=True,
3839
+ )
3840
+
3841
+
3842
+ class SiteLocalEpisodicEffects(SiteSubSection):
3843
+ """
3844
+ 10. Local Episodic Effects Possibly Affecting Data Quality
3845
+
3846
+ 10.x Date : (CCYY-MM-DD/CCYY-MM-DD)
3847
+ Event : (TREE CLEARING/CONSTRUCTION/etc)
3848
+ """
3849
+
3850
+ @classmethod
3851
+ def structure(cls):
3852
+ return ["date", "event"]
3853
+
3854
+ @property
3855
+ def heading(self):
3856
+ return self.event
3857
+
3858
+ @property
3859
+ def effective(self):
3860
+ if self.effective_start and self.effective_end:
3861
+ return (
3862
+ f"{date_to_str(self.effective_start)}/"
3863
+ f"{date_to_str(self.effective_end)}"
3864
+ )
3865
+ elif self.effective_start:
3866
+ return f"{date_to_str(self.effective_start)}"
3867
+ return ""
3868
+
3869
+ @classmethod
3870
+ def section_number(cls):
3871
+ return 10
3872
+
3873
+ @classmethod
3874
+ def subsection_number(cls):
3875
+ return None
3876
+
3877
+ @classmethod
3878
+ def section_header(cls):
3879
+ return "Local Episodic Effects Possibly Affecting Data Quality"
3880
+
3881
+ event = models.TextField(
3882
+ default="",
3883
+ blank=True,
3884
+ verbose_name=_("Event"),
3885
+ help_text=_(
3886
+ "Describe any events near the GNSS station that may affect data "
3887
+ "quality such as tree clearing, construction, or weather events. "
3888
+ "Format: (TREE CLEARING/CONSTRUCTION/etc)"
3889
+ ),
3890
+ )
3891
+ effective_start = models.DateField(
3892
+ blank=True,
3893
+ default=None,
3894
+ null=True,
3895
+ help_text=_(
3896
+ "Enter the effective start date for the local episodic effect. "
3897
+ "Format: (CCYY-MM-DD)"
3898
+ ),
3899
+ db_index=True,
3900
+ )
3901
+
3902
+ effective_end = models.DateField(
3903
+ blank=True,
3904
+ default=None,
3905
+ null=True,
3906
+ help_text=_(
3907
+ "Enter the effective end date for the local episodic effect. "
3908
+ "Format: (CCYY-MM-DD)"
3909
+ ),
3910
+ db_index=True,
3911
+ )
3912
+
3913
+ def date(self):
3914
+ return self.effective
3915
+
3916
+ date.field = (effective_start, effective_end)
3917
+ date.verbose_name = _("Date")
3918
+
3919
+ class Meta(SiteSubSection.Meta):
3920
+ indexes = [
3921
+ models.Index(fields=("site", "subsection", "published", "effective_start")),
3922
+ models.Index(fields=("subsection", "published", "effective_start")),
3923
+ ]
3924
+
3925
+
3926
+ class AgencyPOC(SiteSection):
3927
+ """
3928
+ Agency : (multiple lines)
3929
+ Preferred Abbreviation : (A10)
3930
+ Mailing Address : (multiple lines)
3931
+ Primary Contact
3932
+ Contact Name :
3933
+ Telephone (primary) :
3934
+ Telephone (secondary) :
3935
+ Fax :
3936
+ E-mail :
3937
+ Secondary Contact
3938
+ Contact Name :
3939
+ Telephone (primary) :
3940
+ Telephone (secondary) :
3941
+ Fax :
3942
+ E-mail :
3943
+ Additional Information : (multiple lines)
3944
+ """
3945
+
3946
+ @classmethod
3947
+ def structure(cls):
3948
+ return [
3949
+ "agency",
3950
+ "preferred_abbreviation",
3951
+ "mailing_address",
3952
+ (
3953
+ _("Primary Contact (Organization Only)"),
3954
+ (
3955
+ "primary_name",
3956
+ "primary_phone1",
3957
+ "primary_phone2",
3958
+ "primary_fax",
3959
+ "primary_email",
3960
+ ),
3961
+ ),
3962
+ (
3963
+ _("Secondary Contact"),
3964
+ (
3965
+ "secondary_name",
3966
+ "secondary_phone1",
3967
+ "secondary_phone2",
3968
+ "secondary_fax",
3969
+ "secondary_email",
3970
+ ),
3971
+ ),
3972
+ "additional_information",
3973
+ ]
3974
+
3975
+ agency = models.TextField(
3976
+ max_length=300,
3977
+ blank=True,
3978
+ default="",
3979
+ verbose_name=_("Agency"),
3980
+ help_text=_("Enter contact agency name"),
3981
+ )
3982
+ preferred_abbreviation = models.CharField(
3983
+ max_length=50, # todo A10
3984
+ blank=True,
3985
+ default="",
3986
+ verbose_name=_("Preferred Abbreviation"),
3987
+ help_text=_("Enter the contact agency's preferred abbreviation"),
3988
+ db_index=True,
3989
+ )
3990
+ mailing_address = models.TextField(
3991
+ max_length=300,
3992
+ default="",
3993
+ blank=True,
3994
+ verbose_name=_("Mailing Address"),
3995
+ help_text=_("Enter agency mailing address"),
3996
+ )
3997
+
3998
+ primary_name = models.CharField(
3999
+ max_length=50,
4000
+ blank=True,
4001
+ default="",
4002
+ verbose_name=_("Contact Name"),
4003
+ help_text=_("Enter primary contact organization name"),
4004
+ db_index=True,
4005
+ )
4006
+ primary_phone1 = models.CharField(
4007
+ max_length=50,
4008
+ blank=True,
4009
+ default="",
4010
+ verbose_name=_("Telephone (primary)"),
4011
+ help_text=_("Enter primary contact primary phone number"),
4012
+ db_index=True,
4013
+ )
4014
+ primary_phone2 = models.CharField(
4015
+ max_length=50,
4016
+ default="",
4017
+ blank=True,
4018
+ verbose_name=_("Telephone (secondary)"),
4019
+ help_text=_("Enter primary contact secondary phone number"),
4020
+ db_index=True,
4021
+ )
4022
+ primary_fax = models.CharField(
4023
+ max_length=50,
4024
+ default="",
4025
+ blank=True,
4026
+ verbose_name=_("Fax"),
4027
+ help_text=_("Enter primary contact organization fax number"),
4028
+ db_index=True,
4029
+ )
4030
+ primary_email = models.EmailField(
4031
+ blank=True,
4032
+ default="",
4033
+ verbose_name=_("E-mail"),
4034
+ help_text=_(
4035
+ "Enter primary contact organization email address. MUST be a "
4036
+ "generic email, no personal email addresses."
4037
+ ),
4038
+ db_index=True,
4039
+ )
4040
+
4041
+ secondary_name = models.CharField(
4042
+ max_length=50,
4043
+ default="",
4044
+ blank=True,
4045
+ verbose_name=_("Contact Name"),
4046
+ help_text=_("Enter secondary contact name"),
4047
+ db_index=True,
4048
+ )
4049
+ secondary_phone1 = models.CharField(
4050
+ max_length=50,
4051
+ default="",
4052
+ blank=True,
4053
+ verbose_name=_("Telephone (primary)"),
4054
+ help_text=_("Enter secondary contact primary phone number"),
4055
+ db_index=True,
4056
+ )
4057
+ secondary_phone2 = models.CharField(
4058
+ max_length=50,
4059
+ default="",
4060
+ blank=True,
4061
+ verbose_name=_("Telephone (secondary)"),
4062
+ help_text=_("Enter secondary contact secondary phone number"),
4063
+ db_index=True,
4064
+ )
4065
+ secondary_fax = models.CharField(
4066
+ max_length=50,
4067
+ default="",
4068
+ blank=True,
4069
+ verbose_name=_("Fax"),
4070
+ help_text=_("Enter secondary contact fax number"),
4071
+ db_index=True,
4072
+ )
4073
+ secondary_email = models.EmailField(
4074
+ default="",
4075
+ blank=True,
4076
+ verbose_name=_("E-mail"),
4077
+ help_text=_("Enter secondary contact email address"),
4078
+ db_index=True,
4079
+ )
4080
+
4081
+ additional_information = models.TextField(
4082
+ default="",
4083
+ blank=True,
4084
+ verbose_name=_("Additional Information"),
4085
+ help_text=_(
4086
+ "Enter additional relevant information regarding operational "
4087
+ "contacts. Format: (multiple lines)."
4088
+ ),
4089
+ )
4090
+
4091
+ class Meta:
4092
+ abstract = True
4093
+
4094
+
4095
+ class SiteOperationalContact(AgencyPOC):
4096
+ """
4097
+ 11. On-Site, Point of Contact Agency Information
4098
+
4099
+ Agency : (multiple lines)
4100
+ Preferred Abbreviation : (A10)
4101
+ Mailing Address : (multiple lines)
4102
+ Primary Contact
4103
+ Contact Name :
4104
+ Telephone (primary) :
4105
+ Telephone (secondary) :
4106
+ Fax :
4107
+ E-mail :
4108
+ Secondary Contact
4109
+ Contact Name :
4110
+ Telephone (primary) :
4111
+ Telephone (secondary) :
4112
+ Fax :
4113
+ E-mail :
4114
+ Additional Information : (multiple lines)
4115
+ """
4116
+
4117
+ @classmethod
4118
+ def section_number(cls):
4119
+ return 11
4120
+
4121
+ @classmethod
4122
+ def section_header(cls):
4123
+ return "On-Site, Point of Contact Agency Information"
4124
+
4125
+
4126
+ class SiteResponsibleAgency(AgencyPOC):
4127
+ """
4128
+ 12. Responsible Agency (if different from 11.)
4129
+
4130
+ Agency : (multiple lines)
4131
+ Preferred Abbreviation : (A10)
4132
+ Mailing Address : (multiple lines)
4133
+ Primary Contact
4134
+ Contact Name :
4135
+ Telephone (primary) :
4136
+ Telephone (secondary) :
4137
+ Fax :
4138
+ E-mail :
4139
+ Secondary Contact
4140
+ Contact Name :
4141
+ Telephone (primary) :
4142
+ Telephone (secondary) :
4143
+ Fax :
4144
+ E-mail :
4145
+ Additional Information : (multiple lines)
4146
+ """
4147
+
4148
+ @classmethod
4149
+ def section_number(cls):
4150
+ return 12
4151
+
4152
+ @classmethod
4153
+ def section_header(cls):
4154
+ return "Responsible Agency"
4155
+
4156
+
4157
+ class SiteMoreInformation(SiteSection):
4158
+ """
4159
+ 13. More Information
4160
+
4161
+ Primary Data Center : ROB
4162
+ Secondary Data Center : BKG
4163
+ URL for More Information :
4164
+ Hardcopy on File
4165
+ Site Map : (Y or URL)
4166
+ Site Diagram : (Y or URL)
4167
+ Horizon Mask : (Y or URL)
4168
+ Monument Description : (Y or URL)
4169
+ Site Pictures : (Y or URL)
4170
+ Additional Information : (multiple lines)
4171
+ Antenna Graphics with Dimensions
4172
+ """
4173
+
4174
+ @classmethod
4175
+ def structure(cls):
4176
+ return [
4177
+ "primary",
4178
+ "secondary",
4179
+ "more_info",
4180
+ (
4181
+ _("Hardcopy on File"),
4182
+ (
4183
+ "sitemap",
4184
+ "site_diagram",
4185
+ "horizon_mask",
4186
+ "monument_description",
4187
+ "site_picture",
4188
+ ),
4189
+ ),
4190
+ "additional_information",
4191
+ # (_('Antenna Graphics with Dimensions'), ('antenna_graphic',))
4192
+ ]
4193
+
4194
+ @classmethod
4195
+ def section_number(cls):
4196
+ return 13
4197
+
4198
+ @classmethod
4199
+ def section_header(cls):
4200
+ return "More Information"
4201
+
4202
+ primary = models.CharField(
4203
+ max_length=50,
4204
+ blank=True,
4205
+ default="",
4206
+ verbose_name=_("Primary Data Center"),
4207
+ help_text=_("Enter the name of the primary operational data center"),
4208
+ db_index=True,
4209
+ )
4210
+ secondary = models.CharField(
4211
+ max_length=50,
4212
+ blank=True,
4213
+ default="",
4214
+ verbose_name=_("Secondary Data Center"),
4215
+ help_text=_("Enter the name of the secondary or backup data center"),
4216
+ db_index=True,
4217
+ )
4218
+
4219
+ more_info = models.URLField(
4220
+ default="",
4221
+ null=False,
4222
+ blank=True,
4223
+ verbose_name=_("URL for More Information"),
4224
+ db_index=True,
4225
+ )
4226
+
4227
+ sitemap = models.CharField(
4228
+ max_length=255,
4229
+ default="",
4230
+ blank=True,
4231
+ verbose_name=_("Site Map"),
4232
+ help_text=_("Enter the site map URL"),
4233
+ db_index=True,
4234
+ )
4235
+ site_diagram = models.CharField(
4236
+ max_length=255,
4237
+ default="",
4238
+ blank=True,
4239
+ verbose_name=_("Site Diagram"),
4240
+ help_text=_("Enter URL for site diagram"),
4241
+ db_index=True,
4242
+ )
4243
+ horizon_mask = models.CharField(
4244
+ max_length=255,
4245
+ default="",
4246
+ blank=True,
4247
+ verbose_name=_("Horizon Mask"),
4248
+ help_text=_("Enter Horizon mask URL"),
4249
+ db_index=True,
4250
+ )
4251
+ monument_description = models.CharField(
4252
+ max_length=255,
4253
+ default="",
4254
+ blank=True,
4255
+ verbose_name=_("Monument Description"),
4256
+ help_text=_("Enter monument description URL"),
4257
+ db_index=True,
4258
+ )
4259
+ site_picture = models.CharField(
4260
+ max_length=255,
4261
+ default="",
4262
+ blank=True,
4263
+ verbose_name=_("Site Pictures"),
4264
+ help_text=_("Enter site pictures URL"),
4265
+ db_index=True,
4266
+ )
4267
+
4268
+ additional_information = models.TextField(
4269
+ blank=True,
4270
+ default="",
4271
+ verbose_name=_("Additional Information"),
4272
+ help_text=_("Enter additional relevant information. Format: (multiple lines)"),
4273
+ )
4274
+
4275
+ # def antenna_graphic(self):
4276
+ # return self.site.siteantenna_set.first().graphic
4277
+
4278
+ # antenna_graphic.verbose_name = _('')
4279
+ # antenna_graphic.no_indent = True