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/system.py ADDED
@@ -0,0 +1,723 @@
1
+ import os
2
+ from datetime import datetime, timezone
3
+ from io import BytesIO
4
+ from logging import getLogger
5
+
6
+ from dateutil import parser
7
+ from django.conf import settings
8
+ from django.contrib.contenttypes.models import ContentType
9
+ from django.contrib.gis.db import models as gis_models
10
+ from django.core.files.base import ContentFile
11
+ from django.db import models
12
+ from django.db.models import Q
13
+ from django.urls import reverse
14
+ from django.utils.timezone import is_naive, make_aware, now
15
+ from django.utils.translation import gettext as _
16
+ from django_enum import EnumField
17
+ from PIL import Image
18
+ from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet
19
+ from polymorphic.models import PolymorphicModel
20
+
21
+ from slm.defines import (
22
+ CardinalDirection,
23
+ GeodesyMLVersion,
24
+ LogEntryType,
25
+ SiteFileUploadStatus,
26
+ SiteLogFormat,
27
+ SLMFileType,
28
+ )
29
+ from slm.models.sitelog import DefaultToStrEncoder
30
+ from slm.utils import get_exif_tags
31
+
32
+
33
+ class AgencyManager(models.Manager):
34
+ pass
35
+
36
+
37
+ class AgencyQuerySet(models.QuerySet):
38
+ def membership(self, user):
39
+ """Get the agency(s) this user is a member of."""
40
+ if user.is_authenticated:
41
+ if user.is_superuser:
42
+ return self
43
+ return user.agencies.all()
44
+ return self.none()
45
+
46
+ def public(self):
47
+ """
48
+ Public agencies show up in all unauthenticated interfaces and APIs.
49
+ The must be both active and public.
50
+ :return:
51
+ """
52
+ return self.filter(active=True, public=True)
53
+
54
+ def visible_to(self, user):
55
+ """
56
+ If authenticated and superuser return everything. If not authenticated
57
+ return only public stations, if not super user and authenticated return
58
+ all public agencies and any non-public agencies the user is a member
59
+ of.
60
+
61
+ :param user: The user object associated with the request.
62
+ :return:
63
+ """
64
+ if user.is_authenticated and user.is_superuser:
65
+ return self
66
+ return self.public() | self.membership(user)
67
+
68
+
69
+ class Agency(models.Model):
70
+ id = models.AutoField(primary_key=True) # Field name made lowercase.
71
+ name = models.CharField(max_length=100, blank=False, null=False, db_index=True)
72
+ shortname = models.CharField(max_length=20, blank=False, null=False, db_index=True)
73
+
74
+ url = models.URLField(max_length=255, blank=True, null=True)
75
+
76
+ address = models.CharField(max_length=50, blank=True, null=True)
77
+ address2 = models.CharField(max_length=50, blank=True, null=True)
78
+ city = models.CharField(max_length=50, blank=True, null=True)
79
+ state = models.CharField(max_length=30, blank=True, null=True)
80
+ postal_code = models.CharField(max_length=20, blank=True, null=True)
81
+ country = models.CharField(max_length=50, blank=True, null=True)
82
+ phone1 = models.CharField(max_length=20, blank=True, null=True)
83
+ phone2 = models.CharField(max_length=20, blank=True, null=True)
84
+ email1 = models.EmailField(max_length=100, blank=True, null=True)
85
+ email2 = models.EmailField(max_length=100, blank=True, null=True)
86
+ contact = models.CharField(max_length=50, blank=True, null=True)
87
+ other = models.TextField(blank=True, null=True)
88
+ active = models.BooleanField(blank=True, null=False, default=True)
89
+ created = models.DateTimeField(blank=True, auto_now_add=True)
90
+
91
+ public = models.BooleanField(
92
+ blank=True,
93
+ default=True,
94
+ null=False,
95
+ help_text=_(
96
+ "Set to false to exclude all sites affiliated with this agency "
97
+ "from public exposure."
98
+ ),
99
+ db_index=True,
100
+ )
101
+
102
+ objects = AgencyManager.from_queryset(AgencyQuerySet)()
103
+
104
+ def __str__(self):
105
+ return self.name
106
+
107
+ class Meta:
108
+ managed = True
109
+
110
+
111
+ class NetworkManager(models.Manager):
112
+ pass
113
+
114
+
115
+ class NetworkQuerySet(models.QuerySet):
116
+ def public(self):
117
+ return self.filter(public=True)
118
+
119
+ def visible_to(self, user):
120
+ if user.is_authenticated and user.is_superuser:
121
+ return self
122
+ return self.public()
123
+
124
+
125
+ class Network(models.Model):
126
+ name = models.CharField(max_length=100, blank=False, null=False, db_index=True)
127
+
128
+ public = models.BooleanField(
129
+ null=False,
130
+ default=True,
131
+ blank=True,
132
+ db_index=True,
133
+ help_text=_(
134
+ "If false, this network will not appear in publicly facing data,"
135
+ "interfaces and APIs."
136
+ ),
137
+ )
138
+
139
+ sites = models.ManyToManyField("slm.Site", related_name="networks")
140
+
141
+ objects = NetworkManager.from_queryset(NetworkQuerySet)()
142
+
143
+ def __str__(self):
144
+ return self.name
145
+
146
+
147
+ def site_upload_path(instance, filename):
148
+ """
149
+ file will be saved to:
150
+ MEDIA_ROOT/uploads/<site name>/filename
151
+
152
+ :param instance: The SiteFile instance
153
+ :param filename: The name of the file
154
+ :return: The path where the site file should reside.
155
+ """
156
+ prefix = ""
157
+ if instance.SUB_DIRECTORY:
158
+ prefix = f"{instance.SUB_DIRECTORY}/"
159
+ return f"{prefix}{instance.site.name}/{filename}"
160
+
161
+
162
+ def site_thumbnail_path(instance, filename):
163
+ """
164
+ Return the path for the thumbnail image for the given filename.
165
+
166
+ :param instance: The SiteFile instance
167
+ :param filename: The name of the file
168
+ :return: The path where the thumbnail image should reside.
169
+ """
170
+ parts = str(site_upload_path(instance, filename)).split("/")
171
+ return "/".join([*parts[0:-1], "thumbnails", parts[-1]])
172
+
173
+
174
+ class SiteFile(models.Model):
175
+ SUB_DIRECTORY = "misc"
176
+
177
+ logger = getLogger("slm.models.system.SiteFile")
178
+
179
+ site = models.ForeignKey(
180
+ "slm.Site",
181
+ on_delete=models.CASCADE,
182
+ null=False,
183
+ help_text=_("The site this file is attached to."),
184
+ related_name="%(class)ss",
185
+ )
186
+
187
+ timestamp = models.DateTimeField(
188
+ auto_now_add=True, db_index=True, help_text=_("When the file was uploaded.")
189
+ )
190
+
191
+ file = models.FileField(
192
+ upload_to=site_upload_path,
193
+ null=False,
194
+ max_length=255,
195
+ help_text=_("A pointer to the uploaded file on disk."),
196
+ )
197
+
198
+ size = models.PositiveIntegerField(null=True, default=None, blank=True)
199
+
200
+ thumbnail = models.ImageField(
201
+ upload_to=site_thumbnail_path,
202
+ null=True,
203
+ default=None,
204
+ blank=True,
205
+ help_text=_("A pointer to the generated thumbnail file on disk."),
206
+ )
207
+
208
+ mimetype = models.CharField(
209
+ max_length=255,
210
+ null=False,
211
+ default="",
212
+ db_index=True,
213
+ help_text=_("The mimetype of the file."),
214
+ )
215
+
216
+ file_type = EnumField(
217
+ SLMFileType,
218
+ null=False,
219
+ default=SLMFileType.ATTACHMENT,
220
+ db_index=True,
221
+ help_text=_("The file type of the upload."),
222
+ )
223
+
224
+ log_format = EnumField(
225
+ SiteLogFormat,
226
+ null=True,
227
+ default=None,
228
+ db_index=True,
229
+ help_text=_("The site log format. (Only if file_type is Site Log)"),
230
+ )
231
+
232
+ gml_version = EnumField(
233
+ GeodesyMLVersion,
234
+ null=True,
235
+ default=None,
236
+ db_index=True,
237
+ help_text=_("The Geodesy ML version. (Only if file_type is GeodesyML)"),
238
+ )
239
+
240
+ def update_directory(self):
241
+ for file in [self.file, self.thumbnail]:
242
+ if not file or not file.path:
243
+ continue
244
+
245
+ old_file_path = file.path
246
+ new_name = file.field.upload_to(self, os.path.basename(file.name))
247
+ new_file_path = file.storage.path(new_name)
248
+
249
+ if old_file_path != new_file_path:
250
+ os.makedirs(os.path.dirname(new_file_path), exist_ok=True)
251
+ file.name = new_name
252
+ os.rename(old_file_path, new_file_path)
253
+ self.save()
254
+ old_dir = os.path.dirname(old_file_path)
255
+ if not os.listdir(old_dir):
256
+ os.rmdir(old_dir)
257
+
258
+ def rotate(self, degrees_ccw=90):
259
+ if self.file and self.file_type is SLMFileType.SITE_IMAGE:
260
+ from PIL import Image
261
+
262
+ img = Image.open(self.file.path)
263
+ out = img.rotate(int(degrees_ccw), expand=True)
264
+ out.save(self.file.path)
265
+ self.generate_thumbnail(regenerate=True)
266
+
267
+ def save(self, *args, **kwargs):
268
+ self._discover_type()
269
+ if self.pk is None:
270
+ self._exif_transpose()
271
+ self.generate_thumbnail()
272
+ if self.file:
273
+ self.size = self.file.size
274
+ else:
275
+ self.size = None
276
+ return super().save(*args, **kwargs)
277
+
278
+ def _discover_type(self):
279
+ if not self.mimetype or self.mimetype == "application/octet-stream":
280
+ import mimetypes
281
+
282
+ self.mimetype = mimetypes.guess_type(self.file.path)[0]
283
+ if not self.mimetype:
284
+ self.mimetype = "application/octet-stream"
285
+ if self.file_type in [SLMFileType.ATTACHMENT, None]:
286
+ self.file_type, self.log_format = self.determine_type(
287
+ self.file, self.mimetype
288
+ )
289
+
290
+ @classmethod
291
+ def determine_type(cls, file, mimetype):
292
+ file_type, log_format = SLMFileType.ATTACHMENT, None
293
+ if mimetype == SiteLogFormat.LEGACY.mimetype:
294
+ # todo - better criteria??
295
+ upl = file.open()
296
+ content = upl.read()
297
+ if (
298
+ b"Site Identification of the GNSS Monument" in content
299
+ and b"Site Location Information" in content
300
+ and b"GNSS Receiver Information" in content
301
+ and b"GNSS Antenna Information" in content
302
+ ):
303
+ if b"Nine Character ID" in content:
304
+ return SLMFileType.SITE_LOG, SiteLogFormat.ASCII_9CHAR
305
+ return SLMFileType.SITE_LOG, SiteLogFormat.LEGACY
306
+ elif mimetype == SiteLogFormat.GEODESY_ML.mimetype:
307
+ # todo - determine if this is the right schema etc, otherwise
308
+ # could be an XML file attachment
309
+ return SLMFileType.SITE_LOG, SiteLogFormat.GEODESY_ML
310
+ elif mimetype == SiteLogFormat.JSON.mimetype:
311
+ pass
312
+ elif mimetype and mimetype.split("/")[0] == "image" and "svg" not in mimetype:
313
+ return SLMFileType.SITE_IMAGE, None
314
+
315
+ return file_type, log_format
316
+
317
+ def _exif_transpose(self):
318
+ """
319
+ Transpose the image to match the EXIF orientation if it exists and
320
+ is other than 1.
321
+ :return:
322
+ """
323
+ if self.file_type is SLMFileType.SITE_IMAGE and self.file:
324
+ from PIL import ImageOps
325
+
326
+ buffer = BytesIO()
327
+ ImageOps.exif_transpose(Image.open(self.file.open("rb"))).save(
328
+ buffer, format=self.mimetype.split("/")[1].upper()
329
+ )
330
+ buffer.seek(0)
331
+ self.file.save(
332
+ self.file.name,
333
+ ContentFile(buffer.read()),
334
+ save=False,
335
+ )
336
+
337
+ def generate_thumbnail(self, regenerate=False):
338
+ """
339
+ Generate a thumbnail image for this file if it is an image.
340
+
341
+ :param regenerate: If true, delete and regenerate the existing image.
342
+ :return:
343
+ """
344
+ if (
345
+ self.file_type is SLMFileType.SITE_IMAGE
346
+ and (regenerate or not self.has_thumbnail)
347
+ and self.file.path
348
+ ):
349
+ try:
350
+ image = Image.open(self.file.open("rb")).copy().convert("RGB")
351
+ max_dim = getattr(settings, "SLM_THUMBNAIL_SIZE", 250)
352
+ image.thumbnail(
353
+ (
354
+ (image.width * (max_dim / image.height), max_dim)
355
+ if image.height > image.width
356
+ else (max_dim, image.height * (max_dim / image.width))
357
+ ),
358
+ Image.LANCZOS,
359
+ )
360
+ buffer = BytesIO()
361
+ image.save(buffer, "JPEG")
362
+ buffer.seek(0)
363
+ if self.has_thumbnail:
364
+ try:
365
+ os.remove(self.thumbnail.path)
366
+ except OSError:
367
+ pass
368
+ self.thumbnail.save(
369
+ getattr(
370
+ self,
371
+ "name",
372
+ ".".join(
373
+ [*os.path.basename(self.file.path).split(".")[0:-1], "jpeg"]
374
+ ),
375
+ ),
376
+ ContentFile(buffer.read()),
377
+ save=False,
378
+ )
379
+ buffer.close()
380
+ except Exception:
381
+ self.logger.exception("Error creating thumbnail for %s", self)
382
+
383
+ @property
384
+ def has_thumbnail(self):
385
+ return (
386
+ self.thumbnail
387
+ and self.thumbnail.path
388
+ and os.path.exists(self.thumbnail.path)
389
+ )
390
+
391
+ def __str__(self):
392
+ if hasattr(self, "name"):
393
+ return f"[{self.site.name}] {self.name}"
394
+ return f"[{self.site.name}] {os.path.basename(self.file.path)}"
395
+
396
+ class Meta:
397
+ abstract = True
398
+ ordering = ("-timestamp",)
399
+
400
+
401
+ class SiteFileUploadManager(models.Manager):
402
+ pass
403
+
404
+
405
+ class SiteFileUploadQuerySet(models.QuerySet):
406
+ @staticmethod
407
+ def public_q(public_sites_only=True):
408
+ """
409
+ Return a Q object holding the public definition. Which is any file
410
+ that belongs to a public site that is in the PUBLISHED state.
411
+
412
+ :return: Q object holding the definition of "public"
413
+ """
414
+ from slm.models.sitelog import Site
415
+
416
+ return (
417
+ Q(status=SiteFileUploadStatus.PUBLISHED) & Q(site__in=Site.objects.public())
418
+ if public_sites_only
419
+ else Q()
420
+ )
421
+
422
+ def public(self, public_sites_only=True):
423
+ """
424
+ Fetch the files that are public. This is any file that belongs to a
425
+ public site that is in the PUBLISHED state.
426
+
427
+ :return: A queryset containing the public files
428
+ """
429
+ return self.filter(self.public_q(public_sites_only))
430
+
431
+ def available_to(self, user):
432
+ """
433
+ Fetch the files that are available to the given user. This is any
434
+ public file or any file that is attached to a site a user has edit
435
+ permissions to.
436
+
437
+ :param user: The user (may be unauthenticated)
438
+ :return: A queryset containing available files
439
+ """
440
+ from slm.models.sitelog import Site
441
+
442
+ if not user.is_authenticated:
443
+ return self.public()
444
+ return self.filter(
445
+ self.public_q(public_sites_only=False)
446
+ | (Q(site__in=Site.objects.editable_by(user)))
447
+ )
448
+
449
+
450
+ class SiteFileUpload(SiteFile):
451
+ """
452
+ SiteFileUploads can be any file that was uploaded to a given site.
453
+ This includes, images and attachments as well as legacy site logs,
454
+ GeodesyML files or json files. Access to the files for download publicly
455
+ and by authenticated users is controlled in the manager/queryset above.
456
+
457
+ Only images and attachments are made available for download publicly.
458
+ Uploaded log files are ephemeral - for downloads of rendered log files
459
+ from disk see ArchivedSiteLog.
460
+ """
461
+
462
+ SUB_DIRECTORY = "uploads"
463
+
464
+ name = models.CharField(
465
+ blank=False, db_index=True, max_length=255, help_text=_("The name of the file.")
466
+ )
467
+
468
+ created = models.DateTimeField(
469
+ null=True,
470
+ blank=True,
471
+ default=None,
472
+ db_index=True,
473
+ help_text=_("The date and time the file was created."),
474
+ )
475
+
476
+ status = EnumField(
477
+ SiteFileUploadStatus,
478
+ default=SiteFileUploadStatus.UNPUBLISHED,
479
+ null=False,
480
+ blank=True,
481
+ db_index=True,
482
+ help_text=_(
483
+ "The status of the file. This will also depend on what type the " "file is."
484
+ ),
485
+ )
486
+
487
+ user = models.ForeignKey(
488
+ settings.AUTH_USER_MODEL,
489
+ null=True,
490
+ default=None,
491
+ on_delete=models.SET_NULL,
492
+ help_text=_("The user that uploaded the file."),
493
+ )
494
+
495
+ # context generated at the time of upload - will be added to upload page
496
+ # rendering for the file.
497
+ context = models.JSONField(
498
+ null=False, blank=True, default=dict, encoder=DefaultToStrEncoder
499
+ )
500
+
501
+ description = models.TextField(
502
+ blank=True,
503
+ default="",
504
+ help_text=_("A description of what this file is (optional)."),
505
+ )
506
+
507
+ direction = EnumField(
508
+ CardinalDirection,
509
+ blank=True,
510
+ default=None,
511
+ null=True,
512
+ help_text=_(
513
+ "For images taken at the site, this is the cardinal direction the "
514
+ "camera was pointing towards."
515
+ ),
516
+ )
517
+
518
+ objects = SiteFileUploadManager.from_queryset(SiteFileUploadQuerySet)()
519
+
520
+ def save(self, *args, **kwargs):
521
+ self._discover_type()
522
+ # if this is an image, attempt to pull the real timestamp out of the
523
+ # meta data
524
+ if not self.created and self.file_type == SLMFileType.SITE_IMAGE:
525
+ tags = get_exif_tags(self.file.open("rb"))
526
+ for tag in ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]:
527
+ if tag in tags:
528
+ try:
529
+ self.created = datetime.strptime(tags[tag], "%Y:%m:%d %H:%M:%S")
530
+ except ValueError:
531
+ try:
532
+ self.created = parser.parse(tags[tag])
533
+ except parser.ParserError:
534
+ pass
535
+
536
+ if self.created:
537
+ if is_naive(self.created):
538
+ self.created = make_aware(self.created, timezone.utc)
539
+ break
540
+ return super().save(*args, **kwargs)
541
+
542
+ @property
543
+ def link(self):
544
+ return reverse("slm_public_api:files-detail", kwargs={"pk": self.pk})
545
+
546
+ @property
547
+ def edit_link(self):
548
+ return (
549
+ reverse(
550
+ "slm_edit_api:files-detail",
551
+ kwargs={"pk": self.pk, "site": self.site.name},
552
+ )
553
+ + "?download"
554
+ )
555
+
556
+ @property
557
+ def thumbnail_link(self):
558
+ lnk = reverse("slm_public_api:files-detail", kwargs={"pk": self.pk})
559
+ return f"{lnk}?thumbnail=1"
560
+
561
+ @property
562
+ def edit_thumbnail_link(self):
563
+ lnk = reverse(
564
+ "slm_edit_api:files-detail", kwargs={"pk": self.pk, "site": self.site.name}
565
+ )
566
+ return f"{lnk}?thumbnail=1&download"
567
+
568
+ class Meta:
569
+ ordering = ("-timestamp",)
570
+ verbose_name_plural = "Site File Uploads"
571
+
572
+
573
+ class LogEntryManager(PolymorphicManager):
574
+ pass
575
+
576
+
577
+ class LogEntryQuerySet(PolymorphicQuerySet):
578
+ def for_user(self, user):
579
+ if user.is_superuser:
580
+ return self
581
+ return self.filter(site__agencies__in=user.agencies.all())
582
+
583
+
584
+ class LogEntry(PolymorphicModel):
585
+ user = models.ForeignKey(
586
+ settings.AUTH_USER_MODEL,
587
+ on_delete=models.SET_NULL,
588
+ null=True,
589
+ default=None,
590
+ blank=True,
591
+ related_name="logentries",
592
+ )
593
+
594
+ timestamp = models.DateTimeField(null=False, blank=True, db_index=True)
595
+
596
+ type = EnumField(LogEntryType, null=False, blank=False, db_index=True)
597
+
598
+ site = models.ForeignKey(
599
+ "slm.Site",
600
+ on_delete=models.CASCADE,
601
+ null=True,
602
+ default=None,
603
+ blank=True,
604
+ related_name="edit_history",
605
+ )
606
+
607
+ section = models.ForeignKey(
608
+ ContentType,
609
+ on_delete=models.CASCADE,
610
+ null=True,
611
+ default=None,
612
+ blank=True,
613
+ related_name="edit_history",
614
+ )
615
+
616
+ subsection = models.PositiveSmallIntegerField(null=True, default=None, blank=True)
617
+
618
+ file = models.ForeignKey(
619
+ "slm.SiteFileUpload", null=True, default=None, on_delete=models.SET_NULL
620
+ )
621
+
622
+ ip = models.GenericIPAddressField(null=True, default=None, blank=True)
623
+
624
+ objects = LogEntryManager.from_queryset(LogEntryQuerySet)()
625
+
626
+ def save(self, *args, **kwargs):
627
+ if not self.timestamp:
628
+ self.timestamp = now()
629
+ return super().save(*args, **kwargs)
630
+
631
+ @property
632
+ def link(self):
633
+ """Return a link to the most relevant view for the log entry."""
634
+ if hasattr(self, "section") and self.section:
635
+ section_link = reverse(
636
+ "slm:edit",
637
+ kwargs={
638
+ "station": self.site.name,
639
+ "section": self.section.model_class().section_slug(),
640
+ },
641
+ )
642
+ if self.subsection is not None and self.type != LogEntryType.DELETE:
643
+ section_link += f"#{self.subsection}"
644
+ return section_link
645
+ elif hasattr(self, "file") and self.file:
646
+ return reverse(
647
+ "slm:upload", kwargs={"station": self.site.name, "file": self.file.id}
648
+ )
649
+ elif self.type in {LogEntryType.ATTACHMENT_DELETE, LogEntryType.IMAGE_DELETE}:
650
+ return reverse("slm:upload", kwargs={"station": self.site.name})
651
+ elif hasattr(self, "site") and self.site:
652
+ return reverse("slm:review", kwargs={"station": self.site.name})
653
+ return None
654
+
655
+ def __str__(self):
656
+ return (
657
+ f"({self.site.name} | "
658
+ f'{self.user.name or self.user.email if self.user else ""}) '
659
+ f"[{self.timestamp}]: {self.type} -> "
660
+ f'{self.section or self.file or self.site or ""}'
661
+ )
662
+
663
+ class Meta:
664
+ ordering = ("-timestamp",)
665
+ verbose_name_plural = "Log Entries"
666
+ verbose_name = "Log Entry"
667
+ unique_together = (("timestamp", "type", "site", "section", "subsection"),)
668
+ indexes = [
669
+ models.Index(fields=("timestamp", "type", "site", "section", "subsection"))
670
+ ]
671
+
672
+
673
+ class TideGauge(gis_models.Model):
674
+ name = models.CharField(max_length=128, blank=True, null=False, db_index=True)
675
+
676
+ position = gis_models.PointField(null=True, blank=True, srid=4326, geography=True)
677
+
678
+ sonel_id = models.IntegerField(blank=True, null=True, db_index=True)
679
+
680
+ sites = models.ManyToManyField(
681
+ "slm.Site",
682
+ through="slm.SiteTideGauge",
683
+ through_fields=("gauge", "site"),
684
+ related_name="tide_gauges",
685
+ )
686
+
687
+ @property
688
+ def sonel_link(self):
689
+ if self.sonel_id:
690
+ return (
691
+ f"http://www.sonel.org/spip.php?page=maregraphe"
692
+ f"&idStation={self.sonel_id}"
693
+ )
694
+ return ""
695
+
696
+ @property
697
+ def link(self):
698
+ return self.sonel_link # or ...
699
+
700
+ def __str__(self):
701
+ return self.name
702
+
703
+ class Meta:
704
+ verbose_name = "Tide Gauge"
705
+ verbose_name_plural = "Tide Gauges"
706
+ ordering = ("name",)
707
+
708
+
709
+ class SiteTideGauge(models.Model):
710
+ site = models.ForeignKey(
711
+ "slm.Site", on_delete=models.CASCADE, related_name="tide_gauge_distances"
712
+ )
713
+ gauge = models.ForeignKey(
714
+ TideGauge, on_delete=models.CASCADE, related_name="site_distances"
715
+ )
716
+
717
+ distance = models.IntegerField(blank=True, null=False, db_index=True)
718
+
719
+ def __str__(self):
720
+ return f"{self.site.name} {self.gauge.name}"
721
+
722
+ class Meta:
723
+ ordering = ("site", "distance")