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/api/edit/views.py
ADDED
|
@@ -0,0 +1,1632 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
|
|
6
|
+
import django_filters
|
|
7
|
+
from chardet import detect
|
|
8
|
+
from crispy_forms.helper import FormHelper
|
|
9
|
+
from crispy_forms.layout import Div, Field, Fieldset, Layout
|
|
10
|
+
from django.contrib.auth import get_user_model
|
|
11
|
+
from django.contrib.gis.db import models as gis_models
|
|
12
|
+
from django.contrib.gis.geos import Point
|
|
13
|
+
from django.core.exceptions import PermissionDenied
|
|
14
|
+
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
15
|
+
from django.db import models, transaction
|
|
16
|
+
from django.db.models import Q
|
|
17
|
+
from django.http import Http404
|
|
18
|
+
from django.http.response import (
|
|
19
|
+
FileResponse,
|
|
20
|
+
HttpResponseForbidden,
|
|
21
|
+
HttpResponseNotFound,
|
|
22
|
+
)
|
|
23
|
+
from django.utils.timezone import now
|
|
24
|
+
from django.utils.translation import gettext as _
|
|
25
|
+
from django_enum.drf import EnumField
|
|
26
|
+
from django_enum.fields import (
|
|
27
|
+
EnumBigIntegerField,
|
|
28
|
+
EnumCharField,
|
|
29
|
+
EnumIntegerField,
|
|
30
|
+
EnumPositiveBigIntegerField,
|
|
31
|
+
EnumPositiveIntegerField,
|
|
32
|
+
EnumPositiveSmallIntegerField,
|
|
33
|
+
EnumSmallIntegerField,
|
|
34
|
+
)
|
|
35
|
+
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
|
36
|
+
from rest_framework import mixins, renderers, serializers, status, viewsets
|
|
37
|
+
from rest_framework.exceptions import ValidationError as DRFValidationError
|
|
38
|
+
from rest_framework.filters import OrderingFilter
|
|
39
|
+
from rest_framework.parsers import (
|
|
40
|
+
FileUploadParser,
|
|
41
|
+
FormParser,
|
|
42
|
+
JSONParser,
|
|
43
|
+
MultiPartParser,
|
|
44
|
+
)
|
|
45
|
+
from rest_framework.permissions import IsAuthenticated
|
|
46
|
+
from rest_framework.response import Response
|
|
47
|
+
from rest_framework.serializers import ModelSerializer, SlugRelatedField
|
|
48
|
+
|
|
49
|
+
from slm import signals as slm_signals
|
|
50
|
+
from slm.api.edit.serializers import (
|
|
51
|
+
AlertSerializer,
|
|
52
|
+
LogEntrySerializer,
|
|
53
|
+
ReviewRequestSerializer,
|
|
54
|
+
SiteFileUploadSerializer,
|
|
55
|
+
StationSerializer,
|
|
56
|
+
UserSerializer,
|
|
57
|
+
)
|
|
58
|
+
from slm.api.fields import SLMDateTimeField, SLMPointField
|
|
59
|
+
from slm.api.filter import (
|
|
60
|
+
AcceptListArguments,
|
|
61
|
+
BaseStationFilter,
|
|
62
|
+
CrispyFormCompat,
|
|
63
|
+
SLMBooleanFilter,
|
|
64
|
+
)
|
|
65
|
+
from slm.api.pagination import DataTablesPagination
|
|
66
|
+
from slm.api.permissions import (
|
|
67
|
+
CanDeleteAlert,
|
|
68
|
+
CanEditSite,
|
|
69
|
+
CanRejectReview,
|
|
70
|
+
IsUserOrAdmin,
|
|
71
|
+
UpdateAdminOnly,
|
|
72
|
+
)
|
|
73
|
+
from slm.api.public.views import AgencyViewSet as PublicAgencyViewSet
|
|
74
|
+
from slm.api.public.views import NetworkViewSet as PublicNetworkViewSet
|
|
75
|
+
from slm.api.serializers import SiteLogSerializer
|
|
76
|
+
from slm.api.views import BaseSiteLogDownloadViewSet
|
|
77
|
+
from slm.defines import (
|
|
78
|
+
CardinalDirection,
|
|
79
|
+
SiteFileUploadStatus,
|
|
80
|
+
SiteLogFormat,
|
|
81
|
+
SiteLogStatus,
|
|
82
|
+
SLMFileType,
|
|
83
|
+
)
|
|
84
|
+
from slm.forms import StationFilterForm as BaseStationFilterForm
|
|
85
|
+
from slm.models import (
|
|
86
|
+
Agency,
|
|
87
|
+
Alert,
|
|
88
|
+
LogEntry,
|
|
89
|
+
Network,
|
|
90
|
+
Site,
|
|
91
|
+
SiteAntenna,
|
|
92
|
+
SiteCollocation,
|
|
93
|
+
SiteFileUpload,
|
|
94
|
+
SiteForm,
|
|
95
|
+
SiteFrequencyStandard,
|
|
96
|
+
SiteHumiditySensor,
|
|
97
|
+
SiteIdentification,
|
|
98
|
+
SiteLocalEpisodicEffects,
|
|
99
|
+
SiteLocation,
|
|
100
|
+
SiteMoreInformation,
|
|
101
|
+
SiteMultiPathSources,
|
|
102
|
+
SiteOperationalContact,
|
|
103
|
+
SiteOtherInstrumentation,
|
|
104
|
+
SitePressureSensor,
|
|
105
|
+
SiteRadioInterferences,
|
|
106
|
+
SiteReceiver,
|
|
107
|
+
SiteResponsibleAgency,
|
|
108
|
+
SiteSection,
|
|
109
|
+
SiteSignalObstructions,
|
|
110
|
+
SiteSubSection,
|
|
111
|
+
SiteSurveyedLocalTies,
|
|
112
|
+
SiteTemperatureSensor,
|
|
113
|
+
SiteWaterVaporRadiometer,
|
|
114
|
+
)
|
|
115
|
+
from slm.parsing.legacy.parser import Error, Warn
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class StationFilterForm(BaseStationFilterForm):
|
|
119
|
+
@property
|
|
120
|
+
def helper(self):
|
|
121
|
+
"""
|
|
122
|
+
Todo - how to render help_text as alt or titles?
|
|
123
|
+
"""
|
|
124
|
+
helper = FormHelper()
|
|
125
|
+
helper.form_id = "slm-station-filter"
|
|
126
|
+
helper.disable_csrf = True
|
|
127
|
+
helper.layout = Layout(
|
|
128
|
+
Div(
|
|
129
|
+
Div(
|
|
130
|
+
Field("status", css_class="slm-status"),
|
|
131
|
+
"alert",
|
|
132
|
+
Field("alert_level", css_class="slm-alert-level"),
|
|
133
|
+
css_class="col-3",
|
|
134
|
+
),
|
|
135
|
+
Div(
|
|
136
|
+
Fieldset(
|
|
137
|
+
_("Equipment Filters"),
|
|
138
|
+
Field(
|
|
139
|
+
"current",
|
|
140
|
+
css_class="form-check-input",
|
|
141
|
+
wrapper_class="form-check form-switch",
|
|
142
|
+
),
|
|
143
|
+
"receiver",
|
|
144
|
+
"antenna",
|
|
145
|
+
"radome",
|
|
146
|
+
css_class="slm-form-group",
|
|
147
|
+
),
|
|
148
|
+
css_class="col-4",
|
|
149
|
+
),
|
|
150
|
+
Div(
|
|
151
|
+
"agency",
|
|
152
|
+
"network",
|
|
153
|
+
Field("country", css_class="slm-country search-input"),
|
|
154
|
+
css_class="col-5",
|
|
155
|
+
),
|
|
156
|
+
css_class="row",
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
helper.attrs = {
|
|
160
|
+
"data_slm_initial": json.dumps(
|
|
161
|
+
{
|
|
162
|
+
field.name: field.field.initial
|
|
163
|
+
for field in self
|
|
164
|
+
if field.field.initial
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
return helper
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class StationFilter(BaseStationFilter):
|
|
172
|
+
"""
|
|
173
|
+
Edit API station filter extensions.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def get_form_class(self):
|
|
177
|
+
return StationFilterForm
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def current_equipment(self):
|
|
181
|
+
return self.form.cleaned_data.get("current", None)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class PassThroughRenderer(renderers.BaseRenderer):
|
|
185
|
+
"""
|
|
186
|
+
Return data as-is. View should supply a Response.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
media_type = ""
|
|
190
|
+
format = "legacy"
|
|
191
|
+
|
|
192
|
+
def render(self, data, accepted_media_type=None, renderer_context=None):
|
|
193
|
+
return data
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class DataTablesListMixin(mixins.ListModelMixin):
|
|
197
|
+
"""
|
|
198
|
+
A mixin for adapting list views to work with the datatables library.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
pagination_class = DataTablesPagination
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class AgencyViewSet(PublicAgencyViewSet):
|
|
205
|
+
def get_queryset(self):
|
|
206
|
+
return Agency.objects.membership(self.request.user)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class NetworkViewSet(PublicNetworkViewSet):
|
|
210
|
+
def get_queryset(self):
|
|
211
|
+
return Network.objects.visible_to(self.request.user)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ReviewRequestView(
|
|
215
|
+
DataTablesListMixin,
|
|
216
|
+
mixins.RetrieveModelMixin,
|
|
217
|
+
mixins.UpdateModelMixin,
|
|
218
|
+
mixins.ListModelMixin,
|
|
219
|
+
viewsets.GenericViewSet,
|
|
220
|
+
):
|
|
221
|
+
serializer_class = ReviewRequestSerializer
|
|
222
|
+
permission_classes = (IsAuthenticated, CanEditSite)
|
|
223
|
+
|
|
224
|
+
def perform_update(self, serializer):
|
|
225
|
+
slm_signals.review_requested.send(
|
|
226
|
+
sender=self,
|
|
227
|
+
site=serializer.instance,
|
|
228
|
+
detail=serializer.validated_data.get("detail", None),
|
|
229
|
+
request=self.request,
|
|
230
|
+
)
|
|
231
|
+
serializer.instance.refresh_from_db()
|
|
232
|
+
|
|
233
|
+
def get_queryset(self):
|
|
234
|
+
return (
|
|
235
|
+
Site.objects.editable_by(self.request.user)
|
|
236
|
+
.filter(review_requested__isnull=True)
|
|
237
|
+
.filter(status__in=SiteLogStatus.unpublished_states())
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class RejectUpdatesView(
|
|
242
|
+
DataTablesListMixin,
|
|
243
|
+
mixins.RetrieveModelMixin,
|
|
244
|
+
mixins.UpdateModelMixin,
|
|
245
|
+
mixins.ListModelMixin,
|
|
246
|
+
viewsets.GenericViewSet,
|
|
247
|
+
):
|
|
248
|
+
serializer_class = ReviewRequestSerializer
|
|
249
|
+
permission_classes = (IsAuthenticated, CanRejectReview)
|
|
250
|
+
|
|
251
|
+
def perform_update(self, serializer):
|
|
252
|
+
slm_signals.updates_rejected.send(
|
|
253
|
+
sender=self,
|
|
254
|
+
site=serializer.instance,
|
|
255
|
+
detail=serializer.validated_data.get("detail", None),
|
|
256
|
+
request=self.request,
|
|
257
|
+
)
|
|
258
|
+
serializer.instance.refresh_from_db()
|
|
259
|
+
|
|
260
|
+
def get_queryset(self):
|
|
261
|
+
return Site.objects.moderated(self.request.user).filter(
|
|
262
|
+
review_requested__isnull=False
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class StationListViewSet(
|
|
267
|
+
DataTablesListMixin,
|
|
268
|
+
mixins.CreateModelMixin,
|
|
269
|
+
mixins.RetrieveModelMixin,
|
|
270
|
+
mixins.UpdateModelMixin,
|
|
271
|
+
viewsets.GenericViewSet,
|
|
272
|
+
):
|
|
273
|
+
serializer_class = StationSerializer
|
|
274
|
+
permission_classes = (
|
|
275
|
+
IsAuthenticated,
|
|
276
|
+
CanEditSite,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
|
280
|
+
filterset_class = StationFilter
|
|
281
|
+
ordering_fields = ["name", "num_flags", "created", "last_update", "last_publish"]
|
|
282
|
+
ordering = ("name",)
|
|
283
|
+
|
|
284
|
+
def get_queryset(self):
|
|
285
|
+
return (
|
|
286
|
+
Site.objects.editable_by(self.request.user)
|
|
287
|
+
.prefetch_related(
|
|
288
|
+
"agencies", "networks", "owner__agencies", "last_user__agencies"
|
|
289
|
+
)
|
|
290
|
+
.select_related(
|
|
291
|
+
"owner",
|
|
292
|
+
"last_user",
|
|
293
|
+
"review_requested",
|
|
294
|
+
"updates_rejected",
|
|
295
|
+
"import_alert",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class LogEntryViewSet(DataTablesListMixin, viewsets.GenericViewSet):
|
|
301
|
+
serializer_class = LogEntrySerializer
|
|
302
|
+
permission_classes = (IsAuthenticated,)
|
|
303
|
+
|
|
304
|
+
class LogEntryFilter(CrispyFormCompat, FilterSet):
|
|
305
|
+
sites = None
|
|
306
|
+
|
|
307
|
+
def __init__(self, data=None, queryset=None, *, request=None, **kwargs):
|
|
308
|
+
super().__init__(data, queryset=queryset, request=request, **kwargs)
|
|
309
|
+
# we chain this filter so when someone filters the station list
|
|
310
|
+
# we can show the log entries corresponding to the filtered
|
|
311
|
+
# stations
|
|
312
|
+
self.sites = StationFilter(
|
|
313
|
+
data=data, queryset=Site.objects.all(), request=request, **kwargs
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def filter_queryset(self, queryset):
|
|
317
|
+
return super().filter_queryset(queryset).filter(site__in=self.sites.qs)
|
|
318
|
+
|
|
319
|
+
site = django_filters.CharFilter(field_name="site__name", lookup_expr="iexact")
|
|
320
|
+
user = django_filters.CharFilter(field_name="user__email", lookup_expr="iexact")
|
|
321
|
+
before = django_filters.CharFilter(field_name="timestamp", lookup_expr="lt")
|
|
322
|
+
after = django_filters.CharFilter(field_name="timestamp", lookup_expr="gte")
|
|
323
|
+
ip = django_filters.CharFilter(field_name="ip", lookup_expr="iexact")
|
|
324
|
+
|
|
325
|
+
class Meta:
|
|
326
|
+
model = LogEntry
|
|
327
|
+
fields = ("site", "user", "before", "after", "ip")
|
|
328
|
+
|
|
329
|
+
filter_backends = (DjangoFilterBackend,)
|
|
330
|
+
filterset_class = LogEntryFilter
|
|
331
|
+
|
|
332
|
+
def get_queryset(self):
|
|
333
|
+
return LogEntry.objects.for_user(self.request.user).select_related(
|
|
334
|
+
"section", "site", "file", "user"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class AlertViewSet(
|
|
339
|
+
DataTablesListMixin,
|
|
340
|
+
mixins.RetrieveModelMixin,
|
|
341
|
+
mixins.DestroyModelMixin,
|
|
342
|
+
viewsets.GenericViewSet,
|
|
343
|
+
):
|
|
344
|
+
serializer_class = AlertSerializer
|
|
345
|
+
permission_classes = (IsAuthenticated, CanDeleteAlert)
|
|
346
|
+
|
|
347
|
+
class AlertFilter(CrispyFormCompat, FilterSet):
|
|
348
|
+
sites = None
|
|
349
|
+
|
|
350
|
+
def __init__(self, data=None, queryset=None, *, request=None, **kwargs):
|
|
351
|
+
super().__init__(data, queryset=queryset, request=request, **kwargs)
|
|
352
|
+
self.sites = StationFilter(
|
|
353
|
+
data=data, queryset=Site.objects.all(), request=request, **kwargs
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
site = django_filters.CharFilter(method="for_site", distinct=True)
|
|
357
|
+
user = django_filters.CharFilter(method="for_user", distinct=True)
|
|
358
|
+
|
|
359
|
+
def filter_queryset(self, queryset):
|
|
360
|
+
return (
|
|
361
|
+
super()
|
|
362
|
+
.filter_queryset(queryset)
|
|
363
|
+
.concerning_sites(self.sites.qs)
|
|
364
|
+
.distinct()
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def for_site(self, queryset, name, value):
|
|
368
|
+
return queryset.for_site(
|
|
369
|
+
Site.objects.filter(name__iexact=value).first()
|
|
370
|
+
).distinct()
|
|
371
|
+
|
|
372
|
+
def for_user(self, queryset, name, value):
|
|
373
|
+
return queryset.for_user(
|
|
374
|
+
get_user_model().filter(email__iexact=value)
|
|
375
|
+
).distinct()
|
|
376
|
+
|
|
377
|
+
class Meta:
|
|
378
|
+
model = Alert
|
|
379
|
+
fields = (
|
|
380
|
+
"site",
|
|
381
|
+
"user",
|
|
382
|
+
)
|
|
383
|
+
distinct = True
|
|
384
|
+
|
|
385
|
+
filter_backends = (DjangoFilterBackend,)
|
|
386
|
+
filterset_class = AlertFilter
|
|
387
|
+
|
|
388
|
+
def get_queryset(self):
|
|
389
|
+
Alert.objects.delete_expired()
|
|
390
|
+
return Alert.objects.visible_to(self.request.user)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class UserProfileViewSet(
|
|
394
|
+
mixins.UpdateModelMixin,
|
|
395
|
+
mixins.RetrieveModelMixin,
|
|
396
|
+
mixins.ListModelMixin,
|
|
397
|
+
viewsets.GenericViewSet,
|
|
398
|
+
):
|
|
399
|
+
serializer_class = UserSerializer
|
|
400
|
+
permission_classes = (
|
|
401
|
+
IsAuthenticated,
|
|
402
|
+
IsUserOrAdmin,
|
|
403
|
+
)
|
|
404
|
+
parser_classes = (FormParser, JSONParser)
|
|
405
|
+
|
|
406
|
+
def get_queryset(self):
|
|
407
|
+
return (
|
|
408
|
+
get_user_model()
|
|
409
|
+
.objects.filter(id=self.request.user.id)
|
|
410
|
+
.select_related("profile")
|
|
411
|
+
.prefetch_related("agencies")
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def list(self, request, **kwargs):
|
|
415
|
+
resp = super(UserProfileViewSet, self).list(request, **kwargs)
|
|
416
|
+
resp.data = resp.data[0]
|
|
417
|
+
return resp
|
|
418
|
+
|
|
419
|
+
def update(self, request, *args, **kwargs):
|
|
420
|
+
from django.contrib import messages
|
|
421
|
+
|
|
422
|
+
resp = super().update(request, *args, **kwargs)
|
|
423
|
+
if resp.status_code < 300:
|
|
424
|
+
messages.add_message(
|
|
425
|
+
request, messages.SUCCESS, _("User profile updated successfully.")
|
|
426
|
+
)
|
|
427
|
+
return resp
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class SiteLogDownloadViewSet(BaseSiteLogDownloadViewSet):
|
|
431
|
+
class ArchiveIndexFilter(BaseSiteLogDownloadViewSet.ArchiveIndexFilter):
|
|
432
|
+
unpublished = SLMBooleanFilter(method="noop")
|
|
433
|
+
unpublished.help = _(
|
|
434
|
+
"If true, download the published version of the log. If false,"
|
|
435
|
+
"the HEAD version of the log "
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
class Meta(BaseSiteLogDownloadViewSet.ArchiveIndexFilter.Meta):
|
|
439
|
+
fields = (
|
|
440
|
+
"unpublished",
|
|
441
|
+
*BaseSiteLogDownloadViewSet.ArchiveIndexFilter.Meta.fields,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
filterset_class = ArchiveIndexFilter
|
|
445
|
+
|
|
446
|
+
def retrieve(self, request, *args, **kwargs):
|
|
447
|
+
if request.GET.get("unpublished", False):
|
|
448
|
+
try:
|
|
449
|
+
site = Site.objects.get(name__iexact=kwargs.get("site"))
|
|
450
|
+
return FileResponse(
|
|
451
|
+
BytesIO(
|
|
452
|
+
SiteLogSerializer(instance=site, published=None)
|
|
453
|
+
.format(request.accepted_renderer.format)
|
|
454
|
+
.encode()
|
|
455
|
+
),
|
|
456
|
+
filename=site.get_filename(
|
|
457
|
+
log_format=request.accepted_renderer.format,
|
|
458
|
+
epoch=datetime.now(),
|
|
459
|
+
name_len=request.GET.get("name_len", None),
|
|
460
|
+
lower_case=request.GET.get("lower_case", False),
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
except Site.DoesNotExist:
|
|
464
|
+
raise Http404()
|
|
465
|
+
return super().retrieve(request, *args, **kwargs)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class SectionViewSet(type):
|
|
469
|
+
"""
|
|
470
|
+
POST, PUT and PATCH all behave the same way for the section edit API.
|
|
471
|
+
|
|
472
|
+
Each runs through the following steps:
|
|
473
|
+
|
|
474
|
+
1) Permission check - only moderators are permitted to submit _flags or
|
|
475
|
+
publish=True
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
def __new__(metacls, name, bases, namespace, **kwargs):
|
|
479
|
+
ModelClass = kwargs.pop("model")
|
|
480
|
+
can_delete = issubclass(ModelClass, SiteSubSection)
|
|
481
|
+
parents = [
|
|
482
|
+
*bases,
|
|
483
|
+
mixins.RetrieveModelMixin,
|
|
484
|
+
mixins.ListModelMixin,
|
|
485
|
+
mixins.UpdateModelMixin,
|
|
486
|
+
mixins.CreateModelMixin,
|
|
487
|
+
]
|
|
488
|
+
if can_delete:
|
|
489
|
+
parents.append(mixins.DestroyModelMixin)
|
|
490
|
+
|
|
491
|
+
parents.append(viewsets.GenericViewSet)
|
|
492
|
+
obj = super().__new__(metacls, name, tuple(parents), namespace)
|
|
493
|
+
|
|
494
|
+
class ViewSetFilter(CrispyFormCompat, FilterSet):
|
|
495
|
+
site = django_filters.CharFilter(
|
|
496
|
+
field_name="site__name", lookup_expr="iexact"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
class Meta:
|
|
500
|
+
model = ModelClass
|
|
501
|
+
fields = ["site", "id"] + (
|
|
502
|
+
["subsection"] if issubclass(ModelClass, SiteSubSection) else []
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
class ViewSetSerializer(ModelSerializer):
|
|
506
|
+
def build_standard_field(self, field_name, model_field):
|
|
507
|
+
"""
|
|
508
|
+
Force EnumFields into django_enum's enum field type.
|
|
509
|
+
"""
|
|
510
|
+
if model_field.__class__ in {
|
|
511
|
+
EnumIntegerField,
|
|
512
|
+
EnumPositiveIntegerField,
|
|
513
|
+
EnumBigIntegerField,
|
|
514
|
+
EnumPositiveSmallIntegerField,
|
|
515
|
+
EnumPositiveBigIntegerField,
|
|
516
|
+
EnumSmallIntegerField,
|
|
517
|
+
EnumCharField,
|
|
518
|
+
}:
|
|
519
|
+
return EnumField, {
|
|
520
|
+
**super().build_standard_field(field_name, model_field)[1],
|
|
521
|
+
"enum": model_field.enum,
|
|
522
|
+
"strict": model_field.strict,
|
|
523
|
+
}
|
|
524
|
+
return super().build_standard_field(field_name, model_field)
|
|
525
|
+
|
|
526
|
+
serializer_field_mapping = {
|
|
527
|
+
**ModelSerializer.serializer_field_mapping,
|
|
528
|
+
models.DateTimeField: SLMDateTimeField,
|
|
529
|
+
gis_models.PointField: SLMPointField,
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
_diff = serializers.SerializerMethodField(read_only=True)
|
|
533
|
+
_flags = serializers.JSONField(
|
|
534
|
+
read_only=False,
|
|
535
|
+
required=False,
|
|
536
|
+
encoder=SiteSection._meta.get_field("_flags").encoder,
|
|
537
|
+
decoder=SiteSection._meta.get_field("_flags").decoder,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
can_publish = serializers.SerializerMethodField(read_only=True)
|
|
541
|
+
|
|
542
|
+
publish = serializers.BooleanField(write_only=True, required=False)
|
|
543
|
+
revert = serializers.BooleanField(write_only=True, required=False)
|
|
544
|
+
|
|
545
|
+
def to_internal_value(self, data):
|
|
546
|
+
"""
|
|
547
|
+
Swap empty string for None in post data for any field that
|
|
548
|
+
allows null. (You'd think this wouldn't be necessary or that
|
|
549
|
+
DRF would have some kind of switch?)
|
|
550
|
+
|
|
551
|
+
:param data:
|
|
552
|
+
:return:
|
|
553
|
+
"""
|
|
554
|
+
data = data.copy()
|
|
555
|
+
for field, value in data.items():
|
|
556
|
+
if value == "" and getattr(
|
|
557
|
+
self.fields.get(field, None), "allow_null", False
|
|
558
|
+
):
|
|
559
|
+
data[field] = None
|
|
560
|
+
|
|
561
|
+
# if these are set to None - validation rejects them
|
|
562
|
+
# todo anything else like this?
|
|
563
|
+
if data.get("id", False) in {None, ""}:
|
|
564
|
+
del data["id"]
|
|
565
|
+
|
|
566
|
+
if data.get("subsection", False) in {None, ""}:
|
|
567
|
+
del data["subsection"]
|
|
568
|
+
|
|
569
|
+
return super().to_internal_value(data)
|
|
570
|
+
|
|
571
|
+
def get_can_publish(self, obj):
|
|
572
|
+
if "request" in self.context:
|
|
573
|
+
return self.context["request"].user.is_moderator()
|
|
574
|
+
return None
|
|
575
|
+
|
|
576
|
+
def get__diff(self, obj):
|
|
577
|
+
return obj.published_diff()
|
|
578
|
+
|
|
579
|
+
def perform_section_update(self, validated_data, instance=None):
|
|
580
|
+
"""
|
|
581
|
+
We perform the same update routines on either a PUT, PATCH or
|
|
582
|
+
POST for uniformity and convenience. This could include:
|
|
583
|
+
|
|
584
|
+
1. Field Update
|
|
585
|
+
* If current instance is published and this contains
|
|
586
|
+
edits create a new db row
|
|
587
|
+
* If current instance is not published update instance
|
|
588
|
+
* Issue section edited event.
|
|
589
|
+
2. New Section
|
|
590
|
+
* Issue section added event.
|
|
591
|
+
3. Flag Update
|
|
592
|
+
* Issue fields_flagged or flags_cleared event.
|
|
593
|
+
4. Publish
|
|
594
|
+
* Delete the previously published instance and set
|
|
595
|
+
instance (old or added) to published and issue
|
|
596
|
+
site_published event.
|
|
597
|
+
5. Any combination of 1, 2, 3 and 4
|
|
598
|
+
|
|
599
|
+
:param validated_data: The validated POST data
|
|
600
|
+
:param instance: The instance if this was a PUT or PATCH. POST
|
|
601
|
+
behaves the same way - so we also resolve the instance from
|
|
602
|
+
POST data if the update url was not used.
|
|
603
|
+
:return:
|
|
604
|
+
"""
|
|
605
|
+
with transaction.atomic():
|
|
606
|
+
# permission check - we do this here because operating
|
|
607
|
+
# directly on POST data in a Permission object is more
|
|
608
|
+
# difficult
|
|
609
|
+
site = validated_data.get("site") or instance.site
|
|
610
|
+
is_moderator = site.is_moderator(self.context["request"].user)
|
|
611
|
+
do_publish = validated_data.pop("publish", False)
|
|
612
|
+
do_revert = validated_data.pop("revert", False)
|
|
613
|
+
update_status = None
|
|
614
|
+
|
|
615
|
+
if not is_moderator:
|
|
616
|
+
# non-moderators are not allowed to publish!
|
|
617
|
+
if do_publish:
|
|
618
|
+
raise PermissionDenied(
|
|
619
|
+
_(
|
|
620
|
+
"You must have moderator privileges to "
|
|
621
|
+
"publish site log edits."
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# we don't disallow an accompanying edit with _flags -
|
|
626
|
+
# we just strip them out of the user doesnt have
|
|
627
|
+
# permission to add them
|
|
628
|
+
validated_data.pop("_flags", None)
|
|
629
|
+
|
|
630
|
+
section_id = validated_data.pop("id", None)
|
|
631
|
+
subsection = validated_data.get("subsection", None)
|
|
632
|
+
|
|
633
|
+
line_filter = Q(site=site)
|
|
634
|
+
if subsection is not None:
|
|
635
|
+
line_filter &= Q(subsection=subsection)
|
|
636
|
+
|
|
637
|
+
# get the previous section instance - if it exists
|
|
638
|
+
if instance is None:
|
|
639
|
+
# this is either a section where only one can exist or
|
|
640
|
+
# a subsection where multiple can exist
|
|
641
|
+
if not self.allow_multiple():
|
|
642
|
+
instance = (
|
|
643
|
+
ModelClass.objects.filter(site=site)
|
|
644
|
+
.order_by("-edited")
|
|
645
|
+
.select_for_update()
|
|
646
|
+
.first()
|
|
647
|
+
)
|
|
648
|
+
# this is a subsection - if the subsection IDs are not
|
|
649
|
+
# present it is
|
|
650
|
+
elif subsection is not None:
|
|
651
|
+
instance = (
|
|
652
|
+
ModelClass.objects.filter(line_filter)
|
|
653
|
+
.order_by("-edited")
|
|
654
|
+
.select_for_update()
|
|
655
|
+
.first()
|
|
656
|
+
)
|
|
657
|
+
else:
|
|
658
|
+
instance = (
|
|
659
|
+
ModelClass.objects.filter(pk=instance.pk)
|
|
660
|
+
.select_for_update()
|
|
661
|
+
.first()
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
if do_revert and instance:
|
|
665
|
+
if instance.revert():
|
|
666
|
+
reverted_to = ModelClass.objects.filter(
|
|
667
|
+
line_filter & Q(published=True)
|
|
668
|
+
).first()
|
|
669
|
+
if reverted_to:
|
|
670
|
+
return reverted_to
|
|
671
|
+
self.context[
|
|
672
|
+
"request"
|
|
673
|
+
]._response_status = status.HTTP_204_NO_CONTENT
|
|
674
|
+
return instance
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
# this is a new section
|
|
678
|
+
if instance is None:
|
|
679
|
+
new_section = super().create(validated_data)
|
|
680
|
+
new_section.full_clean()
|
|
681
|
+
new_section.save()
|
|
682
|
+
update_status = new_section.edited
|
|
683
|
+
if not isinstance(new_section, SiteForm):
|
|
684
|
+
form = new_section.site.siteform_set.head()
|
|
685
|
+
if form is None:
|
|
686
|
+
form = SiteForm.objects.create(
|
|
687
|
+
site=site, published=False, report_type="NEW"
|
|
688
|
+
)
|
|
689
|
+
elif form.published:
|
|
690
|
+
form.pk = None
|
|
691
|
+
form.published = False
|
|
692
|
+
if self.context["request"].user.full_name:
|
|
693
|
+
form.prepared_by = self.context[
|
|
694
|
+
"request"
|
|
695
|
+
].user.full_name
|
|
696
|
+
form.save()
|
|
697
|
+
slm_signals.section_added.send(
|
|
698
|
+
sender=self,
|
|
699
|
+
site=site,
|
|
700
|
+
user=self.context["request"].user,
|
|
701
|
+
request=self.context["request"],
|
|
702
|
+
timestamp=update_status,
|
|
703
|
+
section=new_section,
|
|
704
|
+
)
|
|
705
|
+
instance = new_section
|
|
706
|
+
else:
|
|
707
|
+
# if an object id was present and it is not at or past
|
|
708
|
+
# the last section ID - we have a concurrent edit race
|
|
709
|
+
# condition between one or more users.
|
|
710
|
+
if section_id is not None and section_id < instance.id:
|
|
711
|
+
raise DRFValidationError(
|
|
712
|
+
_(
|
|
713
|
+
"Edits must be made on HEAD. Someone else "
|
|
714
|
+
"may be editing the log concurrently. "
|
|
715
|
+
"Refresh and try again."
|
|
716
|
+
)
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
# if not new - does this section have edits?
|
|
720
|
+
update = False
|
|
721
|
+
flags = validated_data.get("_flags", instance._flags)
|
|
722
|
+
edited_fields = []
|
|
723
|
+
|
|
724
|
+
# todo this diffing code is getting a bit messy because
|
|
725
|
+
# of all the special type cases - consider a refactor
|
|
726
|
+
# also needs to be DRYed w/ published_diff function
|
|
727
|
+
for field in ModelClass.site_log_fields():
|
|
728
|
+
if field in validated_data:
|
|
729
|
+
is_many = isinstance(
|
|
730
|
+
instance._meta.get_field(field),
|
|
731
|
+
models.ManyToManyField,
|
|
732
|
+
)
|
|
733
|
+
new_value = validated_data.get(field)
|
|
734
|
+
old_value = getattr(instance, field)
|
|
735
|
+
if (
|
|
736
|
+
not is_many
|
|
737
|
+
and (
|
|
738
|
+
getattr(new_value, "coords", None)
|
|
739
|
+
!= getattr(old_value, "coords", None)
|
|
740
|
+
if isinstance(new_value, Point)
|
|
741
|
+
else new_value != old_value
|
|
742
|
+
)
|
|
743
|
+
) or (
|
|
744
|
+
is_many
|
|
745
|
+
and set(new_value)
|
|
746
|
+
!= set(getattr(instance, field).all())
|
|
747
|
+
):
|
|
748
|
+
update = True
|
|
749
|
+
if not instance.published:
|
|
750
|
+
edited_fields.append(field)
|
|
751
|
+
if is_many:
|
|
752
|
+
if new_value:
|
|
753
|
+
getattr(instance, field).set(new_value)
|
|
754
|
+
else:
|
|
755
|
+
getattr(instance, field).clear()
|
|
756
|
+
else:
|
|
757
|
+
setattr(instance, field, new_value)
|
|
758
|
+
|
|
759
|
+
if field in flags:
|
|
760
|
+
del flags[field]
|
|
761
|
+
if update:
|
|
762
|
+
if instance.published:
|
|
763
|
+
validated_data["_flags"] = flags
|
|
764
|
+
instance.pk = None # copy the instance
|
|
765
|
+
instance.published = False
|
|
766
|
+
instance.save()
|
|
767
|
+
for param, value in validated_data.items():
|
|
768
|
+
if isinstance(
|
|
769
|
+
instance._meta.get_field(param),
|
|
770
|
+
models.ManyToManyField,
|
|
771
|
+
):
|
|
772
|
+
if value:
|
|
773
|
+
getattr(instance, param).set(value)
|
|
774
|
+
else:
|
|
775
|
+
getattr(instance, param).clear()
|
|
776
|
+
else:
|
|
777
|
+
setattr(instance, param, value)
|
|
778
|
+
instance.full_clean()
|
|
779
|
+
instance.save()
|
|
780
|
+
else:
|
|
781
|
+
instance._flags = flags
|
|
782
|
+
instance.full_clean()
|
|
783
|
+
instance.save()
|
|
784
|
+
|
|
785
|
+
# make sure we use edit timestamp if publish and
|
|
786
|
+
# edit are simultaneous
|
|
787
|
+
update_status = instance.edited
|
|
788
|
+
if not isinstance(instance, SiteForm):
|
|
789
|
+
form = instance.site.siteform_set.head()
|
|
790
|
+
if form.published:
|
|
791
|
+
form.pk = None
|
|
792
|
+
form.published = False
|
|
793
|
+
if self.context["request"].user.full_name:
|
|
794
|
+
form.prepared_by = self.context[
|
|
795
|
+
"request"
|
|
796
|
+
].user.full_name
|
|
797
|
+
form.save()
|
|
798
|
+
slm_signals.section_edited.send(
|
|
799
|
+
sender=self,
|
|
800
|
+
site=site,
|
|
801
|
+
user=self.context["request"].user,
|
|
802
|
+
request=self.context["request"],
|
|
803
|
+
timestamp=update_status,
|
|
804
|
+
section=instance,
|
|
805
|
+
fields=edited_fields,
|
|
806
|
+
)
|
|
807
|
+
elif "_flags" in validated_data:
|
|
808
|
+
# this is just a flag update
|
|
809
|
+
added = len(flags) - (
|
|
810
|
+
len(instance._flags) if instance._flags else 0
|
|
811
|
+
)
|
|
812
|
+
site.num_flags += added
|
|
813
|
+
if site.num_flags < 0:
|
|
814
|
+
site.num_flags = 0
|
|
815
|
+
|
|
816
|
+
site.save()
|
|
817
|
+
instance._flags = flags
|
|
818
|
+
instance.save()
|
|
819
|
+
|
|
820
|
+
if do_publish:
|
|
821
|
+
update_status = update_status or now()
|
|
822
|
+
if not isinstance(instance, SiteForm):
|
|
823
|
+
form = instance.site.siteform_set.head()
|
|
824
|
+
if form.published:
|
|
825
|
+
form.pk = None
|
|
826
|
+
form.published = False
|
|
827
|
+
if self.context["request"].user.full_name:
|
|
828
|
+
form.prepared_by = self.context[
|
|
829
|
+
"request"
|
|
830
|
+
].user.full_name
|
|
831
|
+
form.save(modified_section=instance.dot_index)
|
|
832
|
+
form.publish(
|
|
833
|
+
request=self.context.get("request", None),
|
|
834
|
+
silent=True,
|
|
835
|
+
timestamp=update_status,
|
|
836
|
+
update_site=False,
|
|
837
|
+
)
|
|
838
|
+
instance.publish(
|
|
839
|
+
request=self.context.get("request", None),
|
|
840
|
+
timestamp=update_status,
|
|
841
|
+
update_site=False, # this is done below
|
|
842
|
+
)
|
|
843
|
+
try:
|
|
844
|
+
instance.refresh_from_db()
|
|
845
|
+
except instance.DoesNotExist:
|
|
846
|
+
# hacky but it works, not really another way
|
|
847
|
+
# to pass information up and down the chain
|
|
848
|
+
# from serializer to view - probably means a
|
|
849
|
+
# lot of this logic belongs in the view
|
|
850
|
+
self.context[
|
|
851
|
+
"request"
|
|
852
|
+
]._response_status = status.HTTP_204_NO_CONTENT
|
|
853
|
+
|
|
854
|
+
if update_status:
|
|
855
|
+
site.update_status(
|
|
856
|
+
save=True,
|
|
857
|
+
user=self.context["request"].user,
|
|
858
|
+
timestamp=update_status,
|
|
859
|
+
)
|
|
860
|
+
return instance
|
|
861
|
+
except DjangoValidationError as ve:
|
|
862
|
+
raise DRFValidationError(ve.message_dict)
|
|
863
|
+
|
|
864
|
+
def update(self, instance, validated_data):
|
|
865
|
+
return self.perform_section_update(
|
|
866
|
+
validated_data=validated_data, instance=instance
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
def create(self, validated_data):
|
|
870
|
+
return self.perform_section_update(validated_data=validated_data)
|
|
871
|
+
|
|
872
|
+
@classmethod
|
|
873
|
+
def allow_multiple(cls):
|
|
874
|
+
"""
|
|
875
|
+
Does this serializer allow multiple sections per site?
|
|
876
|
+
:return: True if multiple sections are allowed - False
|
|
877
|
+
otherwise
|
|
878
|
+
"""
|
|
879
|
+
return issubclass(ModelClass, SiteSubSection)
|
|
880
|
+
|
|
881
|
+
def build_relational_field(self, field_name, relation_info):
|
|
882
|
+
"""
|
|
883
|
+
By default DRF will use PrimaryKeyRelatedFields to represent
|
|
884
|
+
ForeignKey relations - for certain fields in the API we'd
|
|
885
|
+
rather tie them based on a string field on the related model.
|
|
886
|
+
If API_RELATED_FIELD is set to that field on a related model
|
|
887
|
+
we use a SlugRelatedField instead so instead of passing PKs
|
|
888
|
+
in the API, users can pass human readable names instead and do
|
|
889
|
+
not have to do the work to figure out what the primary key is
|
|
890
|
+
under the covers - as this is SLM database instance specific.
|
|
891
|
+
|
|
892
|
+
This is also critical for our autocomplete fields.
|
|
893
|
+
"""
|
|
894
|
+
related = getattr(
|
|
895
|
+
relation_info.related_model, "API_RELATED_FIELD", None
|
|
896
|
+
)
|
|
897
|
+
_, defaults = super().build_relational_field(field_name, relation_info)
|
|
898
|
+
if related:
|
|
899
|
+
return (SlugRelatedField, {**defaults, "slug_field": related})
|
|
900
|
+
return _, defaults
|
|
901
|
+
|
|
902
|
+
class Meta:
|
|
903
|
+
model = ModelClass
|
|
904
|
+
|
|
905
|
+
# prevent UniqueTogetherValidator from being attached
|
|
906
|
+
# subsection and section ids are attached after the data
|
|
907
|
+
# validation process and uniqueness qualities are enforced
|
|
908
|
+
# by the code, it should be impossible for a user to use the
|
|
909
|
+
# api in a way that would trigger the database to violate
|
|
910
|
+
# this constraint
|
|
911
|
+
validators = []
|
|
912
|
+
|
|
913
|
+
fields = [
|
|
914
|
+
"site",
|
|
915
|
+
"id",
|
|
916
|
+
"publish",
|
|
917
|
+
"published",
|
|
918
|
+
"revert",
|
|
919
|
+
"can_publish",
|
|
920
|
+
"_flags",
|
|
921
|
+
"_diff",
|
|
922
|
+
*ModelClass.site_log_fields(),
|
|
923
|
+
] + (
|
|
924
|
+
["subsection", "heading", "effective", "is_deleted"]
|
|
925
|
+
if issubclass(ModelClass, SiteSubSection)
|
|
926
|
+
else []
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
extra_kwargs = {
|
|
930
|
+
"id": {"required": False, "read_only": False},
|
|
931
|
+
"site": {"required": True},
|
|
932
|
+
**(
|
|
933
|
+
{
|
|
934
|
+
"heading": {"required": False, "read_only": True},
|
|
935
|
+
"effective": {"required": False, "read_only": True},
|
|
936
|
+
"is_deleted": {"required": False, "read_only": True},
|
|
937
|
+
"subsection": {"required": False},
|
|
938
|
+
"four_character_id": { # special case
|
|
939
|
+
"required": False,
|
|
940
|
+
"read_only": True,
|
|
941
|
+
},
|
|
942
|
+
"nine_character_id": { # special case
|
|
943
|
+
"required": False,
|
|
944
|
+
"read_only": True,
|
|
945
|
+
},
|
|
946
|
+
"custom_graphic": {"trim_whitespace": False},
|
|
947
|
+
}
|
|
948
|
+
if issubclass(ModelClass, SiteSubSection)
|
|
949
|
+
else {}
|
|
950
|
+
),
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
obj.serializer_class = ViewSetSerializer
|
|
954
|
+
obj.filterset_class = ViewSetFilter
|
|
955
|
+
obj.permission_classes = (IsAuthenticated, CanEditSite, UpdateAdminOnly)
|
|
956
|
+
obj.pagination_class = DataTablesPagination
|
|
957
|
+
obj.filter_backends = (DjangoFilterBackend,)
|
|
958
|
+
|
|
959
|
+
def get_queryset(self):
|
|
960
|
+
return ModelClass.objects.editable_by(self.request.user).select_related(
|
|
961
|
+
"site"
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
def create(self, request, *args, **kwargs):
|
|
965
|
+
serializer = self.get_serializer(data=request.data)
|
|
966
|
+
serializer.is_valid(raise_exception=True)
|
|
967
|
+
try:
|
|
968
|
+
self.perform_create(serializer)
|
|
969
|
+
except DjangoValidationError as ve:
|
|
970
|
+
raise DRFValidationError(ve.message_dict)
|
|
971
|
+
|
|
972
|
+
headers = self.get_success_headers(serializer.data)
|
|
973
|
+
return Response(
|
|
974
|
+
serializer.data,
|
|
975
|
+
status=getattr(request, "_response_status", status.HTTP_201_CREATED),
|
|
976
|
+
headers=headers,
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
def update(self, request, *args, **kwargs):
|
|
980
|
+
response = mixins.UpdateModelMixin.update(self, request, *args, **kwargs)
|
|
981
|
+
response.status_code = getattr(
|
|
982
|
+
request, "_response_status", response.status_code
|
|
983
|
+
)
|
|
984
|
+
return response
|
|
985
|
+
|
|
986
|
+
def destroy(self, request, *args, **kwargs):
|
|
987
|
+
instance = self.get_object()
|
|
988
|
+
instance = self.perform_destroy(instance)
|
|
989
|
+
if instance:
|
|
990
|
+
serializer = self.get_serializer(instance=instance)
|
|
991
|
+
headers = self.get_success_headers(serializer.data)
|
|
992
|
+
return Response(
|
|
993
|
+
serializer.data,
|
|
994
|
+
status=getattr(request, "_response_status", status.HTTP_200_OK),
|
|
995
|
+
headers=headers,
|
|
996
|
+
)
|
|
997
|
+
return Response(
|
|
998
|
+
status=getattr(request, "_response_status", status.HTTP_204_NO_CONTENT)
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
def perform_destroy(self, instance):
|
|
1002
|
+
"""
|
|
1003
|
+
Deletes must happen on head:
|
|
1004
|
+
|
|
1005
|
+
1) If head is published, the delete flag will be marked
|
|
1006
|
+
2) If head is unpublished, but no previously published section
|
|
1007
|
+
exists the record will be deleted.
|
|
1008
|
+
3) If head is unpublished, but previous published sections exist,
|
|
1009
|
+
the unpublished record will be deleted and the published record
|
|
1010
|
+
will have its is_deleted flag set.
|
|
1011
|
+
|
|
1012
|
+
Sections are fully deleted when a published record with the
|
|
1013
|
+
is_deleted flag set is published.
|
|
1014
|
+
|
|
1015
|
+
:param self: view class instance
|
|
1016
|
+
:param instance: The section or subsection instance
|
|
1017
|
+
"""
|
|
1018
|
+
with transaction.atomic():
|
|
1019
|
+
section = ModelClass.objects.select_for_update().get(pk=instance.pk)
|
|
1020
|
+
site = section.site
|
|
1021
|
+
published = ModelClass.objects.filter(
|
|
1022
|
+
site=section.site, subsection=section.subsection, published=True
|
|
1023
|
+
).first()
|
|
1024
|
+
|
|
1025
|
+
if not section.published:
|
|
1026
|
+
# we delete it if this section or subsection has never
|
|
1027
|
+
# been published before
|
|
1028
|
+
section.delete()
|
|
1029
|
+
|
|
1030
|
+
if published:
|
|
1031
|
+
published.is_deleted = True
|
|
1032
|
+
published.save()
|
|
1033
|
+
form = section.site.siteform_set.head()
|
|
1034
|
+
if form.published:
|
|
1035
|
+
form.pk = None
|
|
1036
|
+
form.published = False
|
|
1037
|
+
if self.request.user.full_name:
|
|
1038
|
+
form.prepared_by = self.request.user.full_name
|
|
1039
|
+
form.save()
|
|
1040
|
+
to_return = published
|
|
1041
|
+
else:
|
|
1042
|
+
to_return = None
|
|
1043
|
+
|
|
1044
|
+
slm_signals.section_deleted.send(
|
|
1045
|
+
sender=self,
|
|
1046
|
+
site=site,
|
|
1047
|
+
user=self.request.user,
|
|
1048
|
+
request=self.request,
|
|
1049
|
+
timestamp=now(),
|
|
1050
|
+
section=section,
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
site.update_status(save=True, user=self.request.user, timestamp=now())
|
|
1054
|
+
return to_return
|
|
1055
|
+
|
|
1056
|
+
obj.get_queryset = get_queryset
|
|
1057
|
+
if can_delete:
|
|
1058
|
+
obj.perform_destroy = perform_destroy
|
|
1059
|
+
obj.destroy = destroy
|
|
1060
|
+
obj.create = create
|
|
1061
|
+
obj.update = update
|
|
1062
|
+
return obj
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
# TODO all these can be constructed dynamically from the models
|
|
1066
|
+
class SiteFormViewSet(metaclass=SectionViewSet, model=SiteForm):
|
|
1067
|
+
pass
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
class SiteIdentificationViewSet(metaclass=SectionViewSet, model=SiteIdentification):
|
|
1071
|
+
pass
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
class SiteLocationViewSet(metaclass=SectionViewSet, model=SiteLocation):
|
|
1075
|
+
pass
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
class SiteReceiverViewSet(metaclass=SectionViewSet, model=SiteReceiver):
|
|
1079
|
+
pass
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
class SiteAntennaViewSet(metaclass=SectionViewSet, model=SiteAntenna):
|
|
1083
|
+
pass
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
class SiteSurveyedLocalTiesViewSet(
|
|
1087
|
+
metaclass=SectionViewSet, model=SiteSurveyedLocalTies
|
|
1088
|
+
):
|
|
1089
|
+
pass
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
class SiteFrequencyStandardViewSet(
|
|
1093
|
+
metaclass=SectionViewSet, model=SiteFrequencyStandard
|
|
1094
|
+
):
|
|
1095
|
+
pass
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
class SiteCollocationViewSet(metaclass=SectionViewSet, model=SiteCollocation):
|
|
1099
|
+
pass
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
class SiteHumiditySensorViewSet(metaclass=SectionViewSet, model=SiteHumiditySensor):
|
|
1103
|
+
pass
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
class SitePressureSensorViewSet(metaclass=SectionViewSet, model=SitePressureSensor):
|
|
1107
|
+
pass
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
class SiteTemperatureSensorViewSet(
|
|
1111
|
+
metaclass=SectionViewSet, model=SiteTemperatureSensor
|
|
1112
|
+
):
|
|
1113
|
+
pass
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
class SiteWaterVaporRadiometerViewSet(
|
|
1117
|
+
metaclass=SectionViewSet, model=SiteWaterVaporRadiometer
|
|
1118
|
+
):
|
|
1119
|
+
pass
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
class SiteOtherInstrumentationViewSet(
|
|
1123
|
+
metaclass=SectionViewSet, model=SiteOtherInstrumentation
|
|
1124
|
+
):
|
|
1125
|
+
pass
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
class SiteRadioInterferencesViewSet(
|
|
1129
|
+
metaclass=SectionViewSet, model=SiteRadioInterferences
|
|
1130
|
+
):
|
|
1131
|
+
pass
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
class SiteMultiPathSourcesViewSet(metaclass=SectionViewSet, model=SiteMultiPathSources):
|
|
1135
|
+
pass
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
class SiteSignalObstructionsViewSet(
|
|
1139
|
+
metaclass=SectionViewSet, model=SiteSignalObstructions
|
|
1140
|
+
):
|
|
1141
|
+
pass
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
class SiteLocalEpisodicEffectsViewSet(
|
|
1145
|
+
metaclass=SectionViewSet, model=SiteLocalEpisodicEffects
|
|
1146
|
+
):
|
|
1147
|
+
pass
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
class SiteOperationalContactViewSet(
|
|
1151
|
+
metaclass=SectionViewSet, model=SiteOperationalContact
|
|
1152
|
+
):
|
|
1153
|
+
pass
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
class SiteResponsibleAgencyViewSet(
|
|
1157
|
+
metaclass=SectionViewSet, model=SiteResponsibleAgency
|
|
1158
|
+
):
|
|
1159
|
+
pass
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
class SiteMoreInformationViewSet(metaclass=SectionViewSet, model=SiteMoreInformation):
|
|
1163
|
+
pass
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
class SiteFileUploadViewSet(
|
|
1167
|
+
DataTablesListMixin,
|
|
1168
|
+
mixins.CreateModelMixin,
|
|
1169
|
+
mixins.RetrieveModelMixin,
|
|
1170
|
+
mixins.UpdateModelMixin,
|
|
1171
|
+
mixins.DestroyModelMixin,
|
|
1172
|
+
viewsets.GenericViewSet,
|
|
1173
|
+
):
|
|
1174
|
+
logger = getLogger("slm.api.edit.views.SiteFileUploadViewSet")
|
|
1175
|
+
|
|
1176
|
+
serializer_class = SiteFileUploadSerializer
|
|
1177
|
+
permission_classes = (
|
|
1178
|
+
IsAuthenticated,
|
|
1179
|
+
CanEditSite,
|
|
1180
|
+
)
|
|
1181
|
+
parser_classes = (MultiPartParser, FormParser, JSONParser, FileUploadParser)
|
|
1182
|
+
|
|
1183
|
+
site = None
|
|
1184
|
+
|
|
1185
|
+
SECTION_VIEWS = {
|
|
1186
|
+
0: SiteFormViewSet,
|
|
1187
|
+
1: SiteIdentificationViewSet,
|
|
1188
|
+
2: SiteLocationViewSet,
|
|
1189
|
+
3: SiteReceiverViewSet,
|
|
1190
|
+
4: SiteAntennaViewSet,
|
|
1191
|
+
5: SiteSurveyedLocalTiesViewSet,
|
|
1192
|
+
6: SiteFrequencyStandardViewSet,
|
|
1193
|
+
7: SiteCollocationViewSet,
|
|
1194
|
+
(8, 1): SiteHumiditySensorViewSet,
|
|
1195
|
+
(8, 2): SitePressureSensorViewSet,
|
|
1196
|
+
(8, 3): SiteTemperatureSensorViewSet,
|
|
1197
|
+
(8, 4): SiteWaterVaporRadiometerViewSet,
|
|
1198
|
+
(8, 5): SiteOtherInstrumentationViewSet,
|
|
1199
|
+
(9, 1): SiteRadioInterferencesViewSet,
|
|
1200
|
+
(9, 2): SiteMultiPathSourcesViewSet,
|
|
1201
|
+
(9, 3): SiteSignalObstructionsViewSet,
|
|
1202
|
+
10: SiteLocalEpisodicEffectsViewSet,
|
|
1203
|
+
11: SiteOperationalContactViewSet,
|
|
1204
|
+
12: SiteResponsibleAgencyViewSet,
|
|
1205
|
+
13: SiteMoreInformationViewSet,
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
@staticmethod
|
|
1209
|
+
def get_subsection_id(section):
|
|
1210
|
+
if section.section_number in {3, 4, 5, 6, 7, 8, 9, 10}:
|
|
1211
|
+
return section.ordering_id
|
|
1212
|
+
return None
|
|
1213
|
+
|
|
1214
|
+
class FileFilter(CrispyFormCompat, AcceptListArguments, FilterSet):
|
|
1215
|
+
name = django_filters.CharFilter(field_name="name", lookup_expr="istartswith")
|
|
1216
|
+
|
|
1217
|
+
file_type = django_filters.MultipleChoiceFilter(choices=SLMFileType.choices)
|
|
1218
|
+
|
|
1219
|
+
log_format = django_filters.MultipleChoiceFilter(choices=SiteLogFormat.choices)
|
|
1220
|
+
|
|
1221
|
+
status = django_filters.MultipleChoiceFilter(
|
|
1222
|
+
choices=SiteFileUploadStatus.choices
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
class Meta:
|
|
1226
|
+
model = SiteFileUpload
|
|
1227
|
+
fields = ("name", "status", "file_type", "log_format")
|
|
1228
|
+
|
|
1229
|
+
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
|
1230
|
+
filterset_class = FileFilter
|
|
1231
|
+
ordering_fields = ["-timestamp", "name"]
|
|
1232
|
+
|
|
1233
|
+
original_request = None
|
|
1234
|
+
|
|
1235
|
+
def dispatch(self, request, *args, **kwargs):
|
|
1236
|
+
self.site = kwargs.pop("site", None)
|
|
1237
|
+
self.original_request = request
|
|
1238
|
+
try:
|
|
1239
|
+
self.site = Site.objects.get(name__iexact=self.site)
|
|
1240
|
+
except Site.DoesNotExist:
|
|
1241
|
+
return HttpResponseNotFound(f"{self.site} does not exist.")
|
|
1242
|
+
|
|
1243
|
+
if not self.site.can_edit(request.user):
|
|
1244
|
+
return HttpResponseForbidden(
|
|
1245
|
+
f"{request.user} cannot edit site {self.site.name}"
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
return super().dispatch(request, *args, **kwargs)
|
|
1249
|
+
|
|
1250
|
+
def get_queryset(self):
|
|
1251
|
+
return SiteFileUpload.objects.available_to(self.request.user).filter(
|
|
1252
|
+
site=self.site
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
def perform_destroy(self, instance):
|
|
1256
|
+
super().perform_destroy(instance)
|
|
1257
|
+
if instance.file_type is not SLMFileType.SITE_LOG:
|
|
1258
|
+
slm_signals.site_file_deleted.send(
|
|
1259
|
+
sender=self,
|
|
1260
|
+
site=self.site,
|
|
1261
|
+
user=self.original_request.user,
|
|
1262
|
+
timestamp=now(),
|
|
1263
|
+
request=self.original_request,
|
|
1264
|
+
upload=instance,
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
def create(self, request, *args, **kwargs):
|
|
1268
|
+
with transaction.atomic():
|
|
1269
|
+
if "file" not in request.FILES:
|
|
1270
|
+
return Response("Expected a file upload with name 'file'.", status=400)
|
|
1271
|
+
|
|
1272
|
+
upload = SiteFileUpload(
|
|
1273
|
+
site=self.site,
|
|
1274
|
+
file=request.FILES["file"],
|
|
1275
|
+
name=request.FILES["file"].name[:255],
|
|
1276
|
+
mimetype=request.FILES["file"].content_type,
|
|
1277
|
+
user=request.user,
|
|
1278
|
+
)
|
|
1279
|
+
upload.save()
|
|
1280
|
+
slm_signals.site_file_uploaded.send(
|
|
1281
|
+
sender=self,
|
|
1282
|
+
site=self.site,
|
|
1283
|
+
user=request.user,
|
|
1284
|
+
timestamp=upload.timestamp,
|
|
1285
|
+
request=request,
|
|
1286
|
+
upload=upload,
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
if upload.file_type is SLMFileType.SITE_LOG:
|
|
1290
|
+
from slm.parsing.legacy import SiteLogBinder, SiteLogParser
|
|
1291
|
+
|
|
1292
|
+
if upload.log_format in [
|
|
1293
|
+
SiteLogFormat.LEGACY,
|
|
1294
|
+
SiteLogFormat.ASCII_9CHAR,
|
|
1295
|
+
]:
|
|
1296
|
+
with upload.file.open() as uplf:
|
|
1297
|
+
content = uplf.read()
|
|
1298
|
+
encoding = detect(content).get("encoding", "utf-8")
|
|
1299
|
+
try:
|
|
1300
|
+
bound_log = SiteLogBinder(
|
|
1301
|
+
SiteLogParser(
|
|
1302
|
+
content.decode(encoding), site_name=self.site.name
|
|
1303
|
+
)
|
|
1304
|
+
).parsed
|
|
1305
|
+
except (UnicodeDecodeError, LookupError):
|
|
1306
|
+
upload.status = SiteFileUploadStatus.INVALID
|
|
1307
|
+
upload.save()
|
|
1308
|
+
return Response(
|
|
1309
|
+
{
|
|
1310
|
+
"file": upload.id,
|
|
1311
|
+
"error": _(
|
|
1312
|
+
"Unable to decode this text file - please "
|
|
1313
|
+
"ensure the file is encoded in UTF-8 and "
|
|
1314
|
+
"try again."
|
|
1315
|
+
),
|
|
1316
|
+
},
|
|
1317
|
+
status=400,
|
|
1318
|
+
)
|
|
1319
|
+
if not bound_log.errors:
|
|
1320
|
+
self.update_from_legacy(request, bound_log)
|
|
1321
|
+
|
|
1322
|
+
upload.context = bound_log.context
|
|
1323
|
+
if bound_log.errors:
|
|
1324
|
+
upload.status = SiteFileUploadStatus.INVALID
|
|
1325
|
+
upload.save()
|
|
1326
|
+
return Response(
|
|
1327
|
+
{
|
|
1328
|
+
"file": upload.id,
|
|
1329
|
+
"error": _(
|
|
1330
|
+
"There were errors parsing the site " "log."
|
|
1331
|
+
),
|
|
1332
|
+
},
|
|
1333
|
+
status=400,
|
|
1334
|
+
)
|
|
1335
|
+
upload.status = (
|
|
1336
|
+
SiteFileUploadStatus.WARNINGS
|
|
1337
|
+
if bound_log.warnings
|
|
1338
|
+
else SiteFileUploadStatus.VALID
|
|
1339
|
+
)
|
|
1340
|
+
upload.save()
|
|
1341
|
+
|
|
1342
|
+
elif upload.log_format is SiteLogFormat.GEODESY_ML:
|
|
1343
|
+
from slm.parsing.xsd import SiteLogBinder, SiteLogParser
|
|
1344
|
+
|
|
1345
|
+
with upload.file.open() as uplf:
|
|
1346
|
+
content = uplf.read()
|
|
1347
|
+
encoding = detect(content).get("encoding", "utf-8")
|
|
1348
|
+
try:
|
|
1349
|
+
parsed = SiteLogParser(
|
|
1350
|
+
content.decode(encoding), site_name=self.site.name
|
|
1351
|
+
)
|
|
1352
|
+
except (UnicodeDecodeError, LookupError):
|
|
1353
|
+
upload.status = SiteFileUploadStatus.INVALID
|
|
1354
|
+
upload.save()
|
|
1355
|
+
return Response(
|
|
1356
|
+
{
|
|
1357
|
+
"file": upload.id,
|
|
1358
|
+
"error": _(
|
|
1359
|
+
"Unable to decode this xml file - please "
|
|
1360
|
+
"ensure the file is encoded in UTF-8 and "
|
|
1361
|
+
"try again."
|
|
1362
|
+
),
|
|
1363
|
+
},
|
|
1364
|
+
status=400,
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
upload.context = parsed.context
|
|
1368
|
+
if parsed.errors:
|
|
1369
|
+
upload.status = SiteFileUploadStatus.INVALID
|
|
1370
|
+
upload.save()
|
|
1371
|
+
return Response(
|
|
1372
|
+
{
|
|
1373
|
+
"file": upload.id,
|
|
1374
|
+
"error": _(
|
|
1375
|
+
"There were errors parsing the site " "log."
|
|
1376
|
+
),
|
|
1377
|
+
},
|
|
1378
|
+
status=400,
|
|
1379
|
+
)
|
|
1380
|
+
upload.save()
|
|
1381
|
+
return Response(
|
|
1382
|
+
"GeodesyML uploads are not yet supported.", status=400
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
elif upload.log_format is SiteLogFormat.JSON:
|
|
1386
|
+
return Response("JSON uploads are not yet supported.", status=400)
|
|
1387
|
+
else:
|
|
1388
|
+
return Response("Unsupported site log upload format.", status=400)
|
|
1389
|
+
elif upload.file_type is SLMFileType.SITE_IMAGE:
|
|
1390
|
+
# automagically set the view direction if its specified on
|
|
1391
|
+
# the filename
|
|
1392
|
+
if upload.direction is None:
|
|
1393
|
+
lower_name = upload.name.lower()
|
|
1394
|
+
if "north" in lower_name:
|
|
1395
|
+
upload.direction = CardinalDirection.NORTH
|
|
1396
|
+
if "east" in lower_name:
|
|
1397
|
+
upload.direction = CardinalDirection.NORTH_EAST
|
|
1398
|
+
elif "west" in lower_name:
|
|
1399
|
+
upload.direction = CardinalDirection.NORTH_WEST
|
|
1400
|
+
upload.save()
|
|
1401
|
+
elif "south" in lower_name:
|
|
1402
|
+
upload.direction = CardinalDirection.SOUTH
|
|
1403
|
+
if "east" in lower_name:
|
|
1404
|
+
upload.direction = CardinalDirection.SOUTH_EAST
|
|
1405
|
+
elif "west" in lower_name:
|
|
1406
|
+
upload.direction = CardinalDirection.SOUTH_WEST
|
|
1407
|
+
upload.save()
|
|
1408
|
+
elif "east" in lower_name:
|
|
1409
|
+
upload.direction = CardinalDirection.EAST
|
|
1410
|
+
upload.save()
|
|
1411
|
+
elif "west" in lower_name:
|
|
1412
|
+
upload.direction = CardinalDirection.WEST
|
|
1413
|
+
upload.save()
|
|
1414
|
+
|
|
1415
|
+
upload.site.synchronize()
|
|
1416
|
+
return Response(self.get_serializer(instance=upload).data, status=200)
|
|
1417
|
+
|
|
1418
|
+
def perform_update(self, serializer):
|
|
1419
|
+
# do permissions check on publish/unpublish action - also only allow
|
|
1420
|
+
# a subset of status changes
|
|
1421
|
+
if (
|
|
1422
|
+
serializer.validated_data.get("status", None) is not None
|
|
1423
|
+
and serializer.validated_data["status"] != serializer.instance.status
|
|
1424
|
+
and not self.site.is_moderator(self.request.user)
|
|
1425
|
+
):
|
|
1426
|
+
raise PermissionDenied("Must be a moderator to publish site files.")
|
|
1427
|
+
if serializer.validated_data.get("status", None) and (
|
|
1428
|
+
SiteFileUploadStatus(serializer.validated_data["status"])
|
|
1429
|
+
not in {SiteFileUploadStatus.PUBLISHED, SiteFileUploadStatus.UNPUBLISHED}
|
|
1430
|
+
):
|
|
1431
|
+
raise PermissionDenied("Files may only be published or unpublished.")
|
|
1432
|
+
status = serializer.instance.status
|
|
1433
|
+
super().perform_update(serializer)
|
|
1434
|
+
|
|
1435
|
+
if (status, serializer.instance.status) == (
|
|
1436
|
+
SiteFileUploadStatus.UNPUBLISHED,
|
|
1437
|
+
SiteFileUploadStatus.PUBLISHED,
|
|
1438
|
+
):
|
|
1439
|
+
slm_signals.site_file_published.send(
|
|
1440
|
+
sender=self,
|
|
1441
|
+
site=self.site,
|
|
1442
|
+
user=self.original_request.user,
|
|
1443
|
+
timestamp=now(),
|
|
1444
|
+
request=self.original_request,
|
|
1445
|
+
upload=serializer.instance,
|
|
1446
|
+
)
|
|
1447
|
+
elif (status, serializer.instance.status) == (
|
|
1448
|
+
SiteFileUploadStatus.PUBLISHED,
|
|
1449
|
+
SiteFileUploadStatus.UNPUBLISHED,
|
|
1450
|
+
):
|
|
1451
|
+
slm_signals.site_file_unpublished.send(
|
|
1452
|
+
sender=self,
|
|
1453
|
+
site=self.site,
|
|
1454
|
+
user=self.original_request.user,
|
|
1455
|
+
timestamp=now(),
|
|
1456
|
+
request=self.original_request,
|
|
1457
|
+
upload=serializer.instance,
|
|
1458
|
+
)
|
|
1459
|
+
self.site.synchronize()
|
|
1460
|
+
|
|
1461
|
+
def update_from_legacy(self, request, parsed):
|
|
1462
|
+
errors = {}
|
|
1463
|
+
|
|
1464
|
+
existing_sections = {}
|
|
1465
|
+
posted_subsections = {
|
|
1466
|
+
3: set(),
|
|
1467
|
+
4: set(),
|
|
1468
|
+
5: set(),
|
|
1469
|
+
6: set(),
|
|
1470
|
+
7: set(),
|
|
1471
|
+
(8, 1): set(),
|
|
1472
|
+
(8, 2): set(),
|
|
1473
|
+
(8, 3): set(),
|
|
1474
|
+
(8, 4): set(),
|
|
1475
|
+
(8, 5): set(),
|
|
1476
|
+
(9, 1): set(),
|
|
1477
|
+
(9, 2): set(),
|
|
1478
|
+
(9, 3): set(),
|
|
1479
|
+
10: set(),
|
|
1480
|
+
}
|
|
1481
|
+
with transaction.atomic():
|
|
1482
|
+
# we reverse this so we process the form section last which will
|
|
1483
|
+
# ensure that the prepared by field will be set to what was given
|
|
1484
|
+
# in the upload log
|
|
1485
|
+
for index, section in reversed(parsed.sections.items()):
|
|
1486
|
+
if section.example or not section.contains_values:
|
|
1487
|
+
continue
|
|
1488
|
+
|
|
1489
|
+
section_view = self.SECTION_VIEWS.get(section.heading_index, None)
|
|
1490
|
+
if section_view:
|
|
1491
|
+
data = {**section.binding, "site": self.site.id}
|
|
1492
|
+
subsection_number = self.get_subsection_id(section)
|
|
1493
|
+
if subsection_number is not None:
|
|
1494
|
+
# we have to find the right subsection identifiers
|
|
1495
|
+
# (which don't necessarily equal the existing ids)
|
|
1496
|
+
if section.heading_index not in existing_sections:
|
|
1497
|
+
existing_sections[section.heading_index] = list(
|
|
1498
|
+
section_view.serializer_class.Meta.model.objects.filter(
|
|
1499
|
+
site=self.site, is_deleted=False
|
|
1500
|
+
)
|
|
1501
|
+
.head()
|
|
1502
|
+
.sort()
|
|
1503
|
+
)
|
|
1504
|
+
if (
|
|
1505
|
+
len(existing_sections[section.heading_index])
|
|
1506
|
+
> subsection_number - 1
|
|
1507
|
+
):
|
|
1508
|
+
instance = existing_sections[section.heading_index][
|
|
1509
|
+
subsection_number - 1
|
|
1510
|
+
]
|
|
1511
|
+
data["subsection"] = instance.subsection
|
|
1512
|
+
posted_subsections[section.heading_index].add(instance)
|
|
1513
|
+
|
|
1514
|
+
serializer = section_view.serializer_class(
|
|
1515
|
+
data=data, context={"request": request}
|
|
1516
|
+
)
|
|
1517
|
+
if not serializer.is_valid(raise_exception=False):
|
|
1518
|
+
errors[index] = {
|
|
1519
|
+
param: "\n".join([str(detail) for detail in details])
|
|
1520
|
+
for param, details in serializer.errors.items()
|
|
1521
|
+
}
|
|
1522
|
+
else:
|
|
1523
|
+
try:
|
|
1524
|
+
serializer.save()
|
|
1525
|
+
for param, flag in serializer.instance._flags.items():
|
|
1526
|
+
params = section.get_params(param)
|
|
1527
|
+
for parsed_param in params:
|
|
1528
|
+
for line_no in range(
|
|
1529
|
+
parsed_param.line_no, parsed_param.line_end + 1
|
|
1530
|
+
):
|
|
1531
|
+
parsed.add_finding(
|
|
1532
|
+
Warn(
|
|
1533
|
+
line_no,
|
|
1534
|
+
parsed,
|
|
1535
|
+
str(flag),
|
|
1536
|
+
section=section,
|
|
1537
|
+
)
|
|
1538
|
+
)
|
|
1539
|
+
except (DjangoValidationError, DRFValidationError) as dve:
|
|
1540
|
+
for param, error_list in getattr(
|
|
1541
|
+
dve, "message_dict", getattr(dve, "detail")
|
|
1542
|
+
).items():
|
|
1543
|
+
errors.setdefault(index, {})[param] = "\n".join(
|
|
1544
|
+
[str(msg) for msg in error_list]
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
if errors:
|
|
1548
|
+
# if any section fails - rollback all sections
|
|
1549
|
+
transaction.set_rollback(True)
|
|
1550
|
+
for section_index, section_errors in errors.items():
|
|
1551
|
+
for param, error in section_errors.items():
|
|
1552
|
+
section = parsed.sections[section_index]
|
|
1553
|
+
params = section.get_params(param)
|
|
1554
|
+
if params:
|
|
1555
|
+
for parsed_param in params:
|
|
1556
|
+
for line_no in range(
|
|
1557
|
+
parsed_param.line_no, parsed_param.line_end + 1
|
|
1558
|
+
):
|
|
1559
|
+
parsed.add_finding(
|
|
1560
|
+
Error(
|
|
1561
|
+
line_no, parsed, str(error), section=section
|
|
1562
|
+
)
|
|
1563
|
+
)
|
|
1564
|
+
else:
|
|
1565
|
+
parsed.add_finding(
|
|
1566
|
+
Error(
|
|
1567
|
+
section.line_no, parsed, str(error), section=section
|
|
1568
|
+
)
|
|
1569
|
+
)
|
|
1570
|
+
return errors
|
|
1571
|
+
|
|
1572
|
+
# delete any subsections that are current but not present in the
|
|
1573
|
+
# uploaded sitelog
|
|
1574
|
+
for heading_index, existing in existing_sections.items():
|
|
1575
|
+
section_view = self.SECTION_VIEWS.get(heading_index, None)
|
|
1576
|
+
if section_view:
|
|
1577
|
+
for instance in set(existing).difference(
|
|
1578
|
+
posted_subsections.get(heading_index, set())
|
|
1579
|
+
):
|
|
1580
|
+
view = section_view()
|
|
1581
|
+
view.request = request
|
|
1582
|
+
try:
|
|
1583
|
+
view.perform_destroy(instance)
|
|
1584
|
+
except Exception:
|
|
1585
|
+
# catch everything here - if this does not happen
|
|
1586
|
+
# its not the end of the world - the exception log
|
|
1587
|
+
# will notify the relevant parties that there's some
|
|
1588
|
+
# kind of issue
|
|
1589
|
+
self.logger.exception(
|
|
1590
|
+
"Error deleting subsection %d of section %s",
|
|
1591
|
+
instance.subsection,
|
|
1592
|
+
heading_index,
|
|
1593
|
+
)
|
|
1594
|
+
return errors
|
|
1595
|
+
|
|
1596
|
+
def retrieve(self, request, *args, **kwargs):
|
|
1597
|
+
"""
|
|
1598
|
+
By default the edit api GET will return a json structure with
|
|
1599
|
+
information about the file. Adding ?download to the url will download
|
|
1600
|
+
the file itself.
|
|
1601
|
+
"""
|
|
1602
|
+
if request.GET.get("download", None) is None:
|
|
1603
|
+
return super().retrieve(request, *args, **kwargs)
|
|
1604
|
+
|
|
1605
|
+
file = self.get_object()
|
|
1606
|
+
if request.GET.get("thumbnail", None):
|
|
1607
|
+
file = file.thumbnail
|
|
1608
|
+
else:
|
|
1609
|
+
file = file.file
|
|
1610
|
+
return FileResponse(
|
|
1611
|
+
file.open("rb"),
|
|
1612
|
+
filename=file.name,
|
|
1613
|
+
# note this might not match the name on disk
|
|
1614
|
+
as_attachment=True,
|
|
1615
|
+
)
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
class ImageOperationsViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
1619
|
+
permission_classes = (IsAuthenticated, CanEditSite)
|
|
1620
|
+
|
|
1621
|
+
def get_queryset(self):
|
|
1622
|
+
return SiteFileUpload.objects.filter(file_type=SLMFileType.SITE_IMAGE)
|
|
1623
|
+
|
|
1624
|
+
def retrieve(self, request, *args, **kwargs):
|
|
1625
|
+
rotate = request.GET.get("rotate", None)
|
|
1626
|
+
try:
|
|
1627
|
+
if rotate:
|
|
1628
|
+
file = self.get_object()
|
|
1629
|
+
file.rotate(int(rotate))
|
|
1630
|
+
except ValueError:
|
|
1631
|
+
return Response({"rotate": "rotate must be an integer"}, status=400)
|
|
1632
|
+
return Response(status=204)
|