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.
- igs_slm-0.1.0b0.dist-info/LICENSE +21 -0
- igs_slm-0.1.0b0.dist-info/METADATA +151 -0
- igs_slm-0.1.0b0.dist-info/RECORD +447 -0
- igs_slm-0.1.0b0.dist-info/WHEEL +4 -0
- igs_slm-0.1.0b0.dist-info/entry_points.txt +3 -0
- igs_tools/__init__.py +0 -0
- igs_tools/connection.py +88 -0
- igs_tools/defines/__init__.py +8 -0
- igs_tools/defines/constellation.py +21 -0
- igs_tools/defines/data_center.py +75 -0
- igs_tools/defines/rinex.py +49 -0
- igs_tools/directory.py +247 -0
- igs_tools/utils.py +66 -0
- slm/__init__.py +21 -0
- slm/admin.py +674 -0
- slm/api/edit/__init__.py +0 -0
- slm/api/edit/serializers.py +316 -0
- slm/api/edit/views.py +1632 -0
- slm/api/fields.py +89 -0
- slm/api/filter.py +504 -0
- slm/api/pagination.py +55 -0
- slm/api/permissions.py +65 -0
- slm/api/public/__init__.py +0 -0
- slm/api/public/serializers.py +249 -0
- slm/api/public/views.py +606 -0
- slm/api/serializers.py +132 -0
- slm/api/views.py +148 -0
- slm/apps.py +323 -0
- slm/authentication.py +198 -0
- slm/bin/__init__.py +0 -0
- slm/bin/startproject.py +262 -0
- slm/bin/templates/{{ project_dir }}/pyproject.toml +35 -0
- slm/bin/templates/{{ project_dir }}/sites/__init__.py +0 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/__init__.py +0 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/base.py +15 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/develop/__init__.py +56 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/develop/local.py +4 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/develop/wsgi.py +16 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/manage.py +34 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/production/__init__.py +61 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/production/wsgi.py +16 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/urls.py +7 -0
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/validation.py +11 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/__init__.py +0 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/admin.py +5 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/apps.py +14 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/management/__init__.py +0 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/management/commands/__init__.py +0 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/management/commands/import_archive.py +64 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/migrations/__init__.py +0 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/models.py +6 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/templates/slm/base.html +8 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/urls.py +10 -0
- slm/bin/templates/{{ project_dir }}/{{ extension_app }}/views.py +5 -0
- slm/defines/AlertLevel.py +24 -0
- slm/defines/AntennaCalibration.py +25 -0
- slm/defines/AntennaFeatures.py +27 -0
- slm/defines/AntennaReferencePoint.py +22 -0
- slm/defines/Aspiration.py +13 -0
- slm/defines/CardinalDirection.py +19 -0
- slm/defines/CollocationStatus.py +12 -0
- slm/defines/EquipmentState.py +22 -0
- slm/defines/FlagSeverity.py +14 -0
- slm/defines/FractureSpacing.py +15 -0
- slm/defines/FrequencyStandardType.py +15 -0
- slm/defines/GeodesyMLVersion.py +48 -0
- slm/defines/ISOCountry.py +1194 -0
- slm/defines/Instrumentation.py +19 -0
- slm/defines/LogEntryType.py +30 -0
- slm/defines/SLMFileType.py +18 -0
- slm/defines/SiteFileUploadStatus.py +61 -0
- slm/defines/SiteLogFormat.py +49 -0
- slm/defines/SiteLogStatus.py +78 -0
- slm/defines/TectonicPlates.py +28 -0
- slm/defines/__init__.py +46 -0
- slm/forms.py +1126 -0
- slm/jinja2/slm/sitelog/ascii_9char.log +346 -0
- slm/jinja2/slm/sitelog/legacy.log +346 -0
- slm/jinja2/slm/sitelog/xsd/0.4/collocationInformation.xml +12 -0
- slm/jinja2/slm/sitelog/xsd/0.4/condition.xml +12 -0
- slm/jinja2/slm/sitelog/xsd/0.4/contact.xml +52 -0
- slm/jinja2/slm/sitelog/xsd/0.4/formInformation.xml +5 -0
- slm/jinja2/slm/sitelog/xsd/0.4/frequencyStandard.xml +12 -0
- slm/jinja2/slm/sitelog/xsd/0.4/gnssAntenna.xml +16 -0
- slm/jinja2/slm/sitelog/xsd/0.4/gnssReceiver.xml +11 -0
- slm/jinja2/slm/sitelog/xsd/0.4/humiditySensor.xml +13 -0
- slm/jinja2/slm/sitelog/xsd/0.4/localEpisodicEffect.xml +10 -0
- slm/jinja2/slm/sitelog/xsd/0.4/moreInformation.xml +22 -0
- slm/jinja2/slm/sitelog/xsd/0.4/multipathSource.xml +10 -0
- slm/jinja2/slm/sitelog/xsd/0.4/otherInstrumentation.xml +5 -0
- slm/jinja2/slm/sitelog/xsd/0.4/pressureSensor.xml +12 -0
- slm/jinja2/slm/sitelog/xsd/0.4/radioInterference.xml +11 -0
- slm/jinja2/slm/sitelog/xsd/0.4/sensor.xml +16 -0
- slm/jinja2/slm/sitelog/xsd/0.4/signalObstruction.xml +10 -0
- slm/jinja2/slm/sitelog/xsd/0.4/siteIdentification.xml +22 -0
- slm/jinja2/slm/sitelog/xsd/0.4/siteLocation.xml +21 -0
- slm/jinja2/slm/sitelog/xsd/0.4/surveyedLocalTie.xml +20 -0
- slm/jinja2/slm/sitelog/xsd/0.4/temperatureSensor.xml +13 -0
- slm/jinja2/slm/sitelog/xsd/0.4/waterVaporSensor.xml +11 -0
- slm/jinja2/slm/sitelog/xsd/0.5/document.xml +10 -0
- slm/jinja2/slm/sitelog/xsd/geodesyml_0.4.xml +99 -0
- slm/jinja2/slm/sitelog/xsd/geodesyml_0.5.xml +112 -0
- slm/management/__init__.py +0 -0
- slm/management/commands/__init__.py +53 -0
- slm/management/commands/build_index.py +96 -0
- slm/management/commands/generate_sinex.py +675 -0
- slm/management/commands/head_from_index.py +541 -0
- slm/management/commands/import_archive.py +908 -0
- slm/management/commands/import_equipment.py +351 -0
- slm/management/commands/set_site.py +56 -0
- slm/management/commands/sitelog.py +144 -0
- slm/management/commands/synchronize.py +60 -0
- slm/management/commands/update_data_availability.py +167 -0
- slm/management/commands/validate_db.py +186 -0
- slm/management/commands/validate_gml.py +73 -0
- slm/map/__init__.py +1 -0
- slm/map/admin.py +5 -0
- slm/map/api/__init__.py +0 -0
- slm/map/api/edit/__init__.py +0 -0
- slm/map/api/edit/serializers.py +28 -0
- slm/map/api/edit/views.py +46 -0
- slm/map/api/public/__init__.py +0 -0
- slm/map/api/public/serializers.py +29 -0
- slm/map/api/public/views.py +64 -0
- slm/map/apps.py +7 -0
- slm/map/defines.py +53 -0
- slm/map/migrations/0001_initial.py +115 -0
- slm/map/migrations/__init__.py +0 -0
- slm/map/models.py +63 -0
- slm/map/static/slm/css/map.css +86 -0
- slm/map/static/slm/js/map.js +159 -0
- slm/map/templates/slm/map.html +374 -0
- slm/map/templates/slm/station/base.html +11 -0
- slm/map/templates/slm/station/edit.html +10 -0
- slm/map/templates/slm/top_nav.html +17 -0
- slm/map/templatetags/__init__.py +0 -0
- slm/map/templatetags/slm_map.py +18 -0
- slm/map/urls.py +25 -0
- slm/map/views.py +36 -0
- slm/middleware.py +29 -0
- slm/migrations/0001_alter_siteantenna_marker_enu_alter_sitelocation_llh_and_more.py +47 -0
- slm/migrations/0001_initial.py +4826 -0
- slm/migrations/0002_alter_dataavailability_site.py +22 -0
- slm/migrations/0003_remove_logentry_slm_logentr_site_lo_7a2af7_idx_and_more.py +80 -0
- slm/migrations/0004_alter_logentry_timestamp_and_more.py +25 -0
- slm/migrations/0005_alter_logentry_options_alter_logentry_section_and_more.py +46 -0
- slm/migrations/0006_alter_logentry_options_alter_logentry_index_together.py +24 -0
- slm/migrations/0007_alter_dataavailability_rate.py +23 -0
- slm/migrations/0008_alter_archiveindex_options_and_more.py +64 -0
- slm/migrations/0009_alter_archiveindex_end.py +21 -0
- slm/migrations/0010_alter_dataavailability_rinex_version_and_more.py +844 -0
- slm/migrations/0011_alter_siteidentification_fracture_spacing.py +33 -0
- slm/migrations/0012_alter_logentry_type.py +36 -0
- slm/migrations/0013_unpublishedfilesalert.py +48 -0
- slm/migrations/0014_sitelogpublished.py +48 -0
- slm/migrations/0015_alter_siteantenna_options_and_more.py +181 -0
- slm/migrations/0016_alter_antenna_description_alter_radome_description_and_more.py +42 -0
- slm/migrations/0017_alter_logentry_unique_together_and_more.py +54 -0
- slm/migrations/0018_afix_deleted.py +34 -0
- slm/migrations/0018_alter_siteantenna_options_and_more.py +244 -0
- slm/migrations/0019_remove_siteantenna_marker_enu_siteantenna_marker_une_and_more.py +101 -0
- slm/migrations/0020_alter_manufacturer_options.py +16 -0
- slm/migrations/0021_alter_siteform_report_type.py +23 -0
- slm/migrations/0022_rename_antcal_antenna_radome_slm_antcal_antenna_20827a_idx_and_more.py +297 -0
- slm/migrations/0023_archivedsitelog_gml_version_and_more.py +55 -0
- slm/migrations/0024_alter_agency_name_alter_agency_shortname.py +24 -0
- slm/migrations/0025_alter_archivedsitelog_log_format_and_more.py +61 -0
- slm/migrations/0026_alter_archivedsitelog_log_format_and_more.py +61 -0
- slm/migrations/0027_importalert_file_contents_importalert_findings_and_more.py +41 -0
- slm/migrations/0028_antenna_replaced_manufacturer_url_radome_replaced_and_more.py +46 -0
- slm/migrations/0029_manufacturer_full_name.py +17 -0
- slm/migrations/0030_alter_antenna_state_alter_radome_state_and_more.py +43 -0
- slm/migrations/__init__.py +0 -0
- slm/migrations/load_satellitesystems.py +27 -0
- slm/models/__init__.py +118 -0
- slm/models/about.py +14 -0
- slm/models/alerts.py +1204 -0
- slm/models/data.py +58 -0
- slm/models/equipment.py +229 -0
- slm/models/help.py +14 -0
- slm/models/index.py +428 -0
- slm/models/sitelog.py +4279 -0
- slm/models/system.py +723 -0
- slm/models/user.py +304 -0
- slm/parsing/__init__.py +786 -0
- slm/parsing/legacy/__init__.py +4 -0
- slm/parsing/legacy/binding.py +817 -0
- slm/parsing/legacy/parser.py +377 -0
- slm/parsing/xsd/__init__.py +34 -0
- slm/parsing/xsd/binding.py +86 -0
- slm/parsing/xsd/geodesyml/0.4/commonTypes.xsd +133 -0
- slm/parsing/xsd/geodesyml/0.4/contact.xsd +29 -0
- slm/parsing/xsd/geodesyml/0.4/dataStreams.xsd +129 -0
- slm/parsing/xsd/geodesyml/0.4/document.xsd +64 -0
- slm/parsing/xsd/geodesyml/0.4/equipment.xsd +427 -0
- slm/parsing/xsd/geodesyml/0.4/fieldMeasurement.xsd +170 -0
- slm/parsing/xsd/geodesyml/0.4/geodesyML.xsd +71 -0
- slm/parsing/xsd/geodesyml/0.4/geodeticEquipment.xsd +343 -0
- slm/parsing/xsd/geodesyml/0.4/geodeticMonument.xsd +147 -0
- slm/parsing/xsd/geodesyml/0.4/lineage.xsd +614 -0
- slm/parsing/xsd/geodesyml/0.4/localInterferences.xsd +131 -0
- slm/parsing/xsd/geodesyml/0.4/measurement.xsd +473 -0
- slm/parsing/xsd/geodesyml/0.4/monumentInfo.xsd +251 -0
- slm/parsing/xsd/geodesyml/0.4/observationSystem.xsd +429 -0
- slm/parsing/xsd/geodesyml/0.4/project.xsd +38 -0
- slm/parsing/xsd/geodesyml/0.4/quality.xsd +176 -0
- slm/parsing/xsd/geodesyml/0.4/referenceFrame.xsd +194 -0
- slm/parsing/xsd/geodesyml/0.4/siteLog.xsd +71 -0
- slm/parsing/xsd/geodesyml/0.5/commonTypes.xsd +133 -0
- slm/parsing/xsd/geodesyml/0.5/contact.xsd +29 -0
- slm/parsing/xsd/geodesyml/0.5/dataStreams.xsd +129 -0
- slm/parsing/xsd/geodesyml/0.5/document.xsd +64 -0
- slm/parsing/xsd/geodesyml/0.5/equipment.xsd +427 -0
- slm/parsing/xsd/geodesyml/0.5/fieldMeasurement.xsd +170 -0
- slm/parsing/xsd/geodesyml/0.5/geodesyML.xsd +71 -0
- slm/parsing/xsd/geodesyml/0.5/geodeticEquipment.xsd +343 -0
- slm/parsing/xsd/geodesyml/0.5/geodeticMonument.xsd +147 -0
- slm/parsing/xsd/geodesyml/0.5/lineage.xsd +614 -0
- slm/parsing/xsd/geodesyml/0.5/localInterferences.xsd +131 -0
- slm/parsing/xsd/geodesyml/0.5/measurement.xsd +473 -0
- slm/parsing/xsd/geodesyml/0.5/monumentInfo.xsd +306 -0
- slm/parsing/xsd/geodesyml/0.5/observationSystem.xsd +429 -0
- slm/parsing/xsd/geodesyml/0.5/project.xsd +38 -0
- slm/parsing/xsd/geodesyml/0.5/quality.xsd +176 -0
- slm/parsing/xsd/geodesyml/0.5/referenceFrame.xsd +194 -0
- slm/parsing/xsd/geodesyml/0.5/siteLog.xsd +73 -0
- slm/parsing/xsd/parser.py +116 -0
- slm/parsing/xsd/resolver.py +28 -0
- slm/receivers/__init__.py +11 -0
- slm/receivers/alerts.py +87 -0
- slm/receivers/cleanup.py +41 -0
- slm/receivers/event_loggers.py +175 -0
- slm/receivers/index.py +67 -0
- slm/settings/__init__.py +55 -0
- slm/settings/auth.py +15 -0
- slm/settings/ckeditor.py +14 -0
- slm/settings/debug.py +47 -0
- slm/settings/internationalization.py +12 -0
- slm/settings/logging.py +113 -0
- slm/settings/platform/__init__.py +0 -0
- slm/settings/platform/darwin.py +10 -0
- slm/settings/rest.py +21 -0
- slm/settings/root.py +152 -0
- slm/settings/routines.py +43 -0
- slm/settings/secrets.py +37 -0
- slm/settings/security.py +5 -0
- slm/settings/slm.py +188 -0
- slm/settings/static_templates.py +53 -0
- slm/settings/templates.py +29 -0
- slm/settings/uploads.py +8 -0
- slm/settings/urls.py +126 -0
- slm/settings/validation.py +196 -0
- slm/signals.py +250 -0
- slm/singleton.py +49 -0
- slm/static/rest_framework/css/bootstrap-tweaks.css +204 -0
- slm/static/rest_framework/css/bootstrap.min.css +7 -0
- slm/static/rest_framework/css/bootstrap.min.css.map +1 -0
- slm/static/rest_framework/css/default.css +82 -0
- slm/static/rest_framework/css/prettify.css +30 -0
- slm/static/rest_framework/docs/css/base.css +344 -0
- slm/static/rest_framework/docs/css/highlight.css +125 -0
- slm/static/rest_framework/docs/css/jquery.json-view.min.css +11 -0
- slm/static/rest_framework/docs/img/favicon.ico +0 -0
- slm/static/rest_framework/docs/img/grid.png +0 -0
- slm/static/rest_framework/docs/js/api.js +321 -0
- slm/static/rest_framework/docs/js/highlight.pack.js +2 -0
- slm/static/rest_framework/docs/js/jquery.json-view.min.js +7 -0
- slm/static/rest_framework/img/grid.png +0 -0
- slm/static/rest_framework/js/ajax-form.js +127 -0
- slm/static/rest_framework/js/bootstrap.bundle.min.js +7 -0
- slm/static/rest_framework/js/bootstrap.bundle.min.js.map +1 -0
- slm/static/rest_framework/js/bootstrap.min.js.map +1 -0
- slm/static/rest_framework/js/coreapi-0.1.1.js +2043 -0
- slm/static/rest_framework/js/csrf.js +52 -0
- slm/static/rest_framework/js/default.js +47 -0
- slm/static/rest_framework/js/jquery-3.5.1.min.js +2 -0
- slm/static/rest_framework/js/prettify-min.js +28 -0
- slm/static/slm/css/admin.css +3 -0
- slm/static/slm/css/defines.css +82 -0
- slm/static/slm/css/forms.css +1 -0
- slm/static/slm/css/style.css +1004 -0
- slm/static/slm/img/email-branding.png +0 -0
- slm/static/slm/img/favicon.ico +0 -0
- slm/static/slm/img/login-bg.jpg +0 -0
- slm/static/slm/img/slm-logo.svg +4 -0
- slm/static/slm/js/autocomplete.js +341 -0
- slm/static/slm/js/enums.js +322 -0
- slm/static/slm/js/fileIcons.js +30 -0
- slm/static/slm/js/form.js +404 -0
- slm/static/slm/js/formWidget.js +23 -0
- slm/static/slm/js/persistable.js +33 -0
- slm/static/slm/js/slm.js +1028 -0
- slm/static/slm/js/time24.js +212 -0
- slm/static_templates/slm/css/defines.css +26 -0
- slm/static_templates/slm/js/enums.js +28 -0
- slm/static_templates/slm/js/fileIcons.js +16 -0
- slm/static_templates/slm/js/urls.js +5 -0
- slm/templates/account/base.html +20 -0
- slm/templates/account/email/base.html +43 -0
- slm/templates/account/email/base_message.txt +7 -0
- slm/templates/account/email/email_confirmation_message.html +16 -0
- slm/templates/account/email/email_confirmation_message.txt +7 -0
- slm/templates/account/email/email_confirmation_signup_message.html +1 -0
- slm/templates/account/email/email_confirmation_signup_message.txt +1 -0
- slm/templates/account/email/email_confirmation_signup_subject.txt +1 -0
- slm/templates/account/email/email_confirmation_subject.txt +4 -0
- slm/templates/account/email/password_reset_key_message.html +28 -0
- slm/templates/account/email/password_reset_key_message.txt +9 -0
- slm/templates/account/email/password_reset_key_subject.txt +4 -0
- slm/templates/account/email/unknown_account_message.html +25 -0
- slm/templates/account/email/unknown_account_message.txt +12 -0
- slm/templates/account/email/unknown_account_subject.txt +4 -0
- slm/templates/account/login.html +67 -0
- slm/templates/account/logout.html +38 -0
- slm/templates/account/password_change.html +48 -0
- slm/templates/account/password_reset.html +51 -0
- slm/templates/account/password_reset_done.html +20 -0
- slm/templates/account/password_reset_from_key.html +52 -0
- slm/templates/account/password_reset_from_key_done.html +17 -0
- slm/templates/admin/base.html +7 -0
- slm/templates/messages.html +8 -0
- slm/templates/rest_framework/README +16 -0
- slm/templates/rest_framework/admin/detail.html +10 -0
- slm/templates/rest_framework/admin/dict_value.html +11 -0
- slm/templates/rest_framework/admin/list.html +32 -0
- slm/templates/rest_framework/admin/list_value.html +11 -0
- slm/templates/rest_framework/admin/simple_list_value.html +2 -0
- slm/templates/rest_framework/admin.html +282 -0
- slm/templates/rest_framework/api.html +3 -0
- slm/templates/rest_framework/base.html +334 -0
- slm/templates/rest_framework/docs/auth/basic.html +42 -0
- slm/templates/rest_framework/docs/auth/session.html +40 -0
- slm/templates/rest_framework/docs/auth/token.html +41 -0
- slm/templates/rest_framework/docs/document.html +37 -0
- slm/templates/rest_framework/docs/error.html +71 -0
- slm/templates/rest_framework/docs/index.html +55 -0
- slm/templates/rest_framework/docs/interact.html +57 -0
- slm/templates/rest_framework/docs/langs/javascript-intro.html +5 -0
- slm/templates/rest_framework/docs/langs/javascript.html +15 -0
- slm/templates/rest_framework/docs/langs/python-intro.html +3 -0
- slm/templates/rest_framework/docs/langs/python.html +13 -0
- slm/templates/rest_framework/docs/langs/shell-intro.html +3 -0
- slm/templates/rest_framework/docs/langs/shell.html +6 -0
- slm/templates/rest_framework/docs/link.html +113 -0
- slm/templates/rest_framework/docs/sidebar.html +78 -0
- slm/templates/rest_framework/filters/base.html +16 -0
- slm/templates/rest_framework/filters/ordering.html +17 -0
- slm/templates/rest_framework/filters/search.html +13 -0
- slm/templates/rest_framework/horizontal/checkbox.html +23 -0
- slm/templates/rest_framework/horizontal/checkbox_multiple.html +32 -0
- slm/templates/rest_framework/horizontal/dict_field.html +11 -0
- slm/templates/rest_framework/horizontal/fieldset.html +16 -0
- slm/templates/rest_framework/horizontal/form.html +6 -0
- slm/templates/rest_framework/horizontal/input.html +21 -0
- slm/templates/rest_framework/horizontal/list_field.html +11 -0
- slm/templates/rest_framework/horizontal/list_fieldset.html +13 -0
- slm/templates/rest_framework/horizontal/radio.html +42 -0
- slm/templates/rest_framework/horizontal/select.html +36 -0
- slm/templates/rest_framework/horizontal/select_multiple.html +38 -0
- slm/templates/rest_framework/horizontal/textarea.html +21 -0
- slm/templates/rest_framework/inline/checkbox.html +8 -0
- slm/templates/rest_framework/inline/checkbox_multiple.html +14 -0
- slm/templates/rest_framework/inline/dict_field.html +9 -0
- slm/templates/rest_framework/inline/fieldset.html +6 -0
- slm/templates/rest_framework/inline/form.html +8 -0
- slm/templates/rest_framework/inline/input.html +9 -0
- slm/templates/rest_framework/inline/list_field.html +9 -0
- slm/templates/rest_framework/inline/list_fieldset.html +3 -0
- slm/templates/rest_framework/inline/radio.html +25 -0
- slm/templates/rest_framework/inline/select.html +24 -0
- slm/templates/rest_framework/inline/select_multiple.html +25 -0
- slm/templates/rest_framework/inline/textarea.html +9 -0
- slm/templates/rest_framework/login.html +3 -0
- slm/templates/rest_framework/login_base.html +65 -0
- slm/templates/rest_framework/pagination/numbers.html +47 -0
- slm/templates/rest_framework/pagination/previous_and_next.html +21 -0
- slm/templates/rest_framework/raw_data_form.html +11 -0
- slm/templates/rest_framework/schema.js +3 -0
- slm/templates/rest_framework/vertical/checkbox.html +16 -0
- slm/templates/rest_framework/vertical/checkbox_multiple.html +30 -0
- slm/templates/rest_framework/vertical/dict_field.html +7 -0
- slm/templates/rest_framework/vertical/fieldset.html +13 -0
- slm/templates/rest_framework/vertical/form.html +6 -0
- slm/templates/rest_framework/vertical/input.html +17 -0
- slm/templates/rest_framework/vertical/list_field.html +7 -0
- slm/templates/rest_framework/vertical/list_fieldset.html +7 -0
- slm/templates/rest_framework/vertical/radio.html +40 -0
- slm/templates/rest_framework/vertical/select.html +34 -0
- slm/templates/rest_framework/vertical/select_multiple.html +31 -0
- slm/templates/rest_framework/vertical/textarea.html +17 -0
- slm/templates/slm/about.html +21 -0
- slm/templates/slm/alerts/alert.html +15 -0
- slm/templates/slm/alerts/geodesymlinvalid.html +8 -0
- slm/templates/slm/alerts/importalert.html +10 -0
- slm/templates/slm/alerts.html +18 -0
- slm/templates/slm/auth_menu.html +41 -0
- slm/templates/slm/base.html +195 -0
- slm/templates/slm/emails/alert_issued.html +31 -0
- slm/templates/slm/emails/alert_issued.txt +9 -0
- slm/templates/slm/emails/base.html +6 -0
- slm/templates/slm/emails/changes_rejected.txt +7 -0
- slm/templates/slm/emails/review_requested.txt +7 -0
- slm/templates/slm/forms/widgets/auto_complete.html +21 -0
- slm/templates/slm/forms/widgets/auto_complete_multiple.html +18 -0
- slm/templates/slm/forms/widgets/checkbox_multiple.html +6 -0
- slm/templates/slm/forms/widgets/inline_multi.html +1 -0
- slm/templates/slm/forms/widgets/splitdatetime.html +14 -0
- slm/templates/slm/forms/widgets/time24.html +37 -0
- slm/templates/slm/help.html +54 -0
- slm/templates/slm/messages.html +13 -0
- slm/templates/slm/new_site.html +88 -0
- slm/templates/slm/profile.html +57 -0
- slm/templates/slm/register.html +40 -0
- slm/templates/slm/reports/file_log.html +43 -0
- slm/templates/slm/reports/head_log.html +23 -0
- slm/templates/slm/reports/head_report.html +55 -0
- slm/templates/slm/reports/index_log.html +23 -0
- slm/templates/slm/reports/index_report.html +71 -0
- slm/templates/slm/station/alert.html +8 -0
- slm/templates/slm/station/alerts.html +19 -0
- slm/templates/slm/station/base.html +104 -0
- slm/templates/slm/station/download.html +87 -0
- slm/templates/slm/station/edit.html +283 -0
- slm/templates/slm/station/form.html +110 -0
- slm/templates/slm/station/log.html +18 -0
- slm/templates/slm/station/review.html +461 -0
- slm/templates/slm/station/upload.html +295 -0
- slm/templates/slm/station/uploads/attachment.html +20 -0
- slm/templates/slm/station/uploads/geodesyml.html +1 -0
- slm/templates/slm/station/uploads/image.html +27 -0
- slm/templates/slm/station/uploads/json.html +0 -0
- slm/templates/slm/station/uploads/legacy.html +77 -0
- slm/templates/slm/top_nav.html +14 -0
- slm/templates/slm/user_activity.html +16 -0
- slm/templates/slm/widgets/alert_scroll.html +135 -0
- slm/templates/slm/widgets/filelist.html +258 -0
- slm/templates/slm/widgets/legend.html +12 -0
- slm/templates/slm/widgets/log_scroll.html +88 -0
- slm/templates/slm/widgets/stationlist.html +233 -0
- slm/templatetags/__init__.py +0 -0
- slm/templatetags/jinja2.py +9 -0
- slm/templatetags/slm.py +459 -0
- slm/urls.py +148 -0
- slm/utils.py +299 -0
- slm/validators.py +297 -0
- slm/views.py +654 -0
- 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
|