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