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