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/alerts.py ADDED
@@ -0,0 +1,1204 @@
1
+ import os
2
+ from logging import getLogger
3
+ from smtplib import SMTPException
4
+
5
+ from ckeditor_uploader.fields import RichTextUploadingField
6
+ from django.conf import settings
7
+ from django.contrib.auth import get_user_model
8
+ from django.contrib.contenttypes.models import ContentType
9
+ from django.contrib.sites.models import Site as DjangoSite
10
+ from django.core.exceptions import ImproperlyConfigured
11
+ from django.core.files.base import ContentFile
12
+ from django.core.mail import EmailMultiAlternatives
13
+ from django.db import models, transaction
14
+ from django.db.models import Q
15
+ from django.template.loader import get_template
16
+ from django.urls import reverse
17
+ from django.utils.timezone import now
18
+ from django.utils.translation import gettext as _
19
+ from django_enum import EnumField
20
+ from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet
21
+ from polymorphic.models import PolymorphicModel
22
+
23
+ from slm import signals as slm_signals
24
+ from slm.defines import AlertLevel, GeodesyMLVersion, SiteLogFormat, SLMFileType
25
+ from slm.models.system import SiteFile
26
+ from slm.parsing.xsd import SiteLogParser
27
+ from slm.utils import from_email
28
+
29
+
30
+ class AlertManager(PolymorphicManager):
31
+ ALERT_MODELS = {}
32
+
33
+ # automated alerts should set this set to this list of signals
34
+ # that may trigger the alert
35
+ SUPPORTED_SIGNALS = {"issue": {}, "rescind": {}}
36
+
37
+ @classmethod
38
+ def _init_alert_models_(cls):
39
+ """
40
+ We lazily init these maps so we only have to do this work once and
41
+ our polymorphic queries can pick out the correct related alerts.
42
+ :return:
43
+ """
44
+ if cls.ALERT_MODELS:
45
+ return
46
+ from django.apps import apps
47
+ from django.contrib.auth import get_user_model
48
+ from django.core.exceptions import FieldDoesNotExist
49
+
50
+ from slm.models import Agency, Site
51
+
52
+ cls.ALERT_MODELS = {
53
+ "untargeted": set(),
54
+ "site": set(),
55
+ "agency": set(),
56
+ "user": set(),
57
+ }
58
+ relations = [
59
+ {"model": Site, "field": "site"},
60
+ {"model": Agency, "field": "agency"},
61
+ {"model": get_user_model(), "field": "user"},
62
+ ]
63
+ for app in apps.get_app_configs():
64
+ for model in app.get_models():
65
+ if issubclass(model, Alert):
66
+ found_relation = False
67
+ for relation in relations:
68
+ try:
69
+ if issubclass(
70
+ model._meta.get_field(relation["field"]).related_model,
71
+ relation["model"],
72
+ ):
73
+ found_relation = True
74
+ cls.ALERT_MODELS[relation["field"]].add(model)
75
+ except FieldDoesNotExist:
76
+ continue
77
+ if not found_relation:
78
+ cls.ALERT_MODELS["untargeted"].add(model)
79
+
80
+ @classmethod
81
+ def site_alerts(cls):
82
+ """Get the Alert classes that target sites"""
83
+ cls._init_alert_models_()
84
+ return cls.ALERT_MODELS["site"]
85
+
86
+ @classmethod
87
+ def agency_alerts(cls):
88
+ """Get the Alert classes that target agencies"""
89
+ cls._init_alert_models_()
90
+ return cls.ALERT_MODELS["agency"]
91
+
92
+ @classmethod
93
+ def user_alerts(cls):
94
+ """Get the Alert classes that target users"""
95
+ cls._init_alert_models_()
96
+ return cls.ALERT_MODELS["user"]
97
+
98
+ @classmethod
99
+ def untargeted_alerts(cls):
100
+ """Get the Alert classes that target all users"""
101
+ cls._init_alert_models_()
102
+ return cls.ALERT_MODELS["untargeted"]
103
+
104
+ def issue_from_signal(self, **kwargs):
105
+ """
106
+ Automated alerts must implement issue_from_signal() to check for and
107
+ create alerts based off supported triggering signals. The list of
108
+ supported signals must be set in the Alert's manager SUPPORTED_SIGNALS
109
+ class field.
110
+
111
+ :param kwargs: The signal kwargs
112
+ :return:
113
+ """
114
+ raise NotImplementedError(
115
+ f"{self.__class__} must implement issue_from_signal() to trigger "
116
+ f"alerts from a signal."
117
+ )
118
+
119
+ def check_issue_signal_supported(self, signal):
120
+ if signal not in self.SUPPORTED_SIGNALS["issue"]:
121
+ from pprint import pformat
122
+
123
+ from slm.signals import signal_name as name
124
+
125
+ names = [name(sig) for sig in self.SUPPORTED_SIGNALS["issue"]]
126
+ raise ImproperlyConfigured(
127
+ f"{self.model.__name__} alert was triggered by {name(signal)} "
128
+ f"which is not a supported issue signal:"
129
+ f"\n{pformat(names, indent=4)}"
130
+ )
131
+
132
+ def check_rescind_signal_supported(self, signal):
133
+ if signal not in self.SUPPORTED_SIGNALS["rescind"]:
134
+ from pprint import pformat
135
+
136
+ from slm.signals import signal_name as name
137
+
138
+ names = [name(sig) for sig in self.SUPPORTED_SIGNALS["rescind"]]
139
+ raise ImproperlyConfigured(
140
+ f"{self.model.__name__} alert was rescinded by {name(signal)} "
141
+ f"which is not a supported rescind signal:"
142
+ f"\n{pformat(names, indent=4)}"
143
+ )
144
+
145
+ def classes(self):
146
+ """
147
+ Get all registered Alert classes of this type.
148
+ :return:
149
+ """
150
+ from django.apps import apps
151
+
152
+ classes = set()
153
+ for app_config in apps.get_app_configs():
154
+ for mdl in app_config.get_models():
155
+ if issubclass(mdl, self.model):
156
+ classes.add(mdl)
157
+ return classes
158
+
159
+ def create(self, **kwargs):
160
+ kwargs.setdefault("priority", getattr(self, "DEFAULT_PRIORITY", 0))
161
+ return super().create(**kwargs)
162
+
163
+
164
+ class AlertQuerySet(PolymorphicQuerySet):
165
+ def delete_expired(self):
166
+ self.filter(expires__lte=now()).delete()
167
+
168
+ def for_site(self, site):
169
+ if not site:
170
+ return self.none()
171
+ return self.filter(self.site_q(site))
172
+
173
+ def for_sites(self, sites):
174
+ if not sites:
175
+ return self.none()
176
+ return self.filter(self.sites_q(sites))
177
+
178
+ def for_agencies(self, agencies):
179
+ if not agencies:
180
+ return self.none()
181
+ return self.filter(self.agencies_q(agencies))
182
+
183
+ def for_user(self, user):
184
+ if user.is_authenticated:
185
+ return self.filter(self.user_q(user))
186
+ return self.none()
187
+
188
+ @classmethod
189
+ def user_q(cls, user):
190
+ qry = Q()
191
+ search = "" if isinstance(user, get_user_model()) else "__in"
192
+ for alert_class in AlertManager.user_alerts():
193
+ qry |= Q(**{f"{alert_class._meta.model_name}__user{search}": user})
194
+ return qry
195
+
196
+ @classmethod
197
+ def sites_q(cls, sites):
198
+ qry = Q()
199
+ for alert_class in AlertManager.site_alerts():
200
+ qry |= Q(**{f"{alert_class._meta.model_name}__site__in": sites})
201
+ return qry
202
+
203
+ @classmethod
204
+ def site_q(cls, sites):
205
+ qry = Q()
206
+ for alert_class in AlertManager.site_alerts():
207
+ qry |= Q(**{f"{alert_class._meta.model_name}__site": sites})
208
+ return qry
209
+
210
+ @classmethod
211
+ def agencies_q(cls, agencies):
212
+ qry = Q()
213
+ for alert_class in AlertManager.agency_alerts():
214
+ qry |= Q(**{f"{alert_class._meta.model_name}__agency__in": agencies})
215
+ return qry
216
+
217
+ @classmethod
218
+ def untargeted_q(cls):
219
+ qry = Q()
220
+ for alert_class in AlertManager.untargeted_alerts():
221
+ qry |= Q(polymorphic_ctype=ContentType.objects.get_for_model(alert_class))
222
+ return qry
223
+
224
+ def visible_to(self, user):
225
+ """
226
+ Return a queryset of Alerts that should be visible to the given user.
227
+ For super users this is all alerts and for everyone else this is any
228
+ untargeted alert as well as any alert targeted at them, any agencies
229
+ they belong to, or any sites belonging to any agencies they belong to.
230
+
231
+ :param user: The user to fetch alerts for
232
+ :return:
233
+ """
234
+ from slm.models import Site
235
+
236
+ if user.is_authenticated:
237
+ if user.is_superuser:
238
+ return self.all()
239
+ else:
240
+ return self.filter(
241
+ self.untargeted_q()
242
+ | self.user_q(user)
243
+ | self.sites_q(Site.objects.editable_by(user))
244
+ | self.agencies_q(user.agencies.all())
245
+ )
246
+ return self.none()
247
+
248
+ def concerning_agencies(self, agencies):
249
+ """
250
+ Given an iterable of agencies return all Alerts that may be relevant.
251
+ This includes all untargeted alerts, any alerts for the specific
252
+ agencies and any alerts for users belonging to representing agencies or
253
+ any users belonging to the represented agencies.
254
+ """
255
+ from slm.models import Site
256
+
257
+ return self.filter(
258
+ self.untargeted_q()
259
+ | self.user_q(get_user_model().objects.filter(agencies__in=agencies))
260
+ | self.sites_q(Site.objects.filter(agencies__in=agencies).distinct())
261
+ | self.agencies_q(agencies)
262
+ )
263
+
264
+ def concerning_sites(self, sites):
265
+ """
266
+ Given an iterable of sites return all Alerts that may be relevant.
267
+ This includes all untargeted alerts, any alerts for the specific sites
268
+ and any alerts for users belonging to representing agencies or directly
269
+ for represented agencies.
270
+ """
271
+ from slm.models import Agency
272
+
273
+ agencies = Agency.objects.filter(sites__in=sites).distinct()
274
+ ret = self.filter(
275
+ self.untargeted_q()
276
+ | self.user_q(
277
+ get_user_model().objects.filter(agencies__in=agencies).distinct()
278
+ )
279
+ | self.sites_q(sites)
280
+ | self.agencies_q(agencies)
281
+ )
282
+ return ret
283
+
284
+ def send_emails(self, request=None):
285
+ for alert in self:
286
+ alert.get_real_instance().send(request)
287
+
288
+
289
+ class Alert(PolymorphicModel):
290
+ # automated alert types are only issued by the system
291
+ automated = False
292
+
293
+ template_txt = "slm/emails/alert_issued.txt"
294
+ template_html = "slm/emails/alert_issued.html"
295
+
296
+ logger = getLogger()
297
+
298
+ DEFAULT_PRIORITY = 1
299
+
300
+ @property
301
+ def context(self):
302
+ """Get the template render context for this alert"""
303
+ return {"alert": self}
304
+
305
+ @property
306
+ def target(self):
307
+ """Return the targeted object if any"""
308
+ real = self.get_real_instance()
309
+ if real == self:
310
+ return None
311
+ return real.target
312
+
313
+ @property
314
+ def target_link(self):
315
+ if self.site_alert:
316
+ return reverse("slm:edit", kwargs={"station": self.target.name})
317
+ elif self.agency_alert:
318
+ return f'{reverse("slm:home")}?agency={self.target.pk}'
319
+ elif self.user_alert:
320
+ return f"mailto:{self.target.email}"
321
+ return reverse("slm:alert", kwargs={"alert": self.pk})
322
+
323
+ @property
324
+ def untargeted(self):
325
+ """Return true if this alert is for all users"""
326
+ return self.get_real_instance().__class__ in Alert.objects.untargeted_alerts()
327
+
328
+ @property
329
+ def agency_alert(self):
330
+ """Return true if this alert is for an Agency"""
331
+ return self.get_real_instance().__class__ in Alert.objects.agency_alerts()
332
+
333
+ @property
334
+ def site_alert(self):
335
+ """Return true if this alert is for a Site"""
336
+ return self.get_real_instance().__class__ in Alert.objects.site_alerts()
337
+
338
+ @property
339
+ def user_alert(self):
340
+ """Return true if this alert is for a User"""
341
+ return self.get_real_instance().__class__ in Alert.objects.user_alerts()
342
+
343
+ @property
344
+ def users(self):
345
+ """
346
+ Return a queryset that contains all targeted users and any relevant
347
+ moderators that may be interested in the alert.
348
+
349
+ :return: User QuerySet
350
+ """
351
+ from django.contrib.auth import get_user_model
352
+
353
+ if self.untargeted:
354
+ return get_user_model().objects.all()
355
+ if self.agency_alert:
356
+ return get_user_model().objects.filter(agencies__in=[self.agency])
357
+ if self.site_alert:
358
+ return get_user_model().objects.filter(Q(pk__in=self.site.editors))
359
+ if self.user_alert:
360
+ return get_user_model().objects.filter(pk=self.user.pk)
361
+ raise RuntimeError(f"Unable to determine targeted users for alert: {self}")
362
+
363
+ issuer = models.ForeignKey(
364
+ settings.AUTH_USER_MODEL,
365
+ on_delete=models.SET_NULL,
366
+ default=None,
367
+ null=True,
368
+ blank=True,
369
+ help_text=_("The issuing user (if any)."),
370
+ )
371
+
372
+ header = models.CharField(
373
+ max_length=50,
374
+ null=False,
375
+ default="",
376
+ help_text=_("A short description of the alert."),
377
+ )
378
+ detail = RichTextUploadingField(
379
+ blank=True,
380
+ null=False,
381
+ default="",
382
+ help_text=_("Longer description containing details of the alert."),
383
+ )
384
+
385
+ level = EnumField(
386
+ AlertLevel,
387
+ null=False,
388
+ blank=False,
389
+ db_index=True,
390
+ help_text=_("The severity level of this alert."),
391
+ )
392
+
393
+ timestamp = models.DateTimeField(
394
+ auto_now_add=True, help_text=_("The time the alert was created."), db_index=True
395
+ )
396
+
397
+ sticky = models.BooleanField(
398
+ default=False,
399
+ blank=True,
400
+ help_text=_(
401
+ "Do not allow target users to clear this alert, only admins may " "clear."
402
+ ),
403
+ )
404
+
405
+ priority = models.IntegerField(
406
+ default=0,
407
+ blank=True,
408
+ help_text=_(
409
+ "The priority ordering for this alert. Alerts are shown by "
410
+ "decreasing priority order first then by decreasing timestamp "
411
+ "order."
412
+ ),
413
+ db_index=True,
414
+ )
415
+
416
+ expires = models.DateTimeField(
417
+ null=True,
418
+ default=None,
419
+ blank=True,
420
+ help_text=_("Automatically remove this alert after this time."),
421
+ db_index=True,
422
+ )
423
+
424
+ send_email = models.BooleanField(
425
+ default=False,
426
+ null=False,
427
+ blank=False,
428
+ help_text=_(
429
+ "If true, an email will be sent for this alert to every targeted " "user."
430
+ ),
431
+ )
432
+
433
+ objects = AlertManager.from_queryset(AlertQuerySet)()
434
+
435
+ def send(self, request=None):
436
+ """
437
+ Send an email to all targeted recipients about this alert. Targeted
438
+ recipients are direct recipients, untargeted administrators are CCed.
439
+ If this is an untargeted alert for all users the recipient list is
440
+ BCCed.
441
+
442
+ :return: True if the email was sent successfully - false otherwise
443
+ """
444
+ from django.contrib.auth import get_user_model
445
+
446
+ text = get_template(self.template_txt)
447
+ html = get_template(self.template_html)
448
+ html_ok = bool(self.untargeted or (self.users.emails_ok(html=True).count()))
449
+
450
+ context = self.context
451
+ context.update(
452
+ {"request": request, "current_site": DjangoSite.objects.get_current()}
453
+ )
454
+
455
+ try:
456
+ to, cc, bcc, bcc_text = set(), set(), set(), set()
457
+
458
+ if self.untargeted:
459
+ bcc = {user.email for user in self.users.emails_ok(html=True)}
460
+ bcc_text = {user.email for user in self.users.emails_ok(html=False)}
461
+ else:
462
+ to = {user.email for user in self.users.emails_ok()}
463
+ cc = {
464
+ user.email
465
+ for user in get_user_model()
466
+ .objects.emails_ok()
467
+ .filter(is_superuser=True)
468
+ if user not in bcc and user not in to
469
+ }
470
+
471
+ email_kwargs = {
472
+ "subject": f"[{DjangoSite.objects.get_current().name}] {self}",
473
+ "body": text.render(context),
474
+ "from_email": from_email(),
475
+ "to": to,
476
+ "cc": cc,
477
+ "bcc": bcc,
478
+ }
479
+ email = EmailMultiAlternatives(**email_kwargs)
480
+ if html_ok:
481
+ email.attach_alternative(html.render(context), "text/html")
482
+ email.send(fail_silently=False)
483
+ if bcc_text:
484
+ EmailMultiAlternatives(**{**email_kwargs, "bcc": bcc_text}).send(
485
+ fail_silently=False
486
+ )
487
+ return True
488
+ except (SMTPException, ConnectionError) as exc:
489
+ self.logger.exception(exc)
490
+ return False
491
+
492
+ def __str__(self):
493
+ if self.target:
494
+ return f"({self.target}) {self.header}"
495
+ return self.header
496
+
497
+ class Meta:
498
+ ordering = (
499
+ "-priority",
500
+ "-timestamp",
501
+ )
502
+ verbose_name_plural = " Alerts"
503
+ verbose_name = "Alerts"
504
+
505
+
506
+ class SiteAlert(Alert):
507
+ DEFAULT_PRIORITY = 2
508
+
509
+ site = models.ForeignKey(
510
+ "slm.Site",
511
+ null=True,
512
+ default=None,
513
+ blank=True,
514
+ on_delete=models.CASCADE,
515
+ help_text=_("Only users with access to this site will see this alert."),
516
+ related_name="alerts",
517
+ )
518
+
519
+ @property
520
+ def target(self):
521
+ return self.site
522
+
523
+ def __str__(self):
524
+ if self.site:
525
+ return f"{self.site.name}: {super().__str__()}"
526
+ return super().__str__()
527
+
528
+ class Meta:
529
+ verbose_name_plural = " Alerts: Site"
530
+ verbose_name = "Site Alert"
531
+
532
+
533
+ class UserAlert(Alert):
534
+ DEFAULT_PRIORITY = 4
535
+
536
+ user = models.ForeignKey(
537
+ settings.AUTH_USER_MODEL,
538
+ null=True,
539
+ default=None,
540
+ blank=True,
541
+ on_delete=models.CASCADE,
542
+ help_text=_("Only this user will see this alert."),
543
+ related_name="alerts",
544
+ )
545
+
546
+ @property
547
+ def target(self):
548
+ return self.user
549
+
550
+ class Meta:
551
+ verbose_name_plural = " Alerts: User"
552
+ verbose_name = "User Alert"
553
+
554
+
555
+ class ImportAlert(Alert):
556
+ """
557
+ An alert reserved for issue when issues arise during import of data
558
+ into the system.
559
+ """
560
+
561
+ DEFAULT_PRIORITY = 1
562
+
563
+ site = models.OneToOneField(
564
+ "slm.Site",
565
+ on_delete=models.CASCADE,
566
+ help_text=_("Only users with access to this site will see this alert."),
567
+ related_name="import_alert",
568
+ null=False,
569
+ )
570
+
571
+ file_contents = models.TextField(
572
+ blank=True,
573
+ default="",
574
+ help_text=_(
575
+ "The text contents of the file that import was attempted from (if applicable)."
576
+ ),
577
+ )
578
+
579
+ findings = models.JSONField(null=True, default=None)
580
+
581
+ log_format = EnumField(SiteLogFormat, null=True, default=None)
582
+
583
+ @property
584
+ def context(self):
585
+ return {
586
+ **super().context,
587
+ "findings": self.findings,
588
+ "site": self.site,
589
+ "file": self.file_contents,
590
+ "upload_tmpl": self.upload_tmpl,
591
+ }
592
+
593
+ @property
594
+ def target(self):
595
+ return self.site
596
+
597
+ @property
598
+ def upload_tmpl(self):
599
+ if self.log_format:
600
+ if self.log_format in [SiteLogFormat.LEGACY, SiteLogFormat.ASCII_9CHAR]:
601
+ return "slm/station/uploads/legacy.html"
602
+ elif self.log_format is SiteLogFormat.GEODESY_ML:
603
+ return "slm/station/uploads/geodesyml.html"
604
+ elif self.log_format is SiteLogFormat.JSON:
605
+ return "slm/station/uploads/json.html"
606
+ return None
607
+
608
+ def __str__(self):
609
+ if self.site:
610
+ return f"{self.site.name}: {super().__str__()}"
611
+ return super().__str__()
612
+
613
+ class Meta:
614
+ verbose_name_plural = " Alerts: Import"
615
+ verbose_name = "Import Alert"
616
+
617
+
618
+ class AgencyAlert(Alert):
619
+ DEFAULT_PRIORITY = 3
620
+
621
+ agency = models.ForeignKey(
622
+ "slm.Agency",
623
+ null=True,
624
+ default=None,
625
+ blank=True,
626
+ on_delete=models.CASCADE,
627
+ help_text=_("Only members of this agency will see this alert."),
628
+ related_name="alerts",
629
+ )
630
+
631
+ @property
632
+ def target(self):
633
+ return self.agency
634
+
635
+ class Meta:
636
+ verbose_name_plural = " Alerts: Agency"
637
+ verbose_name = "Agency Alert"
638
+
639
+
640
+ class AutomatedAlertMixin:
641
+ automated = True
642
+
643
+ def save(self, *args, **kwargs):
644
+ for key, val in (
645
+ getattr(settings, "SLM_AUTOMATED_ALERTS", {})
646
+ .get(self._meta.label, {})
647
+ .items()
648
+ ):
649
+ if key in {"issue", "rescind"}:
650
+ continue
651
+ if callable(val):
652
+ val = val()
653
+ setattr(self, key, val)
654
+ super().save(*args, **kwargs)
655
+
656
+
657
+ class GeodesyMLInvalidManager(AlertManager):
658
+ SUPPORTED_SIGNALS = {
659
+ "issue": {
660
+ slm_signals.site_published,
661
+ slm_signals.site_status_changed,
662
+ slm_signals.section_added,
663
+ slm_signals.section_edited,
664
+ slm_signals.section_deleted,
665
+ slm_signals.site_file_published,
666
+ slm_signals.site_file_unpublished,
667
+ }
668
+ }
669
+
670
+ def issue_from_signal(self, signal, site=None, **kwargs):
671
+ """
672
+ Check if an alert should be issued when the given signal is dispatched
673
+ and issue the alert if necessary.
674
+
675
+ :param signal:
676
+ :param site:
677
+ :param kwargs:
678
+ :return: The alert that was issued if
679
+ """
680
+ self.check_issue_signal_supported(signal)
681
+ site.refresh_from_db()
682
+ return self.check_site(
683
+ site=site,
684
+ published=(
685
+ True
686
+ if signal
687
+ in {
688
+ slm_signals.site_published,
689
+ slm_signals.site_file_published,
690
+ slm_signals.site_file_unpublished,
691
+ }
692
+ else None
693
+ ),
694
+ )
695
+
696
+ def check_site(self, site, published=None):
697
+ """
698
+ Check if this alert should be issued for the given site. If an alert
699
+ should be issued and a current one exists for this site, the current
700
+ one will be deleted before the new alert is issued.
701
+
702
+ :param site: The Site object to check.
703
+ :param published: If True, check the published version of this site's
704
+ log - otherwise check the HEAD version, which may contain updates
705
+ :return: The alert object if one was issued, None otherwise
706
+ """
707
+ from slm.api.serializers import SiteLogSerializer
708
+
709
+ if hasattr(site, "geodesymlinvalid") and site.geodesymlinvalid:
710
+ if os.path.exists(site.geodesymlinvalid.file.path):
711
+ os.remove(site.geodesymlinvalid.file.path)
712
+ site.geodesymlinvalid.delete()
713
+
714
+ geo_version = GeodesyMLVersion.latest()
715
+ serializer = SiteLogSerializer(instance=site, published=published)
716
+ xml_str = serializer.format(SiteLogFormat.GEODESY_ML, version=geo_version)
717
+ parser = SiteLogParser(xml_str, site_name=site.name)
718
+ if parser.errors:
719
+ xml_file = ContentFile(
720
+ xml_str.encode("utf-8"),
721
+ name=site.get_filename(log_format=SiteLogFormat.GEODESY_ML),
722
+ )
723
+ obj = self.model.objects.create(
724
+ published=serializer.is_published,
725
+ site=site,
726
+ schema=geo_version,
727
+ findings={
728
+ lineno: (err.level, err.message)
729
+ for lineno, err in parser.errors.items()
730
+ },
731
+ file=xml_file,
732
+ )
733
+ return obj
734
+ return None
735
+
736
+
737
+ class GeodesyMLInvalidQuerySet(AlertQuerySet):
738
+ def check_all(self, published=None):
739
+ """
740
+ Check if an alert should be issued for all sites in this QuerySet.
741
+
742
+ :param published: If True, check the published version of this site's
743
+ log - otherwise check the HEAD version, which may contain updates
744
+ :return: The number of alerts issued.
745
+ """
746
+ alerts = 0
747
+ for site in self:
748
+ if GeodesyMLInvalidManager.objects.check_site(site, published=published):
749
+ alerts += 1
750
+ return alerts
751
+
752
+
753
+ class GeodesyMLInvalid(AutomatedAlertMixin, SiteFile, Alert):
754
+ SUB_DIRECTORY = "alerts"
755
+ DEFAULT_PRIORITY = 0
756
+
757
+ @property
758
+ def context(self):
759
+ return {
760
+ **super().context,
761
+ "findings": self.findings,
762
+ "site": self.site,
763
+ "file": self,
764
+ }
765
+
766
+ @property
767
+ def target(self):
768
+ return self.site
769
+
770
+ # eliminate conflict between Alert.timestamp and SiteFile.timestamp
771
+ timestamp = Alert.timestamp
772
+
773
+ site = models.OneToOneField(
774
+ "slm.Site",
775
+ null=False,
776
+ default=None,
777
+ blank=True,
778
+ on_delete=models.CASCADE,
779
+ help_text=_("The site this alert applies to."),
780
+ related_name="geodesymlinvalid",
781
+ )
782
+
783
+ findings = models.JSONField()
784
+
785
+ schema = EnumField(
786
+ GeodesyMLVersion,
787
+ null=False,
788
+ default=GeodesyMLVersion.latest(),
789
+ help_text=_("The schema version that failed validation."),
790
+ )
791
+
792
+ published = models.BooleanField(
793
+ null=False,
794
+ help_text=_(
795
+ "True if this alert was issued from the published version of the "
796
+ "site log."
797
+ ),
798
+ )
799
+
800
+ objects = GeodesyMLInvalidManager.from_queryset(GeodesyMLInvalidQuerySet)()
801
+
802
+ def save(self, *args, **kwargs):
803
+ self.mimetype = SiteLogFormat.GEODESY_ML.mimetype
804
+ self.file_type = SLMFileType.SITE_LOG
805
+ self.log_format = SiteLogFormat.GEODESY_ML
806
+ self.header = _("GeodesyML is Invalid.")
807
+ self.detail = _(
808
+ "The data for this site does not validate against GeodesyML "
809
+ "schema version: "
810
+ ) + str(self.schema)
811
+ self.sticky = True
812
+ self.expires = None
813
+ self.send_email = False
814
+ super().save(*args, **kwargs)
815
+
816
+ class Meta:
817
+ unique_together = ("site",)
818
+ verbose_name_plural = " Alerts: GeodesyML Invalid"
819
+ verbose_name = "GeodesyML Invalid"
820
+
821
+
822
+ class ReviewRequestedManager(AlertManager):
823
+ SUPPORTED_SIGNALS = {
824
+ "issue": {
825
+ slm_signals.review_requested,
826
+ slm_signals.section_added,
827
+ slm_signals.section_edited,
828
+ slm_signals.section_deleted,
829
+ slm_signals.site_file_uploaded,
830
+ slm_signals.site_file_unpublished,
831
+ slm_signals.site_proposed,
832
+ },
833
+ "rescind": {
834
+ slm_signals.updates_rejected,
835
+ slm_signals.site_published,
836
+ slm_signals.site_file_published,
837
+ slm_signals.section_added,
838
+ slm_signals.section_edited,
839
+ slm_signals.section_deleted,
840
+ },
841
+ }
842
+
843
+ def issue_from_signal(self, signal, site=None, **kwargs):
844
+ self.check_issue_signal_supported(signal)
845
+ if site:
846
+ if hasattr(site, "review_requested") and site.review_requested:
847
+ site.review_requested.timestamp = now()
848
+ site.review_requested.save()
849
+ else:
850
+ return self.create(
851
+ site=site,
852
+ issuer=getattr(kwargs.get("request", None), "user", None),
853
+ detail=kwargs.get("detail", "") or "",
854
+ )
855
+
856
+ def rescind_from_signal(self, signal, site=None, **kwargs):
857
+ self.check_rescind_signal_supported(signal)
858
+ if site:
859
+ return self.filter(site=site).delete()
860
+
861
+
862
+ class ReviewRequestedQueryset(AlertQuerySet):
863
+ pass
864
+
865
+
866
+ class ReviewRequested(AutomatedAlertMixin, Alert):
867
+ DEFAULT_PRIORITY = 0
868
+
869
+ @property
870
+ def target_link(self):
871
+ return reverse("slm:review", kwargs={"station": self.target.name})
872
+
873
+ @property
874
+ def context(self):
875
+ return {**super().context, "site": self.site}
876
+
877
+ @property
878
+ def target(self):
879
+ return self.site
880
+
881
+ @property
882
+ def requester(self):
883
+ return self.issuer
884
+
885
+ site = models.OneToOneField(
886
+ "slm.Site",
887
+ null=False,
888
+ default=None,
889
+ blank=True,
890
+ on_delete=models.CASCADE,
891
+ help_text=_("The site this alert applies to."),
892
+ related_name="review_requested",
893
+ )
894
+
895
+ objects = ReviewRequestedManager.from_queryset(ReviewRequestedQueryset)()
896
+
897
+ def save(self, *args, **kwargs):
898
+ self.header = _("Review Requested.")
899
+ self.sticky = False
900
+ self.expires = None
901
+ self.send_email = True
902
+ self.level = AlertLevel.NOTICE
903
+ if not self.detail:
904
+ self.detail = (
905
+ _(
906
+ '<a href="mailto:{}">{}</a> has requested the updates to '
907
+ "this site log be published."
908
+ ).format(self.requester.email, self.requester.name)
909
+ if self.requester
910
+ else _(
911
+ "A request has been made to publish the updates to this "
912
+ "site log."
913
+ )
914
+ )
915
+ super().save(*args, **kwargs)
916
+
917
+ class Meta:
918
+ unique_together = ("site",)
919
+ verbose_name_plural = " Alerts: Review Requested"
920
+ verbose_name = "Review Requested"
921
+
922
+
923
+ class UnpublishedFilesAlertManager(AlertManager):
924
+ SUPPORTED_SIGNALS = {
925
+ "issue": {slm_signals.site_file_uploaded, slm_signals.site_file_unpublished},
926
+ "rescind": {slm_signals.site_file_published, slm_signals.site_file_deleted},
927
+ }
928
+
929
+ def issue_from_signal(self, signal, site=None, **kwargs):
930
+ from slm.defines import SiteFileUploadStatus, SLMFileType
931
+ from slm.models import SiteFileUpload
932
+
933
+ self.check_issue_signal_supported(signal)
934
+ if site:
935
+ if (
936
+ hasattr(site, "unpublished_files_alert")
937
+ and site.unpublished_files_alert
938
+ ):
939
+ site.unpublished_files_alert.timestamp = now()
940
+ site.unpublished_files_alert.save()
941
+ elif SiteFileUpload.objects.filter(
942
+ Q(site=site)
943
+ & ~Q(file_type=SLMFileType.SITE_LOG)
944
+ & Q(status=SiteFileUploadStatus.UNPUBLISHED)
945
+ ).exists():
946
+ with transaction.atomic():
947
+ return self.update_or_create(
948
+ site=site,
949
+ defaults={
950
+ "issuer": getattr(
951
+ kwargs.get("request", None), "user", None
952
+ ),
953
+ "detail": kwargs.get("detail", "") or "",
954
+ },
955
+ )[0]
956
+
957
+ def rescind_from_signal(self, signal, site=None, **kwargs):
958
+ from slm.defines import SiteFileUploadStatus, SLMFileType
959
+ from slm.models import SiteFileUpload
960
+
961
+ self.check_rescind_signal_supported(signal)
962
+ if site:
963
+ if (
964
+ hasattr(site, "unpublished_files_alert")
965
+ and site.unpublished_files_alert
966
+ and not SiteFileUpload.objects.filter(
967
+ Q(site=site)
968
+ & ~Q(file_type=SLMFileType.SITE_LOG)
969
+ & Q(status=SiteFileUploadStatus.UNPUBLISHED)
970
+ ).exists()
971
+ ):
972
+ return self.filter(site=site).delete()
973
+
974
+
975
+ class UnpublishedFilesAlertQueryset(AlertQuerySet):
976
+ pass
977
+
978
+
979
+ class UnpublishedFilesAlert(AutomatedAlertMixin, Alert):
980
+ DEFAULT_PRIORITY = 0
981
+
982
+ @property
983
+ def target_link(self):
984
+ return reverse("slm:upload", kwargs={"station": self.target.name})
985
+
986
+ @property
987
+ def context(self):
988
+ return {**super().context, "site": self.site}
989
+
990
+ @property
991
+ def target(self):
992
+ return self.site
993
+
994
+ site = models.OneToOneField(
995
+ "slm.Site",
996
+ null=False,
997
+ default=None,
998
+ blank=True,
999
+ on_delete=models.CASCADE,
1000
+ help_text=_("The site this alert applies to."),
1001
+ related_name="unpublished_files_alert",
1002
+ )
1003
+
1004
+ objects = UnpublishedFilesAlertManager.from_queryset(
1005
+ UnpublishedFilesAlertQueryset
1006
+ )()
1007
+
1008
+ def save(self, *args, **kwargs):
1009
+ self.header = _("Unpublished Files")
1010
+ self.sticky = True
1011
+ self.expires = None
1012
+ self.send_email = True
1013
+ self.level = AlertLevel.NOTICE
1014
+ if not self.detail:
1015
+ self.detail = _("This site has unpublished files.")
1016
+ super().save(*args, **kwargs)
1017
+
1018
+ class Meta:
1019
+ unique_together = ("site",)
1020
+ verbose_name_plural = " Alerts: Unpublished Files"
1021
+ verbose_name = "Unpublished Files"
1022
+
1023
+
1024
+ class SiteLogPublishedManager(AlertManager):
1025
+ SUPPORTED_SIGNALS = {"issue": {slm_signals.site_published}}
1026
+
1027
+ def issue_from_signal(self, signal, site=None, **kwargs):
1028
+ with transaction.atomic():
1029
+ return self.update_or_create(
1030
+ site=site,
1031
+ defaults={
1032
+ "issuer": getattr(kwargs.get("request", None), "user", None),
1033
+ "detail": kwargs.get("detail", "") or "",
1034
+ },
1035
+ )[0]
1036
+
1037
+
1038
+ class SiteLogPublishedQueryset(AlertQuerySet):
1039
+ pass
1040
+
1041
+
1042
+ class SiteLogPublished(AutomatedAlertMixin, Alert):
1043
+ DEFAULT_PRIORITY = 0
1044
+
1045
+ @property
1046
+ def target_link(self):
1047
+ return reverse("slm:download", kwargs={"station": self.target.name})
1048
+
1049
+ @property
1050
+ def context(self):
1051
+ return {**super().context, "site": self.site}
1052
+
1053
+ @property
1054
+ def target(self):
1055
+ return self.site
1056
+
1057
+ site = models.OneToOneField(
1058
+ "slm.Site",
1059
+ null=False,
1060
+ default=None,
1061
+ blank=True,
1062
+ on_delete=models.CASCADE,
1063
+ help_text=_("The site this alert applies to."),
1064
+ related_name="published_alerts",
1065
+ )
1066
+
1067
+ objects = SiteLogPublishedManager.from_queryset(SiteLogPublishedQueryset)()
1068
+
1069
+ def save(self, *args, **kwargs):
1070
+ from slm.templatetags.slm import file_url
1071
+
1072
+ self.header = _("Log Published")
1073
+ self.sticky = False
1074
+ # by default expire these alerts immediately - this will mean any
1075
+ # configured emails will go out but the alert will never be visible
1076
+ # in the system interface
1077
+ self.expires = self.expires or now()
1078
+ self.send_email = True
1079
+ self.level = AlertLevel.NOTICE
1080
+ if not self.detail:
1081
+ legacy_link = file_url(
1082
+ reverse(
1083
+ "slm_public_api:download-detail",
1084
+ kwargs={"site": self.site.name, "format": "log"},
1085
+ )
1086
+ )
1087
+ gml_link = file_url(
1088
+ reverse(
1089
+ "slm_public_api:download-detail",
1090
+ kwargs={"site": self.site.name, "format": "xml"},
1091
+ )
1092
+ )
1093
+ self.detail = _(
1094
+ "An updated log has been published for this site. Download "
1095
+ "the new {legacy_file} or the new {geodesyml_file}."
1096
+ ).format(
1097
+ legacy_file=f'<a href="{legacy_link}" download>'
1098
+ f'{_("legacy file")}</a>',
1099
+ geodesyml_file=f'<a href="{gml_link}" download>'
1100
+ f'{_("GeodesyML file")}</a>',
1101
+ )
1102
+ super().save(*args, **kwargs)
1103
+
1104
+ class Meta:
1105
+ unique_together = ("site",)
1106
+ verbose_name_plural = " Alerts: Log Published"
1107
+ verbose_name = "Log Published"
1108
+
1109
+
1110
+ class UpdatesRejectedManager(AlertManager):
1111
+ SUPPORTED_SIGNALS = {
1112
+ "issue": {slm_signals.updates_rejected},
1113
+ "rescind": {
1114
+ slm_signals.site_published,
1115
+ slm_signals.site_file_published,
1116
+ slm_signals.review_requested,
1117
+ slm_signals.section_added,
1118
+ slm_signals.section_edited,
1119
+ slm_signals.section_deleted,
1120
+ },
1121
+ }
1122
+
1123
+ def issue_from_signal(self, signal, site=None, **kwargs):
1124
+ self.check_issue_signal_supported(signal)
1125
+ if site:
1126
+ if hasattr(site, "updates_rejected") and site.updates_rejected:
1127
+ site.updates_rejected.timestamp = now()
1128
+ site.updates_rejected.save()
1129
+ else:
1130
+ return self.create(
1131
+ site=site,
1132
+ issuer=getattr(kwargs.get("request", None), "user", None),
1133
+ detail=kwargs.get("detail", "") or "",
1134
+ )
1135
+
1136
+ def rescind_from_signal(self, signal, site=None, **kwargs):
1137
+ self.check_rescind_signal_supported(signal)
1138
+ if site:
1139
+ return self.filter(site=site).delete()
1140
+
1141
+
1142
+ class UpdatesRejectedQueryset(AlertQuerySet):
1143
+ pass
1144
+
1145
+
1146
+ class UpdatesRejected(AutomatedAlertMixin, Alert):
1147
+ DEFAULT_PRIORITY = 0
1148
+
1149
+ @property
1150
+ def target_link(self):
1151
+ return reverse("slm:alerts", kwargs={"station": self.target.name})
1152
+
1153
+ @property
1154
+ def context(self):
1155
+ return {**super().context, "site": self.site}
1156
+
1157
+ @property
1158
+ def target(self):
1159
+ return self.site
1160
+
1161
+ @property
1162
+ def rejecter(self):
1163
+ return self.issuer
1164
+
1165
+ requester = models.ForeignKey(
1166
+ settings.AUTH_USER_MODEL,
1167
+ null=True,
1168
+ default=None,
1169
+ blank=True,
1170
+ on_delete=models.SET_NULL,
1171
+ )
1172
+
1173
+ site = models.OneToOneField(
1174
+ "slm.Site",
1175
+ null=False,
1176
+ default=None,
1177
+ blank=True,
1178
+ on_delete=models.CASCADE,
1179
+ help_text=_("The site this alert applies to."),
1180
+ related_name="updates_rejected",
1181
+ )
1182
+
1183
+ objects = UpdatesRejectedManager.from_queryset(UpdatesRejectedQueryset)()
1184
+
1185
+ def save(self, *args, **kwargs):
1186
+ self.header = _("Updates were rejected.")
1187
+ self.sticky = False
1188
+ self.expires = None
1189
+ self.send_email = True
1190
+ self.level = AlertLevel.ERROR
1191
+ if not self.detail:
1192
+ self.detail = (
1193
+ _('Updates were rejected by <a href="mailto:{}">{}</a>').format(
1194
+ self.rejecter.email, self.rejecter.name
1195
+ )
1196
+ if self.rejecter
1197
+ else _("Updates were rejected.")
1198
+ )
1199
+ super().save(*args, **kwargs)
1200
+
1201
+ class Meta:
1202
+ unique_together = ("site",)
1203
+ verbose_name_plural = " Alerts: Updates Rejected"
1204
+ verbose_name = "Updates Rejected"