igs-slm 0.1.2b0__py3-none-any.whl → 0.1.5b0__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.5b0.dist-info/METADATA +115 -0
- {igs_slm-0.1.2b0.dist-info → igs_slm-0.1.5b0.dist-info}/RECORD +192 -172
- {igs_slm-0.1.2b0.dist-info → igs_slm-0.1.5b0.dist-info}/WHEEL +1 -1
- igs_slm-0.1.5b0.dist-info/entry_points.txt +3 -0
- {igs_slm-0.1.2b0.dist-info → igs_slm-0.1.5b0.dist-info/licenses}/LICENSE +1 -1
- slm/__init__.py +17 -14
- slm/admin.py +32 -5
- slm/api/edit/views.py +22 -9
- slm/api/public/views.py +10 -8
- slm/api/views.py +45 -6
- slm/apps.py +28 -6
- slm/authentication.py +3 -2
- slm/bin/startproject.py +102 -31
- slm/bin/templates/{{ project_dir }}/pyproject.toml +30 -21
- slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/base.py +12 -1
- slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/develop/__init__.py +5 -27
- slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/production/__init__.py +6 -27
- slm/bin/templates/{{ project_dir }}/src/sites/{{ site }}/validation.py +29 -0
- slm/context.py +5 -0
- slm/defines/AlertLevel.py +10 -2
- slm/defines/AntennaCalibration.py +6 -4
- slm/defines/AntennaFeatures.py +12 -9
- slm/defines/AntennaReferencePoint.py +12 -10
- slm/defines/Aspiration.py +4 -2
- slm/defines/CardinalDirection.py +6 -4
- slm/defines/CollocationStatus.py +3 -1
- slm/defines/EquipmentState.py +6 -12
- slm/defines/FlagSeverity.py +4 -2
- slm/defines/FractureSpacing.py +7 -5
- slm/defines/FrequencyStandardType.py +6 -4
- slm/defines/GeodesyMLVersion.py +2 -0
- slm/defines/Instrumentation.py +9 -7
- slm/defines/LogEntryType.py +17 -15
- slm/defines/SLMFileType.py +4 -2
- slm/defines/SiteFileUploadStatus.py +7 -24
- slm/defines/SiteLogFormat.py +8 -32
- slm/defines/SiteLogStatus.py +9 -28
- slm/defines/TectonicPlates.py +18 -16
- slm/manage.py +24 -0
- slm/management/commands/check_upgrade.py +142 -0
- slm/management/commands/generate_sinex.py +110 -92
- slm/management/commands/head_from_index.py +11 -8
- slm/management/commands/import_archive.py +27 -18
- slm/management/commands/import_equipment.py +1 -1
- slm/management/commands/sitelog.py +1 -3
- slm/management/commands/synchronize.py +1 -1
- slm/management/commands/validate_db.py +6 -4
- slm/map/defines.py +18 -14
- slm/map/templates/slm/map.html +4 -6
- slm/migrations/0001_remove_archiveindex_no_overlapping_ranges_per_site_and_more.py +26 -0
- slm/migrations/0002_alter_archivedsitelog_file_and_more.py +44 -0
- slm/migrations/0003_alter_archivedsitelog_name_and_more.py +35 -0
- slm/migrations/0004_alter_site_name.py +24 -0
- slm/migrations/0005_slmversion.py +30 -0
- slm/migrations/0017_alter_logentry_unique_together_and_more.py +3 -1
- slm/migrations/0018_afix_deleted.py +3 -1
- slm/migrations/0018_alter_siteantenna_options_and_more.py +87 -56
- slm/migrations/0019_remove_siteantenna_marker_enu_siteantenna_marker_une_and_more.py +1 -1
- slm/migrations/0023_archivedsitelog_gml_version_and_more.py +1 -1
- slm/migrations/0031_alter_antenna_features.py +44 -0
- slm/migrations/0032_archiveindex_valid_range_and_more.py +84 -0
- slm/migrations/add_index_order_index.py +54 -0
- slm/migrations/normalize_index.py +147 -0
- slm/migrations/simplify_index.py +48 -0
- slm/migrations/verify_index.py +67 -0
- slm/models/__init__.py +2 -0
- slm/models/alerts.py +7 -10
- slm/models/data.py +1 -2
- slm/models/equipment.py +1 -1
- slm/models/fields.py +41 -0
- slm/models/index.py +183 -53
- slm/models/sitelog.py +35 -38
- slm/models/system.py +72 -31
- slm/models/user.py +1 -1
- slm/parsing/__init__.py +34 -17
- slm/parsing/legacy/binding.py +65 -34
- slm/parsing/legacy/parser.py +2 -2
- slm/parsing/xsd/binding.py +1 -1
- slm/parsing/xsd/parser.py +1 -2
- slm/receivers/__init__.py +2 -2
- slm/receivers/index.py +2 -1
- slm/receivers/migration.py +21 -0
- slm/settings/__init__.py +192 -4
- slm/settings/assets.py +26 -0
- slm/settings/auth.py +18 -14
- slm/settings/ckeditor.py +12 -6
- slm/settings/debug.py +2 -2
- slm/settings/emails.py +50 -0
- slm/settings/internationalization.py +8 -6
- slm/settings/logging.py +100 -88
- slm/settings/platform/darwin.py +16 -6
- slm/settings/rest.py +20 -15
- slm/settings/root.py +192 -98
- slm/settings/routines.py +5 -1
- slm/settings/secrets.py +20 -31
- slm/settings/security.py +7 -5
- slm/settings/slm.py +35 -16
- slm/settings/static_templates.py +12 -9
- slm/settings/templates.py +31 -25
- slm/settings/uploads.py +33 -5
- slm/settings/urls.py +1 -1
- slm/settings/validation.py +165 -165
- slm/signals.py +3 -2
- slm/static/slm/css/style.css +37 -36
- slm/static/slm/js/autocomplete.js +6 -4
- slm/static/slm/js/file_modal.js +62 -0
- slm/static/slm/js/form.js +3 -3
- slm/static/slm/js/formWidget.js +3 -3
- slm/static/slm/js/persistable.js +5 -1
- slm/templates/admin/base.html +1 -0
- slm/templates/rest_framework/base.html +23 -11
- slm/templates/slm/base.html +27 -22
- slm/templates/slm/forms/widgets/auto_complete.html +12 -11
- slm/templates/slm/forms/widgets/auto_complete_multiple.html +8 -7
- slm/templates/slm/station/download.html +6 -6
- slm/templates/slm/station/edit.html +9 -17
- slm/templates/slm/station/review.html +5 -3
- slm/templates/slm/station/upload.html +4 -1
- slm/templates/slm/station/uploads/legacy.html +1 -1
- slm/templates/slm/widgets/alert_scroll.html +4 -8
- slm/templates/slm/widgets/filelist.html +0 -5
- slm/templates/slm/widgets/log_scroll.html +2 -13
- slm/templates/slm/widgets/stationlist.html +2 -8
- slm/templatetags/slm.py +70 -9
- slm/utils.py +13 -4
- slm/validators.py +14 -14
- slm/views.py +6 -6
- slm/wsgi.py +16 -0
- igs_slm-0.1.2b0.dist-info/METADATA +0 -151
- igs_slm-0.1.2b0.dist-info/entry_points.txt +0 -3
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/validation.py +0 -11
- /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/__init__.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/__init__.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/develop/local.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/develop/wsgi.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/manage.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/production/wsgi.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/urls.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/__init__.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/admin.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/apps.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/__init__.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/commands/__init__.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/commands/import_archive.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/migrations/__init__.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/models.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/templates/slm/base.html +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/urls.py +0 -0
- /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/views.py +0 -0
slm/__init__.py
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
r"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
|
6
|
-
| |
|
|
7
|
-
| |
|
|
8
|
-
| |
|
|
9
|
-
| |
|
|
10
|
-
| |
|
|
11
|
-
| '
|
|
12
|
-
|
|
2
|
+
::
|
|
3
|
+
|
|
4
|
+
.----------------. .----------------. .----------------.
|
|
5
|
+
| .--------------. || .--------------. || .--------------. |
|
|
6
|
+
| | _______ | || | _____ | || | ____ ____ | |
|
|
7
|
+
| | / ___ | | || | |_ _| | || ||_ \ / _|| |
|
|
8
|
+
| | | (__ \_| | || | | | | || | | \/ | | |
|
|
9
|
+
| | '.___`-. | || | | | _ | || | | |\ /| | | |
|
|
10
|
+
| | |`\____) | | || | _| |__/ | | || | _| |_\/_| |_ | |
|
|
11
|
+
| | |_______.' | || | |________| | || ||_____||_____|| |
|
|
12
|
+
| | | || | | || | | |
|
|
13
|
+
| '--------------' || '--------------' || '--------------' |
|
|
14
|
+
'----------------' '----------------' '----------------'
|
|
15
|
+
|
|
13
16
|
"""
|
|
14
17
|
|
|
15
|
-
VERSION = (0, 1, "
|
|
18
|
+
VERSION = (0, 1, "5b0")
|
|
16
19
|
|
|
17
20
|
__title__ = "IGS/Site Log Manager"
|
|
18
21
|
__version__ = ".".join(str(i) for i in VERSION)
|
|
19
|
-
__author__ =
|
|
22
|
+
__author__ = "Ashley Santiago, Brian Kohan, Rachel Pham, Robert Khachikyan"
|
|
20
23
|
__license__ = "MIT"
|
|
21
|
-
__copyright__ = "Copyright 2022-
|
|
24
|
+
__copyright__ = "Copyright 2022-2025 International GNSS Service"
|
slm/admin.py
CHANGED
|
@@ -27,6 +27,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|
|
27
27
|
from django.contrib.auth.models import Group
|
|
28
28
|
from django.db.models import BooleanField, Count, Q
|
|
29
29
|
from django.db.models.expressions import ExpressionWrapper
|
|
30
|
+
from django.utils.html import format_html
|
|
30
31
|
from django.utils.safestring import mark_safe
|
|
31
32
|
from django.utils.translation import gettext_lazy as _
|
|
32
33
|
from polymorphic.admin import (
|
|
@@ -281,6 +282,11 @@ class NetworkAdmin(admin.ModelAdmin):
|
|
|
281
282
|
ordering = ("name",)
|
|
282
283
|
list_filter = ("public",)
|
|
283
284
|
|
|
285
|
+
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
|
286
|
+
if db_field.name == "sites":
|
|
287
|
+
kwargs["queryset"] = Site.objects.order_by("name")
|
|
288
|
+
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
|
289
|
+
|
|
284
290
|
|
|
285
291
|
class AgencyUserInline(admin.TabularInline):
|
|
286
292
|
model = Agency.users.through
|
|
@@ -459,29 +465,50 @@ class ArchiveFileInline(admin.TabularInline):
|
|
|
459
465
|
field.name if field.name != "file" else "path"
|
|
460
466
|
for field in ArchivedSiteLog._meta.get_fields()
|
|
461
467
|
if field.name not in ["id", "site", "thumbnail", "name", "timestamp"]
|
|
462
|
-
]
|
|
468
|
+
] + ["view_file"]
|
|
463
469
|
can_delete = False
|
|
464
470
|
exclude = ["site", "file", "thumbnail", "name", "timestamp"]
|
|
465
471
|
|
|
466
472
|
def path(self, obj):
|
|
467
|
-
return obj.file.
|
|
473
|
+
return format_html('<a href="{}" download>{}</a>', obj.link, obj.file.name)
|
|
468
474
|
|
|
469
475
|
def has_add_permission(self, request, obj):
|
|
470
476
|
return False
|
|
471
477
|
|
|
478
|
+
def view_file(self, obj):
|
|
479
|
+
if obj.pk:
|
|
480
|
+
return format_html(
|
|
481
|
+
'<button type="button" class="button view-file" data-url="{}">{}</button>',
|
|
482
|
+
obj.link,
|
|
483
|
+
_("View"),
|
|
484
|
+
)
|
|
485
|
+
return ""
|
|
486
|
+
|
|
487
|
+
view_file.short_description = _("View")
|
|
488
|
+
|
|
472
489
|
|
|
473
490
|
class ArchiveIndexAdmin(admin.ModelAdmin):
|
|
474
491
|
search_fields = ["site__name"]
|
|
475
492
|
list_display = ("site", "begin", "end")
|
|
476
493
|
|
|
477
|
-
ordering = ("-begin",)
|
|
478
|
-
|
|
479
494
|
inlines = [ArchiveFileInline]
|
|
480
|
-
readonly_fields = ["site"
|
|
495
|
+
readonly_fields = ["site"]
|
|
496
|
+
|
|
497
|
+
def begin(self, obj):
|
|
498
|
+
return obj.begin
|
|
499
|
+
|
|
500
|
+
def end(self, obj):
|
|
501
|
+
return obj.end
|
|
481
502
|
|
|
482
503
|
def get_queryset(self, request):
|
|
483
504
|
return self.model.objects.select_related("site").prefetch_related("files")
|
|
484
505
|
|
|
506
|
+
class Media:
|
|
507
|
+
js = (
|
|
508
|
+
"admin/js/jquery.init.js",
|
|
509
|
+
"slm/js/file_modal.js",
|
|
510
|
+
)
|
|
511
|
+
|
|
485
512
|
|
|
486
513
|
"""
|
|
487
514
|
class SLMAdminSite(admin.AdminSite):
|
slm/api/edit/views.py
CHANGED
|
@@ -726,11 +726,12 @@ class SectionViewSet(type):
|
|
|
726
726
|
# also needs to be DRYed w/ published_diff function
|
|
727
727
|
for field in ModelClass.site_log_fields():
|
|
728
728
|
if field in validated_data:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
models.ManyToManyField,
|
|
732
|
-
)
|
|
729
|
+
mdl_field = instance._meta.get_field(field)
|
|
730
|
+
is_many = isinstance(mdl_field, models.ManyToManyField)
|
|
733
731
|
new_value = validated_data.get(field)
|
|
732
|
+
if new_value is None and not mdl_field.null:
|
|
733
|
+
# convert Nones to empty strings if warranted
|
|
734
|
+
new_value = mdl_field.default
|
|
734
735
|
old_value = getattr(instance, field)
|
|
735
736
|
if (
|
|
736
737
|
not is_many
|
|
@@ -1327,7 +1328,7 @@ class SiteFileUploadViewSet(
|
|
|
1327
1328
|
{
|
|
1328
1329
|
"file": upload.id,
|
|
1329
1330
|
"error": _(
|
|
1330
|
-
"There were errors parsing the site
|
|
1331
|
+
"There were errors parsing the site log."
|
|
1331
1332
|
),
|
|
1332
1333
|
},
|
|
1333
1334
|
status=400,
|
|
@@ -1372,7 +1373,7 @@ class SiteFileUploadViewSet(
|
|
|
1372
1373
|
{
|
|
1373
1374
|
"file": upload.id,
|
|
1374
1375
|
"error": _(
|
|
1375
|
-
"There were errors parsing the site
|
|
1376
|
+
"There were errors parsing the site log."
|
|
1376
1377
|
),
|
|
1377
1378
|
},
|
|
1378
1379
|
status=400,
|
|
@@ -1483,12 +1484,24 @@ class SiteFileUploadViewSet(
|
|
|
1483
1484
|
# ensure that the prepared by field will be set to what was given
|
|
1484
1485
|
# in the upload log
|
|
1485
1486
|
for index, section in reversed(parsed.sections.items()):
|
|
1486
|
-
if section.example
|
|
1487
|
+
if section.example:
|
|
1487
1488
|
continue
|
|
1488
|
-
|
|
1489
1489
|
section_view = self.SECTION_VIEWS.get(section.heading_index, None)
|
|
1490
|
+
# this is a complicated conditional to determine if the section is a subsection
|
|
1491
|
+
# header with no bindable values - think about encapsulating this logic onto
|
|
1492
|
+
# the binder
|
|
1493
|
+
if (
|
|
1494
|
+
section_view
|
|
1495
|
+
and issubclass(
|
|
1496
|
+
section_view.serializer_class.Meta.model, SiteSubSection
|
|
1497
|
+
)
|
|
1498
|
+
and not isinstance(section.order, int)
|
|
1499
|
+
and not section.contains_values
|
|
1500
|
+
):
|
|
1501
|
+
# skip subsection headers
|
|
1502
|
+
continue
|
|
1490
1503
|
if section_view:
|
|
1491
|
-
data = {**section.binding, "site": self.site.id}
|
|
1504
|
+
data = {**(section.binding or {}), "site": self.site.id}
|
|
1492
1505
|
subsection_number = self.get_subsection_id(section)
|
|
1493
1506
|
if subsection_number is not None:
|
|
1494
1507
|
# we have to find the right subsection identifiers
|
slm/api/public/views.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
3
|
|
|
4
4
|
import django_filters
|
|
5
5
|
from crispy_forms.helper import FormHelper
|
|
@@ -304,7 +304,7 @@ class SiteFileUploadViewSet(
|
|
|
304
304
|
|
|
305
305
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
|
306
306
|
filterset_class = FileFilter
|
|
307
|
-
ordering_fields =
|
|
307
|
+
ordering_fields = ("timestamp", "name", "site")
|
|
308
308
|
|
|
309
309
|
def get_queryset(self):
|
|
310
310
|
return SiteFileUpload.objects.public().select_related("site")
|
|
@@ -565,6 +565,8 @@ class ArchiveViewSet(
|
|
|
565
565
|
permission_classes = []
|
|
566
566
|
|
|
567
567
|
class ArchiveFilter(CrispyFormCompat, FilterSet):
|
|
568
|
+
NULL_EPOCH = datetime.max.replace(tzinfo=timezone.utc)
|
|
569
|
+
|
|
568
570
|
site = django_filters.CharFilter(
|
|
569
571
|
field_name="site__name",
|
|
570
572
|
help_text=_("The site of the archived log to download."),
|
|
@@ -577,17 +579,17 @@ class ArchiveViewSet(
|
|
|
577
579
|
|
|
578
580
|
epoch = SLMDateTimeFilter(
|
|
579
581
|
method="at_epoch",
|
|
580
|
-
initial=
|
|
582
|
+
initial=NULL_EPOCH,
|
|
581
583
|
help_text=_(
|
|
582
|
-
"Get the archive that was active at this given date or
|
|
584
|
+
"Get the archive that was active at this given date or datetime."
|
|
583
585
|
),
|
|
584
586
|
)
|
|
585
587
|
|
|
586
588
|
def at_epoch(self, queryset, name, value):
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
589
|
+
if value == self.NULL_EPOCH:
|
|
590
|
+
return queryset.order_by("-valid_range")[:1]
|
|
591
|
+
else:
|
|
592
|
+
return queryset.filter(valid_range__contains=value)
|
|
591
593
|
|
|
592
594
|
class Meta:
|
|
593
595
|
model = ArchivedSiteLog
|
slm/api/views.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
from datetime import datetime
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
2
|
|
|
3
|
-
from django.db.models import Q
|
|
4
3
|
from django.http import FileResponse
|
|
5
4
|
from django.utils.translation import gettext as _
|
|
6
5
|
from django_filters import filters
|
|
7
6
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
8
7
|
from rest_framework import mixins, renderers, viewsets
|
|
8
|
+
from rest_framework.generics import get_object_or_404
|
|
9
9
|
|
|
10
10
|
from slm.api.filter import InitialValueFilterSet, SLMDateTimeFilter
|
|
11
11
|
from slm.defines import SiteLogFormat
|
|
@@ -71,9 +71,11 @@ class BaseSiteLogDownloadViewSet(mixins.RetrieveModelMixin, viewsets.GenericView
|
|
|
71
71
|
queryset = ArchiveIndex.objects.all()
|
|
72
72
|
|
|
73
73
|
class ArchiveIndexFilter(InitialValueFilterSet):
|
|
74
|
+
NULL_EPOCH = datetime.max.replace(tzinfo=timezone.utc)
|
|
75
|
+
|
|
74
76
|
epoch = SLMDateTimeFilter(
|
|
75
77
|
method="at_epoch",
|
|
76
|
-
initial=
|
|
78
|
+
initial=NULL_EPOCH,
|
|
77
79
|
help_text=_("Get the log that was active at this given date or datetime."),
|
|
78
80
|
)
|
|
79
81
|
|
|
@@ -90,9 +92,17 @@ class BaseSiteLogDownloadViewSet(mixins.RetrieveModelMixin, viewsets.GenericView
|
|
|
90
92
|
)
|
|
91
93
|
|
|
92
94
|
def at_epoch(self, queryset, name, value):
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
if value == self.NULL_EPOCH:
|
|
96
|
+
# # this does not work because it pulls the most recent site log *first* - would need to
|
|
97
|
+
# # do this after the site log filter!
|
|
98
|
+
# last_index = Subquery(
|
|
99
|
+
# queryset.order_by(Func('valid_range', function='lower').desc())
|
|
100
|
+
# .values('valid_range')[:1]
|
|
101
|
+
# )
|
|
102
|
+
# return queryset.filter(valid_range=last_index)
|
|
103
|
+
return queryset.order_by("-valid_range")[:1]
|
|
104
|
+
else:
|
|
105
|
+
return queryset.filter(valid_range__contains=value)
|
|
96
106
|
|
|
97
107
|
def noop(self, queryset, _1, _2):
|
|
98
108
|
return queryset
|
|
@@ -104,6 +114,35 @@ class BaseSiteLogDownloadViewSet(mixins.RetrieveModelMixin, viewsets.GenericView
|
|
|
104
114
|
filter_backends = (DjangoFilterBackend,)
|
|
105
115
|
filterset_class = ArchiveIndexFilter
|
|
106
116
|
|
|
117
|
+
def get_object(self):
|
|
118
|
+
"""
|
|
119
|
+
We override get_object so we can apply the station lookup first to the queryset.
|
|
120
|
+
|
|
121
|
+
This is necessary because the at_epoch query uses a subquery.
|
|
122
|
+
"""
|
|
123
|
+
# Perform the lookup filtering.
|
|
124
|
+
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
|
125
|
+
|
|
126
|
+
assert lookup_url_kwarg in self.kwargs, (
|
|
127
|
+
"Expected view %s to be called with a URL keyword argument "
|
|
128
|
+
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
|
129
|
+
"attribute on the view correctly."
|
|
130
|
+
% (self.__class__.__name__, lookup_url_kwarg)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
obj = get_object_or_404(
|
|
134
|
+
self.filter_queryset(
|
|
135
|
+
self.get_queryset().filter(
|
|
136
|
+
**{self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# May raise a permission denied
|
|
142
|
+
self.check_object_permissions(self.request, obj)
|
|
143
|
+
|
|
144
|
+
return obj
|
|
145
|
+
|
|
107
146
|
def get_format_suffix(self, **kwargs):
|
|
108
147
|
requested_format = super().get_format_suffix(**kwargs)
|
|
109
148
|
|
slm/apps.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import shutil
|
|
1
2
|
import sys
|
|
2
3
|
from pprint import pformat
|
|
3
4
|
|
|
@@ -20,6 +21,30 @@ from slm.signals import signal_name
|
|
|
20
21
|
from slm.utils import clear_caches
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
@register("slm", Tags.staticfiles)
|
|
25
|
+
def check_node_available(**kwargs):
|
|
26
|
+
if (
|
|
27
|
+
any(
|
|
28
|
+
(
|
|
29
|
+
cmd.startswith("npx")
|
|
30
|
+
for _, cmd in getattr(settings, "COMPRESS_PRECOMPILERS", [])
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
and shutil.which("npx") is None
|
|
34
|
+
):
|
|
35
|
+
return [
|
|
36
|
+
Warning(
|
|
37
|
+
"nodejs is not available.",
|
|
38
|
+
hint=(
|
|
39
|
+
"Make sure nodejs is installed so esbuild can be used to bundle "
|
|
40
|
+
"static assets. (https://nodejs.org/en/download)"
|
|
41
|
+
),
|
|
42
|
+
id="slm.W002",
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
|
|
23
48
|
@register("slm", Tags.security)
|
|
24
49
|
def check_permission_groups_setting(**kwargs):
|
|
25
50
|
error_id = "slm.E002"
|
|
@@ -81,10 +106,7 @@ def check_permissions_setting(**kwargs):
|
|
|
81
106
|
except ImportError:
|
|
82
107
|
return [
|
|
83
108
|
Warning(
|
|
84
|
-
_(
|
|
85
|
-
f"Was unable to load SLM_PERMISSIONS callable: "
|
|
86
|
-
f"{permissions}"
|
|
87
|
-
),
|
|
109
|
+
_(f"Was unable to load SLM_PERMISSIONS callable: {permissions}"),
|
|
88
110
|
hint=_(
|
|
89
111
|
"Set SLM_PERMISSIONS to the import string for a "
|
|
90
112
|
"callable that returns a queryset of valid system"
|
|
@@ -158,8 +180,7 @@ def check_automated_alerts(**kwargs):
|
|
|
158
180
|
errors.append(
|
|
159
181
|
alert_conf_error(
|
|
160
182
|
_(
|
|
161
|
-
"SLM_AUTOMATED_ALERTS[{}][{}][{}] is not a "
|
|
162
|
-
"Signal"
|
|
183
|
+
"SLM_AUTOMATED_ALERTS[{}][{}][{}] is not a Signal"
|
|
163
184
|
).format(alert, signal_type, signal_name(signal)),
|
|
164
185
|
)
|
|
165
186
|
)
|
|
@@ -262,6 +283,7 @@ def site_save(sender, instance, created, raw, using, update_fields, **kwargs):
|
|
|
262
283
|
site=instance,
|
|
263
284
|
previous_status=instance._slm_pre_status,
|
|
264
285
|
new_status=instance.status,
|
|
286
|
+
reverted=getattr(instance, "_reverted", False),
|
|
265
287
|
)
|
|
266
288
|
|
|
267
289
|
|
slm/authentication.py
CHANGED
|
@@ -15,7 +15,9 @@ from allauth.account.utils import user_pk_to_url_str, user_username
|
|
|
15
15
|
from allauth.utils import build_absolute_uri
|
|
16
16
|
from django.conf import settings
|
|
17
17
|
from django.contrib.auth import get_user_model
|
|
18
|
+
from django.contrib.auth.models import Permission
|
|
18
19
|
from django.contrib.sites.shortcuts import get_current_site
|
|
20
|
+
from django.db.models import QuerySet
|
|
19
21
|
from django.urls import reverse
|
|
20
22
|
from django.utils.module_loading import import_string
|
|
21
23
|
from rest_framework import authentication, exceptions
|
|
@@ -187,9 +189,8 @@ def permissions():
|
|
|
187
189
|
return Permission.objects.all()
|
|
188
190
|
|
|
189
191
|
|
|
190
|
-
def default_permissions():
|
|
192
|
+
def default_permissions() -> QuerySet[Permission]:
|
|
191
193
|
from django.contrib.auth import get_user_model
|
|
192
|
-
from django.contrib.auth.models import Permission
|
|
193
194
|
from django.contrib.contenttypes.models import ContentType
|
|
194
195
|
|
|
195
196
|
return Permission.objects.filter(
|
slm/bin/startproject.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import shutil
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from urllib.parse import urlparse
|
|
6
7
|
|
|
7
8
|
import django
|
|
9
|
+
import environ
|
|
8
10
|
from django.conf import settings
|
|
9
11
|
from django_typer.completers import complete_path
|
|
12
|
+
from packaging.version import Version
|
|
10
13
|
from render_static.engine import StaticTemplateEngine
|
|
11
14
|
from rich.console import Console
|
|
12
15
|
from rich.markdown import Markdown
|
|
@@ -36,36 +39,59 @@ REPORT_MARKDOWN = """
|
|
|
36
39
|
|
|
37
40
|
### Installation Instructions
|
|
38
41
|
|
|
39
|
-
1. Create a database called `{
|
|
42
|
+
1. Create a database called `{database_name}`.
|
|
40
43
|
|
|
41
|
-
2. Use `
|
|
44
|
+
2. Use `uv` to install your project's virtual environment:
|
|
42
45
|
|
|
43
|
-
{
|
|
46
|
+
{install_uv}
|
|
44
47
|
|
|
45
48
|
Install dependencies:
|
|
46
49
|
|
|
47
50
|
```bash
|
|
48
|
-
|
|
51
|
+
uv sync --all-extras
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
3. To install your database and import existing sitelogs, run:
|
|
52
55
|
|
|
53
56
|
```bash
|
|
54
|
-
{site} routine install
|
|
57
|
+
uv run {site} routine install
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
4. To run the development server, run:
|
|
58
61
|
|
|
59
62
|
```bash
|
|
60
|
-
|
|
63
|
+
uv run {site} runserver
|
|
61
64
|
```
|
|
62
65
|
"""
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
**You do not have `
|
|
67
|
+
INSTALL_UV = """
|
|
68
|
+
**You do not have `uv` installed, see**: https://docs.astral.sh/uv/getting-started/installation/
|
|
66
69
|
"""
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
def sanitize_package_dir(name: str) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Convert an arbitrary string to a valid Python importable name.
|
|
75
|
+
|
|
76
|
+
Rules:
|
|
77
|
+
- Replace any non-alphanumeric character with underscore.
|
|
78
|
+
- Collapse multiple underscores.
|
|
79
|
+
- Remove leading/trailing underscores.
|
|
80
|
+
- Lowercase the result.
|
|
81
|
+
"""
|
|
82
|
+
return re.sub(r"_+", "_", re.sub(r"[^A-Za-z0-9]+", "_", name)).strip("_").lower()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def sanitize_package_name(name: str) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Replace illegal characters in a package name with underscores
|
|
88
|
+
according to PyPI naming rules (PEP 508).
|
|
89
|
+
"""
|
|
90
|
+
return re.sub(
|
|
91
|
+
r"_+", "_", re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip(".-")
|
|
92
|
+
).lower()
|
|
93
|
+
|
|
94
|
+
|
|
69
95
|
@app.command(
|
|
70
96
|
help="Create a directory structure for an slm deployment. This includes settings files and an extension app. You can change any names or settings at a later time!"
|
|
71
97
|
)
|
|
@@ -84,7 +110,7 @@ def main(
|
|
|
84
110
|
prompt="What is the name of your organization?",
|
|
85
111
|
help="What is the name of your organization?",
|
|
86
112
|
),
|
|
87
|
-
] = "",
|
|
113
|
+
] = "{domain}",
|
|
88
114
|
project_dir: Annotated[
|
|
89
115
|
str,
|
|
90
116
|
Option(
|
|
@@ -120,22 +146,29 @@ def main(
|
|
|
120
146
|
help="What should we call your custom Django app?",
|
|
121
147
|
),
|
|
122
148
|
] = "{org}_extensions",
|
|
149
|
+
database: Annotated[
|
|
150
|
+
str,
|
|
151
|
+
Option(
|
|
152
|
+
prompt="How should we connect to the database (db url)?",
|
|
153
|
+
help="How should we connect to the database (db url)?",
|
|
154
|
+
),
|
|
155
|
+
] = "postgis:///{site}",
|
|
123
156
|
include_map: Annotated[
|
|
124
157
|
bool,
|
|
125
158
|
Option(
|
|
126
|
-
"--include-map",
|
|
159
|
+
"--no-include-map",
|
|
127
160
|
prompt="Install the mapbox map app?",
|
|
128
161
|
help="Install the mapbox map app?",
|
|
129
162
|
),
|
|
130
|
-
] =
|
|
163
|
+
] = True,
|
|
131
164
|
use_igs_validation: Annotated[
|
|
132
165
|
bool,
|
|
133
166
|
Option(
|
|
134
|
-
"--use-igs-validation",
|
|
167
|
+
"--no-use-igs-validation",
|
|
135
168
|
prompt="Use IGS sitelog validation defaults?",
|
|
136
169
|
help="Use IGS sitelog validation defaults?",
|
|
137
170
|
),
|
|
138
|
-
] =
|
|
171
|
+
] = True,
|
|
139
172
|
):
|
|
140
173
|
netloc = (urlparse(netloc).netloc or netloc).lower()
|
|
141
174
|
parts = netloc.split(".")
|
|
@@ -186,13 +219,28 @@ def main(
|
|
|
186
219
|
]
|
|
187
220
|
}
|
|
188
221
|
)
|
|
189
|
-
if not organization:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
222
|
+
if not organization or organization == "{domain}":
|
|
223
|
+
parts = domain.split(".")
|
|
224
|
+
organization = parts[0]
|
|
225
|
+
if len(parts) > 1:
|
|
226
|
+
organization = parts[-2]
|
|
227
|
+
organization = organization.title()
|
|
228
|
+
org = organization.replace(" ", "_").replace(".", "_").lower()
|
|
229
|
+
extension_app = sanitize_package_dir(extension_app.format(org=(org or "slm")))
|
|
193
230
|
site = site.format(subdomain=subdomain).replace(" ", "_").lower()
|
|
194
231
|
project_dir = project_dir.format(subdomain=subdomain)
|
|
195
|
-
package_name = package_name.format(project_dir=project_dir)
|
|
232
|
+
package_name = sanitize_package_name(package_name.format(project_dir=project_dir))
|
|
233
|
+
database = database.format(site=site)
|
|
234
|
+
if database.isalpha():
|
|
235
|
+
database = f"postgis:///{database}"
|
|
236
|
+
|
|
237
|
+
env = environ.Env()
|
|
238
|
+
database_name = env.db_url_config(
|
|
239
|
+
database, engine="django.contrib.gis.db.backends.postgis"
|
|
240
|
+
)["NAME"]
|
|
241
|
+
|
|
242
|
+
if not database_name:
|
|
243
|
+
raise
|
|
196
244
|
|
|
197
245
|
# find site packages dir
|
|
198
246
|
local_slm = None
|
|
@@ -200,16 +248,33 @@ def main(
|
|
|
200
248
|
for pth in (Path(pth) for pth in sys.path):
|
|
201
249
|
if pth.name == "site-packages":
|
|
202
250
|
if pth not in slm_pth.parents:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
208
|
-
):
|
|
209
|
-
local_slm = os.path.relpath(
|
|
210
|
-
slm_pth.parent, directory.absolute().resolve() / project_dir
|
|
211
|
-
)
|
|
251
|
+
local_slm = os.path.relpath(
|
|
252
|
+
slm_pth.parent.parent,
|
|
253
|
+
directory.absolute().resolve() / project_dir,
|
|
254
|
+
)
|
|
212
255
|
break
|
|
256
|
+
else:
|
|
257
|
+
# uvx --from path edge case
|
|
258
|
+
import json
|
|
259
|
+
from importlib.metadata import version
|
|
260
|
+
|
|
261
|
+
direct_url = (
|
|
262
|
+
pth / f"igs_slm-{version('igs-slm')}.dist-info" / "direct_url.json"
|
|
263
|
+
)
|
|
264
|
+
if direct_url.is_file():
|
|
265
|
+
installed_from = json.loads(direct_url.read_text()).get("url", None)
|
|
266
|
+
if installed_from.startswith("file:///"):
|
|
267
|
+
local_slm = str(Path(installed_from[7:]).resolve())
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
if local_slm:
|
|
271
|
+
if not yes(
|
|
272
|
+
input(
|
|
273
|
+
"It looks like you are using a local clone of the SLM, would "
|
|
274
|
+
"you prefer to use this instead of a release on pypi? (Y/n): "
|
|
275
|
+
)
|
|
276
|
+
):
|
|
277
|
+
local_slm = None
|
|
213
278
|
|
|
214
279
|
ctx = {
|
|
215
280
|
"netloc": netloc,
|
|
@@ -218,8 +283,11 @@ def main(
|
|
|
218
283
|
"org": org,
|
|
219
284
|
"project_dir": project_dir,
|
|
220
285
|
"site": site,
|
|
286
|
+
"database": database,
|
|
287
|
+
"database_name": database_name,
|
|
221
288
|
"production_dir": production_dir.format(site=site),
|
|
222
289
|
"slm_version": slm.__version__,
|
|
290
|
+
"slm_version_next_major": f"{Version(slm.__version__).major + 1}.0",
|
|
223
291
|
"extension_app_class": extension_app.title().replace(" ", "").replace("_", ""),
|
|
224
292
|
"extension_app": extension_app.replace(" ", "_").lower(),
|
|
225
293
|
"include_map": include_map,
|
|
@@ -231,17 +299,19 @@ def main(
|
|
|
231
299
|
"{{ project_dir }}/**",
|
|
232
300
|
context=ctx,
|
|
233
301
|
dest=directory,
|
|
234
|
-
exclude=[template_dir / "{{ project_dir }}/{{ extension_app }}/templates"],
|
|
302
|
+
exclude=[template_dir / "{{ project_dir }}/src/{{ extension_app }}/templates"],
|
|
235
303
|
)
|
|
236
304
|
engine.render_to_disk(
|
|
237
|
-
"{{ project_dir }}/{{ extension_app }}/templates/**",
|
|
305
|
+
"{{ project_dir }}/src/{{ extension_app }}/templates/**",
|
|
238
306
|
context=ctx,
|
|
239
307
|
dest=directory,
|
|
240
308
|
render_contents=False,
|
|
241
309
|
)
|
|
242
310
|
|
|
243
311
|
output = (directory / project_dir).absolute().resolve()
|
|
244
|
-
assert output.is_dir() and output.exists()
|
|
312
|
+
assert output.is_dir() and output.exists(), (
|
|
313
|
+
f"Failed to create project directory: {output}"
|
|
314
|
+
)
|
|
245
315
|
|
|
246
316
|
Console().print(
|
|
247
317
|
Markdown(
|
|
@@ -250,11 +320,12 @@ def main(
|
|
|
250
320
|
develop=(output / "sites" / site / "develop/__init__.py").relative_to(
|
|
251
321
|
output
|
|
252
322
|
),
|
|
323
|
+
database_name=database_name,
|
|
253
324
|
production=(
|
|
254
325
|
output / "sites" / site / "production/__init__.py"
|
|
255
326
|
).relative_to(output),
|
|
256
327
|
site=site,
|
|
257
|
-
|
|
328
|
+
install_uv=INSTALL_UV if not shutil.which("uv") else "",
|
|
258
329
|
pyproject=(output / "pyproject.toml").relative_to(output),
|
|
259
330
|
)
|
|
260
331
|
)
|