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
@@ -0,0 +1,908 @@
1
+ """
2
+ When transitioning site log management into an SLM you will have to import all of the
3
+ existing data. There are two primary steps involved in doing this:
4
+
5
+ 1. Import the index of existing serialized point in time site logs.
6
+ 2. Populate the sitelog fields in the database from the most recent site logs for each
7
+ station.
8
+
9
+ This command will perform #1 and optionally call :ref:`command_head_from_index` on the
10
+ imported stations to also perform #2.
11
+
12
+ .. tip::
13
+
14
+ Rich HTML logs of the import process will be written to:
15
+
16
+ ``settings.LOG_DIR / import_archive.TIMESTAMP``
17
+
18
+ Logs will be parsed and errors reported, but parsing errors will not prevent logs from
19
+ being indexed.
20
+
21
+ .. warning::
22
+
23
+ If timestamps cannot be determined for files they will not be indexed
24
+ because each entry in the file index requires a begin and end time.
25
+ """
26
+
27
+ import os
28
+ import re
29
+ import sys
30
+ import tarfile
31
+ import typing as t
32
+ from dataclasses import dataclass, field
33
+ from datetime import date, datetime, timezone
34
+ from pathlib import Path
35
+
36
+ from dateutil.parser import ParserError
37
+ from dateutil.parser import parse as parse_datetime
38
+ from django.core.files.base import ContentFile
39
+ from django.core.management import CommandError
40
+ from django.conf import settings
41
+ from django.db import transaction
42
+ from django.template.loader import render_to_string
43
+ from django.utils.timezone import is_naive, make_aware
44
+ from django.utils.translation import gettext as _
45
+ from django_typer.completers import complete_directory, complete_path, these_strings
46
+ from django_typer.management import TyperCommand, get_command, model_parser_completer
47
+ from django_typer.types import Traceback, Verbosity
48
+ from tqdm import tqdm
49
+ from typer import Argument, Option
50
+ from typing_extensions import Annotated
51
+
52
+ from slm.defines import SiteLogFormat, SiteLogStatus, SLMFileType
53
+ from slm.models import Agency, ArchivedSiteLog, ArchiveIndex, Site, User
54
+ from slm.parsing.legacy import SiteLogBinder, SiteLogParser
55
+ from slm.parsing.xsd import (
56
+ SiteLogBinder as XSDSiteLogBinder,
57
+ )
58
+ from slm.parsing.xsd import (
59
+ SiteLogParser as XSDSiteLogParser,
60
+ )
61
+
62
+ from .head_from_index import Command as HeadFromIndex
63
+
64
+
65
+ def make_aware_utc(dt: t.Union[str, datetime, date]) -> datetime:
66
+ if not dt:
67
+ return dt
68
+ if isinstance(dt, str):
69
+ dt = parse_datetime(dt, tzinfos={"UT": timezone.utc, "UTUT": timezone.utc})
70
+ if isinstance(dt, datetime):
71
+ if is_naive(dt):
72
+ return make_aware(dt, timezone.utc)
73
+ elif isinstance(dt, date):
74
+ return datetime(month=dt.month, day=dt.day, year=dt.year, tzinfo=timezone.utc)
75
+ return dt
76
+
77
+
78
+ def list_files(directory: Path) -> t.List[Path]:
79
+ """
80
+ Return a list of all files at a below the given directory path.
81
+
82
+ :param directory: The root path to fetch files from.
83
+ :return: A list of file paths in the directory or subdirectories
84
+ """
85
+ file_list = []
86
+ for root, _1, files in os.walk(directory):
87
+ for file in files:
88
+ file_list.append(Path(os.path.join(root, file)))
89
+ return file_list
90
+
91
+
92
+ @dataclass
93
+ class FileMeta:
94
+ filename: str
95
+ name: str
96
+ format: SiteLogFormat
97
+ mtime: t.Optional[int] = None
98
+ file_date: t.Optional[datetime] = None
99
+ prep_date: t.Optional[datetime] = None
100
+ site: t.Optional[Site] = None
101
+ contents: t.Optional[bytes] = field(repr=False, default=None)
102
+ index: t.Optional[ArchiveIndex] = None
103
+ archived_log: t.Optional[ArchivedSiteLog] = None
104
+
105
+ # a year, but no date could be determined from the filename
106
+ no_day: bool = True
107
+
108
+ bound: t.Optional[SiteLogBinder] = None
109
+
110
+ def get_param(self, section_index, field_name, null_val=None):
111
+ if not self.bound:
112
+ return null_val
113
+ section = self.bound.parsed.sections.get(section_index, None)
114
+ binding = getattr(section, "binding", {})
115
+ if binding and field_name in binding:
116
+ return binding.get(field_name)
117
+ if section:
118
+ # param may be unbound (i.e. not captured by a known name)
119
+ params = section.get_params(field_name)
120
+ if params:
121
+ if len(params) == 1:
122
+ return params[0].value
123
+ return [param.value for param in params]
124
+ return null_val
125
+
126
+ @property
127
+ def warnings(self):
128
+ if self.bound:
129
+ return self.bound.parsed.warnings
130
+ return []
131
+
132
+ @property
133
+ def errors(self):
134
+ if self.bound:
135
+ return self.bound.parsed.errors
136
+ return []
137
+
138
+
139
+ def parse_format(fmt: str):
140
+ if isinstance(fmt, SiteLogFormat):
141
+ return fmt
142
+ return SiteLogFormat(fmt)
143
+
144
+
145
+ class Command(TyperCommand):
146
+ """
147
+ This command will import serialized site logs into the index and the site log
148
+ data model. Hooks are provided to derive from this class and customize parts
149
+ of the import process. See:
150
+
151
+ * process_filename() - To customize how log files are recognized and how
152
+ needed information is extracted from their names
153
+ * decode_log() - Decode log file bytes into a string.
154
+ """
155
+
156
+ help = _(
157
+ "Import an archive of old site logs - creating indexes and optionally "
158
+ "importing the latest file information into the database."
159
+ )
160
+
161
+ suppressed_base_arguments = {
162
+ "version",
163
+ "pythonpath",
164
+ "settings",
165
+ }
166
+
167
+ # the file argument
168
+ file: Path
169
+
170
+ formats: t.List[t.Union[str, SiteLogFormat]] = list(
171
+ set([fmt.ext for fmt in SiteLogFormat])
172
+ )
173
+ site_dirs: bool = False
174
+ unresolved: t.List[t.Tuple[str, str]]
175
+ imported: t.Set[Site]
176
+ created: t.Set[Site]
177
+ indexes: t.Set[ArchiveIndex]
178
+ skipped: t.List[FileMeta]
179
+ site_files = t.Dict[Site, t.Dict[datetime, t.List[FileMeta]]]
180
+ head_indexes = t.Dict[Site, FileMeta]
181
+
182
+ no_create_sites: bool = False
183
+ set_status: t.Optional[SiteLogStatus] = None
184
+ update_head: bool = True
185
+ agencies: t.List[Agency] = []
186
+ owner: t.Optional[User] = None
187
+
188
+ logs: Path = Path("{LOG_DIR}") / "import_archive.{TIMESTAMP}"
189
+ verbosity: int = 1
190
+ traceback: bool = False
191
+
192
+ log_file_tmpl = "slm/reports/file_log.html"
193
+ log_index_tmpl = "slm/reports/index_log.html"
194
+
195
+ # this matches fourYYMM.log and four_YYYYMMDD.log styles
196
+ FILE_NAME_REGEX = re.compile(
197
+ r"^((?P<four_id>[a-zA-Z\d]{4})(?P<yymm>\d{4})|"
198
+ r"((?P<site>[a-zA-Z\d]{4,9})?\D*(?P<date_part>\d{8})(?P<extra>.*)))"
199
+ r"[.](?P<ext>(log)|(txt)|(xml))$"
200
+ )
201
+
202
+ def process_filename(
203
+ self, filename: str, site_name: str = "", mtime: t.Optional[int] = None
204
+ ) -> t.Optional[FileMeta]:
205
+ """
206
+ Process a filename and return a FileMeta object or None if this file
207
+ is not a log file and/or uninterpretable as a log file.
208
+
209
+ .. note::
210
+
211
+ Deriving import commands can override this function or the FILE_NAME_REGEX
212
+ to customize this functions behavior.
213
+
214
+ This will match files with .log or .txt extensions and names that have the
215
+ following format:
216
+
217
+ * starts with the site name (or not if site_name is passed)
218
+ * middle characters can be any non-digit character
219
+ * ends with a timestamp in either:
220
+ * yyyymmdd
221
+ * yymm
222
+ * mmddyyyy
223
+
224
+ .. note::
225
+
226
+ If no day was present on the filename date stamp, jan 1 is used
227
+
228
+ :param filename: The name of the file, including the extension.
229
+ :param site_name: A string that might possibly be the site name and could
230
+ be used if no site name is determinable from the filename.
231
+ :return FileMeta or None if the filename was not interpretable
232
+ """
233
+ match = self.FILE_NAME_REGEX.match(filename)
234
+
235
+ if match:
236
+ groups = match.groupdict()
237
+ if not (self.site_dirs and site_name):
238
+ site_name = groups.get("site") or groups.get("four_id") or site_name
239
+ if not site_name:
240
+ return None
241
+ site_name = site_name.upper()
242
+ file_date = None
243
+ no_day = False
244
+ if groups["yymm"]:
245
+ no_day = True
246
+ year = int(groups["yymm"][:2])
247
+ if year > 80:
248
+ year += 1900
249
+ else:
250
+ year += 2000
251
+ month = int(groups["yymm"][2:])
252
+ file_date = datetime(year=year, month=month, day=1)
253
+ elif groups["date_part"]:
254
+ date_part_full = f"{groups['date_part']}{groups['extra'] or ''}"
255
+ try:
256
+ file_date = parse_datetime(
257
+ date_part_full,
258
+ tzinfos={"UT": timezone.utc, "UTUT": timezone.utc},
259
+ )
260
+ except ParserError:
261
+ try:
262
+ file_date = parse_datetime(
263
+ groups["date_part"],
264
+ tzinfos={"UT": timezone.utc, "UTUT": timezone.utc},
265
+ )
266
+ except ParserError:
267
+ if self.verbosity > 1:
268
+ self.secho(
269
+ _(
270
+ "Unable to interpret {date_part_full} as a date "
271
+ "on {filename}."
272
+ ).format(
273
+ date_part_full=date_part_full, filename=filename
274
+ ),
275
+ fg="red",
276
+ )
277
+
278
+ return FileMeta(
279
+ filename=filename,
280
+ name=site_name,
281
+ mtime=mtime,
282
+ format=SiteLogFormat(groups.get("ext", "log")),
283
+ file_date=make_aware_utc(file_date),
284
+ no_day=no_day,
285
+ )
286
+
287
+ return None
288
+
289
+ def handle(
290
+ self,
291
+ archive: Annotated[
292
+ t.Optional[Path],
293
+ Argument(
294
+ exists=True,
295
+ dir_okay=True,
296
+ help=_(
297
+ "The path to the archive containing the legacy site logs to "
298
+ "import. May be a tar file, a directory or a single site log."
299
+ ),
300
+ shell_complete=complete_path,
301
+ ),
302
+ ] = None,
303
+ no_create_sites: Annotated[
304
+ bool,
305
+ Option(
306
+ "--no-create-sites",
307
+ help=_(
308
+ "Do not create sites if they do not already exist in the database."
309
+ ),
310
+ ),
311
+ ] = no_create_sites,
312
+ set_status: Annotated[
313
+ t.Optional[SiteLogStatus],
314
+ Option(
315
+ metavar="STATUS",
316
+ help=_("Set this status as the site status for imported sites."),
317
+ parser=lambda f: SiteLogStatus(f),
318
+ shell_complete=these_strings(
319
+ [str(status.label) for status in SiteLogStatus]
320
+ ),
321
+ ),
322
+ ] = set_status,
323
+ no_update_head: Annotated[
324
+ bool,
325
+ Option(
326
+ "--no-update-head",
327
+ help=_(
328
+ "For all indexes added at the head index for each site, import "
329
+ "that site log data into the database."
330
+ ),
331
+ ),
332
+ ] = not update_head,
333
+ agencies: Annotated[
334
+ t.List[Agency],
335
+ Option(
336
+ "--agency",
337
+ help=_("Assign all sites to this agency or these agencies."),
338
+ **model_parser_completer(
339
+ Agency, "shortname", case_insensitive=True, help_field="name"
340
+ ),
341
+ ),
342
+ ] = agencies,
343
+ owner: Annotated[
344
+ t.Optional[User],
345
+ Option(
346
+ "--owner",
347
+ help=_("Assign all sites to this owner."),
348
+ **model_parser_completer(
349
+ User, "email", case_insensitive=True, help_field="full_name"
350
+ ),
351
+ ),
352
+ ] = owner,
353
+ logs: Annotated[
354
+ Path,
355
+ Option(
356
+ help=_(
357
+ "Write parser logs to this directory."
358
+ ),
359
+ shell_complete=complete_directory,
360
+ ),
361
+ ] = logs,
362
+ formats: Annotated[
363
+ t.List[
364
+ str
365
+ ], # list of SiteLogFormat's does not work for some upstream reason
366
+ Option(
367
+ "--format",
368
+ metavar="FORMAT",
369
+ help=_("Only import any site logs of the specified format(s)."),
370
+ shell_complete=these_strings(set([fmt.ext for fmt in SiteLogFormat])),
371
+ ),
372
+ ] = formats,
373
+ site_dirs: Annotated[
374
+ bool,
375
+ Option(
376
+ "--site-dirs",
377
+ help=_(
378
+ "Interpret the directories containing the logs as the site names."
379
+ ),
380
+ ),
381
+ ] = site_dirs,
382
+ verbosity: Verbosity = verbosity,
383
+ traceback: Traceback = traceback,
384
+ ):
385
+ if not archive:
386
+ archive = Path(input(_("Where is the archive? (directory or tar/zip): ")))
387
+ self.archive = archive.expanduser()
388
+ if not self.archive.exists():
389
+ raise CommandError(
390
+ _("{archive} does not exist!").format(archive=self.archive)
391
+ )
392
+ self.no_create_sites = no_create_sites
393
+ self.set_status = set_status
394
+ self.update_head = not no_update_head
395
+ self.agencies = agencies
396
+ self.owner = owner
397
+ self.logs = Path(
398
+ str(logs).format(
399
+ LOG_DIR=getattr(settings, "LOG_DIR", "./"),
400
+ TIMESTAMP=datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
401
+ )
402
+ )
403
+ self.formats = [SiteLogFormat(fmt) for fmt in formats]
404
+ self.site_dirs = site_dirs
405
+ self.verbosity = verbosity
406
+ self.traceback = traceback
407
+
408
+ self.unresolved = []
409
+ self.created = set()
410
+ self.imported = set()
411
+ self.indexes = set()
412
+ self.skipped = []
413
+ self.site_files = {}
414
+ self.head_indexes = {}
415
+
416
+ if not self.archive.exists():
417
+ raise CommandError(_("{file} does not exist.").format(file=self.archive))
418
+
419
+ if SiteLogFormat.GEODESY_ML in self.formats:
420
+ from slm.parsing.xsd import load_schemas
421
+
422
+ load_schemas()
423
+
424
+ if self.archive.is_file() and tarfile.is_tarfile(self.archive):
425
+ with tarfile.open(self.archive, "r") as archive:
426
+ with tqdm(
427
+ total=len(archive.getnames()),
428
+ desc=_("Importing"),
429
+ unit="logs",
430
+ postfix={"log": ""},
431
+ disable=self.verbosity != 1,
432
+ ) as p_bar:
433
+ for member in archive.getmembers():
434
+ if not member.isfile():
435
+ continue
436
+
437
+ archive_path = Path(member.name)
438
+ archive_name = archive_path.name
439
+ p_bar.set_postfix({"log": archive_name})
440
+
441
+ file_meta = self.process_filename(
442
+ archive_name,
443
+ site_name=archive_path.parent.name
444
+ if archive_path.parent != Path(".")
445
+ else "",
446
+ mtime=member.mtime,
447
+ )
448
+ if not file_meta:
449
+ p_bar.update(n=1)
450
+ self.unresolved.append(
451
+ (archive_name, _("Unable to interpret filename."))
452
+ )
453
+ if self.verbosity > 1:
454
+ self.secho(
455
+ _(
456
+ "Unable to interpret {filename} as a sitelog."
457
+ ).format(filename=archive_name),
458
+ fg="red",
459
+ )
460
+ continue
461
+ if file_meta.format not in self.formats:
462
+ self.unresolved.append(
463
+ (
464
+ archive_name,
465
+ _(
466
+ "Skipping {filename} because it is not one of "
467
+ "{formats}."
468
+ ).format(
469
+ filename=archive_name, formats=self.formats
470
+ ),
471
+ )
472
+ )
473
+ continue
474
+
475
+ file_meta.contents = archive.extractfile(member).read()
476
+ p_bar.update(n=1)
477
+ try:
478
+ self.process_file(file_meta)
479
+ except Exception as err:
480
+ if self.traceback:
481
+ raise err
482
+ self.unresolved.append((archive_name, str(err)))
483
+
484
+ elif self.archive.is_dir():
485
+ files = list_files(self.archive)
486
+ with tqdm(
487
+ total=len(files),
488
+ desc=_("Importing"),
489
+ unit="logs",
490
+ postfix={"log": ""},
491
+ disable=self.verbosity != 1,
492
+ ) as p_bar:
493
+ for file in files:
494
+ p_bar.set_postfix({"log": file.name})
495
+ file_meta = self.process_filename(
496
+ file.name,
497
+ site_name=file.parent.name if file.parent != Path(".") else "",
498
+ mtime=file.stat().st_mtime,
499
+ )
500
+ if not file_meta:
501
+ p_bar.update(n=1)
502
+ self.unresolved.append(
503
+ (file.name, _("Unable to interpret filename."))
504
+ )
505
+ if self.verbosity > 1:
506
+ self.secho(
507
+ _(
508
+ "Unable to interpret {filename} as a sitelog."
509
+ ).format(filename=file.name),
510
+ fg="red",
511
+ )
512
+ continue
513
+
514
+ if file_meta.format not in self.formats:
515
+ self.unresolved.append(
516
+ (
517
+ file_meta.filename,
518
+ _(
519
+ "Skipping {filename} because it is not one of "
520
+ "{formats}."
521
+ ).format(filename=file.name, formats=self.formats),
522
+ )
523
+ )
524
+ continue
525
+
526
+ file_meta.contents = file.read_bytes()
527
+ p_bar.update(n=1)
528
+ try:
529
+ self.process_file(file_meta)
530
+ except Exception as err:
531
+ if self.traceback:
532
+ raise err
533
+ self.unresolved.append((file_meta.filename, str(err)))
534
+
535
+ else:
536
+ file_meta = self.process_filename(
537
+ self.archive.name, mtime=self.archive.stat().st_mtime
538
+ )
539
+ if not file_meta:
540
+ raise CommandError(
541
+ _(
542
+ "Unable to interpret {file} as either an tar archive or a sitelog."
543
+ ).format(file=self.archive)
544
+ )
545
+ file_meta.contents = self.archive.read_bytes()
546
+ self.process_file(file_meta)
547
+
548
+ if self.verbosity > 0:
549
+ if self.unresolved:
550
+ self.secho(
551
+ _("Unindexed files: {unresolved}").format(
552
+ unresolved=len(self.unresolved)
553
+ ),
554
+ fg="red",
555
+ )
556
+ if self.created:
557
+ self.secho(
558
+ _("Created {sites} sites.").format(sites=len(self.created)),
559
+ fg="green",
560
+ )
561
+ if self.skipped:
562
+ self.secho(
563
+ _("Skipped {skipped} files due to timestamp conflicts.").format(
564
+ skipped=len(self.skipped)
565
+ ),
566
+ fg="yellow",
567
+ )
568
+ self.secho(
569
+ _("Indexed {count} files.").format(count=len(self.indexes)), fg="green"
570
+ )
571
+
572
+ with tqdm(
573
+ total=len(self.imported),
574
+ desc=_("Updating Site State"),
575
+ unit="sites",
576
+ postfix={"site": ""},
577
+ disable=self.verbosity != 1,
578
+ ) as p_bar:
579
+ for site in self.imported:
580
+ site.refresh_from_db()
581
+ p_bar.set_postfix({"site": site.name})
582
+ if self.verbosity > 1:
583
+ self.secho(
584
+ _("Updating {site} state.").format(site=site.name), fg="blue"
585
+ )
586
+ save = False
587
+ recent_first = ArchiveIndex.objects.filter(site=site).order_by("-begin")
588
+ latest = recent_first.first()
589
+ oldest = recent_first.last()
590
+ if site.last_publish is None or site.last_publish < latest.begin:
591
+ site.last_publish = latest.begin
592
+ save = True
593
+ if site.last_update is None or site.last_update < site.last_publish:
594
+ site.last_update = site.last_publish
595
+ save = True
596
+ if site.created is None or site.created < oldest.begin:
597
+ site.created = oldest.begin
598
+ save = True
599
+ if site.join_date is None and site in self.created:
600
+ site.join_date = site.created.date()
601
+ save = True
602
+ if self.set_status:
603
+ site.status = self.set_status
604
+ save = True
605
+ if save:
606
+ site.save()
607
+ p_bar.update(n=1)
608
+
609
+ log = None
610
+ if self.logs:
611
+ sites: t.Dict[Site, t.List[FileMeta]] = {}
612
+ for site in sorted(self.site_files.keys(), key=lambda s: s.name):
613
+ site.refresh_from_db()
614
+ sites[site] = []
615
+ for dt in sorted(self.site_files[site].keys()):
616
+ sites[site].extend(
617
+ [
618
+ file
619
+ for file in sorted(
620
+ self.site_files[site][dt], key=lambda f: f.index.begin
621
+ )
622
+ if file.index and file.bound
623
+ ]
624
+ )
625
+ with tqdm(
626
+ total=len(sites),
627
+ desc=_("Writing Logs"),
628
+ unit="sites",
629
+ postfix={"site": ""},
630
+ disable=self.verbosity != 1,
631
+ ) as p_bar:
632
+ for site, files in sites.items():
633
+ for file in files:
634
+ parser_log = render_to_string(
635
+ self.log_file_tmpl,
636
+ {
637
+ "site": site,
638
+ "file": self.decode_log(file.contents),
639
+ "filename": file.filename,
640
+ "findings": file.bound.parsed.findings_context,
641
+ "format": file.archived_log.log_format
642
+ if file.archived_log
643
+ else None,
644
+ "SiteLogFormat": SiteLogFormat,
645
+ },
646
+ )
647
+ parser_log_dir = self.logs / site.name
648
+ os.makedirs(parser_log_dir, exist_ok=True)
649
+ log_file = parser_log_dir / f"{file.filename}.html"
650
+ if self.verbosity > 1:
651
+ self.secho(
652
+ _("Writing log {log_file}").format(log_file=log_file),
653
+ fg="blue",
654
+ )
655
+ with open(log_file, "wt") as log_f:
656
+ log_f.write(parser_log)
657
+ p_bar.update(n=1)
658
+ p_bar.set_postfix({"site": site.name})
659
+
660
+ head_logs = None
661
+ if self.update_head:
662
+ head_logs = self.logs / "head" if self.logs else None
663
+ get_command("head_from_index", HeadFromIndex)(
664
+ self.head_indexes.keys(),
665
+ formats=self.formats,
666
+ no_prompt=True,
667
+ verbosity=self.verbosity,
668
+ logs=head_logs,
669
+ traceback=traceback,
670
+ )
671
+
672
+ if self.logs:
673
+ log_index = render_to_string(
674
+ self.log_index_tmpl,
675
+ {
676
+ "command": " ".join([Path(sys.argv[0]).name, *sys.argv[1:]]),
677
+ "runtime": datetime.now(),
678
+ "sites": sites,
679
+ "unresolved": self.unresolved,
680
+ "head_logs": (head_logs / "index.html").relative_to(self.logs),
681
+ },
682
+ )
683
+ parser_log_dir = self.logs
684
+ os.makedirs(parser_log_dir, exist_ok=True)
685
+ log = parser_log_dir / "index.html"
686
+ with open(log, "wt") as log_f:
687
+ log_f.write(log_index)
688
+
689
+ if log and self.verbosity > 0:
690
+ return log.absolute()
691
+
692
+ def process_file(self, file_meta: FileMeta):
693
+ with transaction.atomic():
694
+ site = Site.objects.filter(name__istartswith=file_meta.name[0:4]).first()
695
+
696
+ if not site:
697
+ if not self.no_create_sites:
698
+ site = Site.objects.create(
699
+ name=file_meta.name.upper(),
700
+ status=SiteLogStatus.EMPTY,
701
+ )
702
+ self.created.add(site)
703
+ if self.verbosity > 1:
704
+ self.secho(
705
+ _("Created site {site}.").format(site=site.name), fg="green"
706
+ )
707
+ else:
708
+ raise CommandError(
709
+ _(
710
+ "Unable to find site for {filename} and site creation is disabled (see --create-sites)."
711
+ ).format(filename=file_meta.filename)
712
+ )
713
+
714
+ file_meta.site = site
715
+
716
+ self.parse_log(file_meta)
717
+
718
+ # format may have changed after parsing, so we check again
719
+ if file_meta.format not in self.formats:
720
+ raise CommandError(
721
+ _("Skipping {filename} because it is not one of {formats}.").format(
722
+ filename=file_meta.filename, formats=self.formats
723
+ )
724
+ )
725
+
726
+ if site in self.created:
727
+ name_changed = False
728
+ if site.name != file_meta.name.upper() and len(site.name) < len(
729
+ file_meta.name
730
+ ):
731
+ site.name = file_meta.name.upper()
732
+ site.save()
733
+ name_changed = True
734
+
735
+ if (
736
+ file_meta.bound.parsed.site_name
737
+ and len(file_meta.bound.parsed.site_name) > len(site.name)
738
+ and file_meta.bound.parsed.site_name.upper().startswith(site.name)
739
+ ):
740
+ # sometimes the fullname is only in the file! here we use it if
741
+ # its prefixed by the site name and is longer
742
+ site.name = file_meta.bound.parsed.site_name.upper()
743
+ site.save()
744
+ name_changed = True
745
+
746
+ if name_changed:
747
+ for index in ArchiveIndex.objects.filter(site=site):
748
+ for file in index.files.all():
749
+ file.update_directory()
750
+
751
+ self.imported.add(site)
752
+
753
+ log_time = self.determine_time(file_meta)
754
+ if not log_time:
755
+ raise CommandError(
756
+ _("Unable to determine timestamp for {filename}.").format(
757
+ filename=file_meta.filename
758
+ )
759
+ )
760
+
761
+ self.site_files.setdefault(site, {})
762
+ self.site_files[site].setdefault(log_time, [])
763
+ self.site_files[site][log_time].append(file_meta)
764
+
765
+ if site.join_date is None or site.join_date > log_time.date():
766
+ site.join_date = log_time.date()
767
+
768
+ if site.created is None or site.created > log_time:
769
+ site.created = log_time
770
+
771
+ if self.owner:
772
+ site.owner = self.owner
773
+
774
+ if self.agencies:
775
+ site.agencies.set(self.agencies)
776
+
777
+ site.save()
778
+
779
+ index = ArchiveIndex.objects.insert_index(site=site, begin=log_time)
780
+ file_meta.index = index
781
+ self.indexes.add(index)
782
+ if not index.end:
783
+ self.head_indexes[site] = file_meta
784
+
785
+ if any(
786
+ [
787
+ meta.mtime > file_meta.mtime
788
+ for meta in self.site_files[site][log_time]
789
+ if meta is not file_meta and meta.format is file_meta.format
790
+ ]
791
+ ):
792
+ # only save the most recently modified site log of the given format
793
+ # at the given time
794
+ self.skipped.append(file_meta)
795
+ return
796
+
797
+ file_meta.archived_log = ArchivedSiteLog.objects.get_or_create(
798
+ index=index,
799
+ log_format=file_meta.format,
800
+ defaults={
801
+ "site": site,
802
+ "name": file_meta.filename,
803
+ "file_type": SLMFileType.SITE_LOG,
804
+ "file": ContentFile(file_meta.contents, name=file_meta.filename),
805
+ },
806
+ )[0]
807
+
808
+ if self.verbosity > 1:
809
+ self.secho(
810
+ _("Added index {index_file}").format(
811
+ index_file=str(file_meta.archived_log)
812
+ ),
813
+ fg="blue",
814
+ )
815
+
816
+ def parse_log(self, file_meta: FileMeta):
817
+ """
818
+ Parse the log file and attach the binder instance to file_meta.bound. Optionally,
819
+ if configured write the parsing log html file out to the specified directory.
820
+ """
821
+
822
+ log_str = self.decode_log(file_meta.contents)
823
+ if file_meta.format is SiteLogFormat.GEODESY_ML:
824
+ file_meta.bound = XSDSiteLogBinder(
825
+ XSDSiteLogParser(log_str, site_name=file_meta.site.name)
826
+ )
827
+ else:
828
+ file_meta.bound = SiteLogBinder(
829
+ SiteLogParser(log_str, site_name=file_meta.site.name)
830
+ )
831
+
832
+ prep_time = file_meta.get_param((0, None, None), "date_prepared")
833
+ if not prep_time:
834
+ prep_time = file_meta.get_param((0, None, None), "date")
835
+ if prep_time:
836
+ try:
837
+ prep_date = make_aware_utc(prep_time)
838
+ except ParserError:
839
+ prep_date = None
840
+
841
+ file_meta.prep_date = prep_date
842
+ elif file_meta.format in [SiteLogFormat.LEGACY, SiteLogFormat.ASCII_9CHAR]:
843
+ # sometimes you see files in the wild with site form subsections
844
+ def get_subsection_date(idx: int):
845
+ return make_aware_utc(
846
+ file_meta.get_param((0, idx, None), "date_prepared")
847
+ or file_meta.get_param((0, idx, None), "date")
848
+ )
849
+
850
+ prepared_dates = [get_subsection_date(0), get_subsection_date(1)]
851
+ while prepared_dates[-1]:
852
+ prepared_dates.append(get_subsection_date(len(prepared_dates)))
853
+ prepared_dates = sorted([dt for dt in prepared_dates if dt])
854
+ if prepared_dates:
855
+ file_meta.prep_date = prepared_dates[-1]
856
+
857
+ if file_meta.format is SiteLogFormat.LEGACY:
858
+ if file_meta.get_param((1, None, None), "nine_character_id"):
859
+ file_meta.format = SiteLogFormat.ASCII_9CHAR
860
+
861
+ def decode_log(self, contents: t.Union[bytes, str]) -> str:
862
+ """
863
+ Decode the contents of a log file. The site log format has been around since the
864
+ early 1990s so we may encounter exotic encodings.
865
+
866
+ :param contents: The bytes of the log file contents.
867
+ :return a decoded string of the contents
868
+ """
869
+ if isinstance(contents, str):
870
+ return contents
871
+ try:
872
+ return contents.decode("utf-8")
873
+ except UnicodeDecodeError:
874
+ try:
875
+ return contents.decode("ascii")
876
+ except UnicodeDecodeError:
877
+ return contents.decode("latin")
878
+
879
+ def determine_time(self, file_meta: FileMeta) -> datetime:
880
+ """
881
+ Determine the timestamp to use for the file:
882
+
883
+ * Use the timestamp from the file if it is higher resolution than a date.
884
+ * If no day was given on the file timestamp use the prep_date if it
885
+ agrees with the file timestamp month and year.
886
+ * Use prepared by date in the log if it is given and no timestamp could be
887
+ determined from the file name.
888
+ """
889
+
890
+ log_time = None
891
+ if file_meta.file_date:
892
+ log_time = file_meta.file_date
893
+ if log_time.hour or log_time.minute or log_time.second:
894
+ return log_time
895
+
896
+ if file_meta.prep_date:
897
+ if not log_time:
898
+ return file_meta.prep_date
899
+ elif (
900
+ file_meta.prep_date.month == log_time.month
901
+ and file_meta.prep_date.year == log_time.year
902
+ and file_meta.no_day
903
+ ):
904
+ # use the prep date if it agrees with a lower
905
+ # resolution log_date (i.e. no day - old style)
906
+ log_time = file_meta.prep_date
907
+
908
+ return log_time