igs-slm 0.1.4b0__py3-none-any.whl → 0.1.5b1__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.5b1.dist-info/METADATA +115 -0
- {igs_slm-0.1.4b0.dist-info → igs_slm-0.1.5b1.dist-info}/RECORD +193 -173
- {igs_slm-0.1.4b0.dist-info → igs_slm-0.1.5b1.dist-info}/WHEEL +1 -1
- igs_slm-0.1.5b1.dist-info/entry_points.txt +3 -0
- {igs_slm-0.1.4b0.dist-info → igs_slm-0.1.5b1.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 +9 -7
- 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 }}/urls.py +15 -0
- slm/bin/templates/{{ project_dir }}/src/sites/{{ site }}/validation.py +29 -0
- slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/urls.py +1 -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 +5 -7
- 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 +33 -16
- 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 -23
- slm/settings/static_templates.py +12 -9
- slm/settings/templates.py +31 -25
- slm/settings/uploads.py +33 -5
- slm/settings/urls.py +7 -12
- 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/enums.js +6 -5
- 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.4b0.dist-info/METADATA +0 -154
- igs_slm-0.1.4b0.dist-info/entry_points.txt +0 -3
- slm/bin/templates/{{ project_dir }}/sites/{{ site }}/urls.py +0 -7
- 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 }}/{{{ 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 }}}/views.py +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Generated by Django 4.2.20 on 2025-05-08 19:52
|
|
2
|
+
|
|
3
|
+
import django.contrib.postgres.constraints
|
|
4
|
+
import django.contrib.postgres.fields.ranges
|
|
5
|
+
from django.contrib.postgres.fields.ranges import DateTimeTZRange
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def populate_valid_range(apps, schema_editor):
|
|
10
|
+
ArchiveIndex = apps.get_model("slm", "ArchiveIndex")
|
|
11
|
+
|
|
12
|
+
for row in ArchiveIndex.objects.all():
|
|
13
|
+
ArchiveIndex.objects.filter(pk=row.pk).update(
|
|
14
|
+
valid_range=DateTimeTZRange(row.begin, row.end)
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def reverse_valid_range(apps, schema_editor):
|
|
19
|
+
ArchiveIndex = apps.get_model("slm", "ArchiveIndex")
|
|
20
|
+
|
|
21
|
+
for row in ArchiveIndex.objects.all():
|
|
22
|
+
begin = row.valid_range.lower if row.valid_range else None
|
|
23
|
+
end = row.valid_range.upper if row.valid_range else None
|
|
24
|
+
ArchiveIndex.objects.filter(pk=row.pk).update(begin=begin, end=end)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Migration(migrations.Migration):
|
|
28
|
+
dependencies = [
|
|
29
|
+
("slm", "0031_alter_antenna_features"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
operations = [
|
|
33
|
+
migrations.RunSQL("CREATE EXTENSION IF NOT EXISTS citext;"),
|
|
34
|
+
migrations.RunSQL("CREATE EXTENSION IF NOT EXISTS btree_gist;"),
|
|
35
|
+
migrations.AddField(
|
|
36
|
+
model_name="archiveindex",
|
|
37
|
+
name="valid_range",
|
|
38
|
+
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(
|
|
39
|
+
blank=True, db_index=True, null=True
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
migrations.AddIndex(
|
|
43
|
+
model_name="archiveindex",
|
|
44
|
+
index=models.Index(
|
|
45
|
+
fields=["site", "valid_range"], name="slm_archive_site_id_5a8c6e_idx"
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
migrations.AddConstraint(
|
|
49
|
+
model_name="archiveindex",
|
|
50
|
+
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
|
51
|
+
expressions=[("site", "="), ("valid_range", "&&")],
|
|
52
|
+
name="no_overlapping_ranges_per_site",
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
migrations.RunPython(populate_valid_range, reverse_code=reverse_valid_range),
|
|
56
|
+
migrations.AlterModelOptions(
|
|
57
|
+
name="archiveindex",
|
|
58
|
+
options={
|
|
59
|
+
"ordering": ("-valid_range",),
|
|
60
|
+
"verbose_name": "Archive Index",
|
|
61
|
+
"verbose_name_plural": "Archive Index",
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
migrations.RemoveIndex(
|
|
65
|
+
model_name="archiveindex",
|
|
66
|
+
name="slm_archive_begin_b89d03_idx",
|
|
67
|
+
),
|
|
68
|
+
migrations.RemoveIndex(
|
|
69
|
+
model_name="archiveindex",
|
|
70
|
+
name="slm_archive_site_id_c1b6a6_idx",
|
|
71
|
+
),
|
|
72
|
+
migrations.AlterUniqueTogether(
|
|
73
|
+
name="archiveindex",
|
|
74
|
+
unique_together=set(),
|
|
75
|
+
),
|
|
76
|
+
migrations.RemoveField(
|
|
77
|
+
model_name="archiveindex",
|
|
78
|
+
name="begin",
|
|
79
|
+
),
|
|
80
|
+
migrations.RemoveField(
|
|
81
|
+
model_name="archiveindex",
|
|
82
|
+
name="end",
|
|
83
|
+
),
|
|
84
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from django.db import migrations
|
|
2
|
+
|
|
3
|
+
# TODO when migrations are squashed - we need to reinclude this one
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
# Replace with your latest migration
|
|
9
|
+
("slm", "0032_archiveindex_valid_range_and_more"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RunSQL(
|
|
14
|
+
sql="""
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_archiveindex_lower_bound
|
|
16
|
+
ON slm_archiveindex (lower(valid_range));
|
|
17
|
+
""",
|
|
18
|
+
reverse_sql="""
|
|
19
|
+
DROP INDEX IF EXISTS idx_archiveindex_lower_bound;
|
|
20
|
+
""",
|
|
21
|
+
),
|
|
22
|
+
migrations.RunSQL(
|
|
23
|
+
sql="""
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_archiveindex_upper_bound
|
|
25
|
+
ON slm_archiveindex (upper(valid_range));
|
|
26
|
+
""",
|
|
27
|
+
reverse_sql="""
|
|
28
|
+
DROP INDEX IF EXISTS idx_archiveindex_upper_bound;
|
|
29
|
+
""",
|
|
30
|
+
),
|
|
31
|
+
# for idempotency - drop it first
|
|
32
|
+
migrations.RunSQL(
|
|
33
|
+
"""
|
|
34
|
+
ALTER TABLE slm_archiveindex
|
|
35
|
+
DROP CONSTRAINT IF EXISTS index_valid_range_lower_not_null;
|
|
36
|
+
""",
|
|
37
|
+
reverse_sql="""
|
|
38
|
+
ALTER TABLE slm_archiveindex
|
|
39
|
+
ADD CONSTRAINT index_valid_range_lower_not_null
|
|
40
|
+
CHECK (lower(valid_range) IS NOT NULL);
|
|
41
|
+
""",
|
|
42
|
+
),
|
|
43
|
+
migrations.RunSQL(
|
|
44
|
+
"""
|
|
45
|
+
ALTER TABLE slm_archiveindex
|
|
46
|
+
ADD CONSTRAINT index_valid_range_lower_not_null
|
|
47
|
+
CHECK (lower(valid_range) IS NOT NULL);
|
|
48
|
+
""",
|
|
49
|
+
reverse_sql="""
|
|
50
|
+
ALTER TABLE slm_archiveindex
|
|
51
|
+
DROP CONSTRAINT IF EXISTS index_valid_range_lower_not_null;
|
|
52
|
+
""",
|
|
53
|
+
),
|
|
54
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from django.db import migrations
|
|
5
|
+
from django.db.models import DateTimeField
|
|
6
|
+
from django.db.models.functions import Lower
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def site_upload_path(instance, filename):
|
|
10
|
+
"""
|
|
11
|
+
The historical site_upload_path implementation.
|
|
12
|
+
"""
|
|
13
|
+
return f"archive/{instance.site.name}/{filename}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize_index(apps, schema_editor):
|
|
17
|
+
from django.conf import settings
|
|
18
|
+
|
|
19
|
+
ArchivedSiteLog = apps.get_model("slm", "ArchivedSiteLog")
|
|
20
|
+
Site = apps.get_model("slm", "Site")
|
|
21
|
+
|
|
22
|
+
# paths to update - we do this after the database updates succeed, so if they're rolled
|
|
23
|
+
# back after an exception the disk state will still match the db
|
|
24
|
+
path_changes = []
|
|
25
|
+
unindexed = absolute = normalized = timestamps = 0
|
|
26
|
+
|
|
27
|
+
for archived in ArchivedSiteLog.objects.filter(file__startswith="/"):
|
|
28
|
+
current_name = Path(archived.file.name)
|
|
29
|
+
# change to relative path
|
|
30
|
+
archived.file.name = site_upload_path(archived, current_name.name)
|
|
31
|
+
assert not archived.file.name.startswith("/")
|
|
32
|
+
if not Path(archived.file.path).is_file():
|
|
33
|
+
if current_name.is_file():
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
f"Archived file {archived.file.name} does not exist at the new path: {Path(archived.file.path)}"
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
print(
|
|
39
|
+
f"WARNING: Archived file {archived.file.name} does not exist on disk!"
|
|
40
|
+
)
|
|
41
|
+
archived.name = Path(archived.file.name).name
|
|
42
|
+
archived.save(update_fields=["file", "name"])
|
|
43
|
+
absolute += 1
|
|
44
|
+
# print(f"Fixed indexed file {current_name} -> {archived.file.name}")
|
|
45
|
+
|
|
46
|
+
# remove unindexed files
|
|
47
|
+
for site in Site.objects.all():
|
|
48
|
+
site_dir = Path(settings.MEDIA_ROOT / "archive" / site.name)
|
|
49
|
+
if not site_dir.is_dir():
|
|
50
|
+
continue
|
|
51
|
+
files = {site_dir / file for file in os.listdir(site_dir)}
|
|
52
|
+
if ArchivedSiteLog.objects.filter(site=site).count() != len(files):
|
|
53
|
+
for archived in ArchivedSiteLog.objects.filter(site=site):
|
|
54
|
+
try:
|
|
55
|
+
files.remove(Path(archived.file.path))
|
|
56
|
+
except KeyError as err:
|
|
57
|
+
raise RuntimeError() from err # this should not happen
|
|
58
|
+
for file in files:
|
|
59
|
+
# print(f"WARNING: deleting unindexed {file} in archive!")
|
|
60
|
+
unindexed += 1
|
|
61
|
+
file.unlink()
|
|
62
|
+
|
|
63
|
+
names = set()
|
|
64
|
+
for archived in ArchivedSiteLog.objects.annotate(
|
|
65
|
+
begin=Lower("index__valid_range", output_field=DateTimeField())
|
|
66
|
+
).all():
|
|
67
|
+
current_path = Path(archived.file.path)
|
|
68
|
+
new_path = Path(archived.file.path)
|
|
69
|
+
update_fields = []
|
|
70
|
+
|
|
71
|
+
if archived.timestamp != archived.begin:
|
|
72
|
+
update_fields.append("timestamp")
|
|
73
|
+
archived.timestamp = archived.begin
|
|
74
|
+
timestamps += 1
|
|
75
|
+
|
|
76
|
+
# make lowercase if necessary
|
|
77
|
+
if not current_path.name.islower():
|
|
78
|
+
new_path = current_path.with_name(current_path.name.lower())
|
|
79
|
+
|
|
80
|
+
# convert nine to four char if necessary
|
|
81
|
+
if (
|
|
82
|
+
archived.log_format == 1
|
|
83
|
+
and "_" in new_path.name
|
|
84
|
+
and new_path.name.index("_") == 9
|
|
85
|
+
):
|
|
86
|
+
new_path = new_path.with_name(f"{new_path.name[0:4]}{new_path.name[9:]}")
|
|
87
|
+
# convert four char to nine if necessary
|
|
88
|
+
elif (
|
|
89
|
+
archived.log_format == 4
|
|
90
|
+
and "_" in new_path.name
|
|
91
|
+
and new_path.name.index("_") == 4
|
|
92
|
+
):
|
|
93
|
+
new_path = new_path.with_name(
|
|
94
|
+
f"{archived.index.site.name[0:9].lower()}{new_path.name[4:]}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# add timestamp if necessary
|
|
98
|
+
# collisions - can happen in weird 4char/9char inconsistent same date cases
|
|
99
|
+
if new_path.name.count("_") == 2 or new_path.name in names:
|
|
100
|
+
new_path = new_path.with_name(
|
|
101
|
+
f"{new_path.stem[0 : new_path.stem.rindex('_')]}_{archived.index.valid_range.lower.strftime('%H%M%S')}{new_path.suffix}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if new_path != current_path:
|
|
105
|
+
normalized += 1
|
|
106
|
+
path_changes.append((current_path, new_path))
|
|
107
|
+
names.add(new_path.name)
|
|
108
|
+
archived.file.name = archived.file.name.replace(
|
|
109
|
+
current_path.name, new_path.name
|
|
110
|
+
)
|
|
111
|
+
archived.name = Path(archived.file.name).name
|
|
112
|
+
update_fields.extend(["file", "name"])
|
|
113
|
+
# print(f"Normalized indexed file name {current_path.name} -> {new_path.name}")
|
|
114
|
+
else:
|
|
115
|
+
names.add(current_path.name)
|
|
116
|
+
|
|
117
|
+
if update_fields:
|
|
118
|
+
archived.save(update_fields=update_fields)
|
|
119
|
+
|
|
120
|
+
for old, new in path_changes:
|
|
121
|
+
if (
|
|
122
|
+
new.is_file()
|
|
123
|
+
and not (new.name == old.name.lower())
|
|
124
|
+
and new.read_text() != old.read_text()
|
|
125
|
+
):
|
|
126
|
+
print(f"WARNING: {old} will overwrite {new}!")
|
|
127
|
+
old.rename(new)
|
|
128
|
+
|
|
129
|
+
print(f"\nConverted {absolute} absolute paths to relative paths.")
|
|
130
|
+
print(f"Deleted {unindexed} files.")
|
|
131
|
+
print(f"Normalized {normalized} files.")
|
|
132
|
+
print(f"Synchronized {timestamps} file timestamps.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def noop(apps, schema_editor):
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Migration(migrations.Migration):
|
|
140
|
+
dependencies = [
|
|
141
|
+
# Replace with your latest migration
|
|
142
|
+
("slm", "add_index_order_index"),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
operations = [
|
|
146
|
+
migrations.RunPython(normalize_index, reverse_code=noop, atomic=True),
|
|
147
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def noop(apps, schema_editor):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def simplify_files(apps, schema_editor):
|
|
11
|
+
ArchivedSiteLog = apps.get_model("slm", "ArchivedSiteLog")
|
|
12
|
+
|
|
13
|
+
# paths to update - we do this after the database updates succeed, so if they're rolled
|
|
14
|
+
# back after an exception the disk state will still match the db
|
|
15
|
+
path_changes = []
|
|
16
|
+
simplified = 0
|
|
17
|
+
|
|
18
|
+
for archived in ArchivedSiteLog.objects.order_by("timestamp"):
|
|
19
|
+
if archived.file.name.count("_") == 2:
|
|
20
|
+
# make sure the first on a date does not have the timestamp on the end of it
|
|
21
|
+
current_path = Path(archived.file.path)
|
|
22
|
+
stem, ext = current_path.stem, current_path.suffix
|
|
23
|
+
simple_path = current_path.with_name(f"{stem[: stem.rindex('_')]}{ext}")
|
|
24
|
+
if not simple_path.exists():
|
|
25
|
+
path_changes.append((current_path, simple_path))
|
|
26
|
+
archived.file.name = archived.file.name.replace(
|
|
27
|
+
current_path.name, simple_path.name
|
|
28
|
+
)
|
|
29
|
+
archived.name = Path(archived.file.name).name
|
|
30
|
+
archived.save(update_fields=["file", "name"])
|
|
31
|
+
# print(f"Simplified indexed file name {current_path.name} -> {simple_path.name}")
|
|
32
|
+
simplified += 1
|
|
33
|
+
|
|
34
|
+
for old, new in path_changes:
|
|
35
|
+
old.rename(new)
|
|
36
|
+
|
|
37
|
+
print(f"\nSimplified {simplified} files.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Migration(migrations.Migration):
|
|
41
|
+
dependencies = [
|
|
42
|
+
# Replace with your latest migration
|
|
43
|
+
("slm", "normalize_index"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
operations = [
|
|
47
|
+
migrations.RunPython(simplify_files, reverse_code=noop, atomic=True),
|
|
48
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from pprint import pformat
|
|
3
|
+
|
|
4
|
+
from django.db import migrations
|
|
5
|
+
from django.db.models import Count, Func, Q
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def noop(apps, schema_editor):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify_index(apps, schema_editor):
|
|
13
|
+
"""
|
|
14
|
+
Run final sanity check.
|
|
15
|
+
|
|
16
|
+
Verifies the following conditions:
|
|
17
|
+
|
|
18
|
+
1. All files exist
|
|
19
|
+
2. All db row names match the file names
|
|
20
|
+
3. No files are referenced by more than one db row
|
|
21
|
+
4. That all file timestamps match the index begin timestamp
|
|
22
|
+
"""
|
|
23
|
+
ArchivedSiteLog = apps.get_model("slm", "ArchivedSiteLog")
|
|
24
|
+
|
|
25
|
+
assert ArchivedSiteLog.objects.filter(file__startswith="/").count() == 0, (
|
|
26
|
+
"There are still absolute paths in the archived site log file field."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# final sanity checks
|
|
30
|
+
# check that all files exist
|
|
31
|
+
for archived in ArchivedSiteLog.objects.all():
|
|
32
|
+
assert Path(archived.file.path).is_file(), (
|
|
33
|
+
f"WARNING: {archived} index file does not exist!"
|
|
34
|
+
)
|
|
35
|
+
assert archived.name == Path(archived.file.name).name, (
|
|
36
|
+
f"{Path(archived.file.name).name}/{archived.name} mismatch!"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# check that all files are unique
|
|
40
|
+
duplicates = (
|
|
41
|
+
ArchivedSiteLog.objects.values("file") # file.name
|
|
42
|
+
.annotate(count=Count("id"))
|
|
43
|
+
.filter(count__gt=1)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# this must be true because following migrations will make this field unique!
|
|
47
|
+
assert not duplicates.exists(), (
|
|
48
|
+
f"Some indexes share the same files: {pformat(duplicates)}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
bad_timestamps = ArchivedSiteLog.objects.filter(
|
|
52
|
+
~Q(timestamp=Func("index__valid_range", function="lower"))
|
|
53
|
+
)
|
|
54
|
+
assert not bad_timestamps.count(), (
|
|
55
|
+
f"Some indexes timestamps do not align with the valid range: {pformat(bad_timestamps)}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Migration(migrations.Migration):
|
|
60
|
+
dependencies = [
|
|
61
|
+
# Replace with your latest migration
|
|
62
|
+
("slm", "simplify_index"),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
operations = [
|
|
66
|
+
migrations.RunPython(verify_index, reverse_code=noop),
|
|
67
|
+
]
|
slm/models/__init__.py
CHANGED
|
@@ -55,6 +55,7 @@ from slm.models.system import (
|
|
|
55
55
|
SiteFile,
|
|
56
56
|
SiteFileUpload,
|
|
57
57
|
SiteTideGauge,
|
|
58
|
+
SLMVersion,
|
|
58
59
|
TideGauge,
|
|
59
60
|
)
|
|
60
61
|
from slm.models.user import User, UserProfile
|
|
@@ -115,4 +116,5 @@ __all__ = [
|
|
|
115
116
|
"UserProfile",
|
|
116
117
|
"About",
|
|
117
118
|
"Help",
|
|
119
|
+
"SLMVersion",
|
|
118
120
|
]
|
slm/models/alerts.py
CHANGED
|
@@ -315,7 +315,7 @@ class Alert(PolymorphicModel):
|
|
|
315
315
|
if self.site_alert:
|
|
316
316
|
return reverse("slm:edit", kwargs={"station": self.target.name})
|
|
317
317
|
elif self.agency_alert:
|
|
318
|
-
return f
|
|
318
|
+
return f"{reverse('slm:home')}?agency={self.target.pk}"
|
|
319
319
|
elif self.user_alert:
|
|
320
320
|
return f"mailto:{self.target.email}"
|
|
321
321
|
return reverse("slm:alert", kwargs={"alert": self.pk})
|
|
@@ -398,7 +398,7 @@ class Alert(PolymorphicModel):
|
|
|
398
398
|
default=False,
|
|
399
399
|
blank=True,
|
|
400
400
|
help_text=_(
|
|
401
|
-
"Do not allow target users to clear this alert, only admins may
|
|
401
|
+
"Do not allow target users to clear this alert, only admins may clear."
|
|
402
402
|
),
|
|
403
403
|
)
|
|
404
404
|
|
|
@@ -426,7 +426,7 @@ class Alert(PolymorphicModel):
|
|
|
426
426
|
null=False,
|
|
427
427
|
blank=False,
|
|
428
428
|
help_text=_(
|
|
429
|
-
"If true, an email will be sent for this alert to every targeted
|
|
429
|
+
"If true, an email will be sent for this alert to every targeted user."
|
|
430
430
|
),
|
|
431
431
|
)
|
|
432
432
|
|
|
@@ -792,8 +792,7 @@ class GeodesyMLInvalid(AutomatedAlertMixin, SiteFile, Alert):
|
|
|
792
792
|
published = models.BooleanField(
|
|
793
793
|
null=False,
|
|
794
794
|
help_text=_(
|
|
795
|
-
"True if this alert was issued from the published version of the "
|
|
796
|
-
"site log."
|
|
795
|
+
"True if this alert was issued from the published version of the site log."
|
|
797
796
|
),
|
|
798
797
|
)
|
|
799
798
|
|
|
@@ -908,8 +907,7 @@ class ReviewRequested(AutomatedAlertMixin, Alert):
|
|
|
908
907
|
).format(self.requester.email, self.requester.name)
|
|
909
908
|
if self.requester
|
|
910
909
|
else _(
|
|
911
|
-
"A request has been made to publish the updates to this "
|
|
912
|
-
"site log."
|
|
910
|
+
"A request has been made to publish the updates to this site log."
|
|
913
911
|
)
|
|
914
912
|
)
|
|
915
913
|
super().save(*args, **kwargs)
|
|
@@ -1094,10 +1092,9 @@ class SiteLogPublished(AutomatedAlertMixin, Alert):
|
|
|
1094
1092
|
"An updated log has been published for this site. Download "
|
|
1095
1093
|
"the new {legacy_file} or the new {geodesyml_file}."
|
|
1096
1094
|
).format(
|
|
1097
|
-
legacy_file=f'<a href="{legacy_link}" download>'
|
|
1098
|
-
f'{_("legacy file")}</a>',
|
|
1095
|
+
legacy_file=f'<a href="{legacy_link}" download>{_("legacy file")}</a>',
|
|
1099
1096
|
geodesyml_file=f'<a href="{gml_link}" download>'
|
|
1100
|
-
f
|
|
1097
|
+
f"{_('GeodesyML file')}</a>",
|
|
1101
1098
|
)
|
|
1102
1099
|
super().save(*args, **kwargs)
|
|
1103
1100
|
|
slm/models/data.py
CHANGED
|
@@ -15,8 +15,7 @@ class DataAvailability(models.Model):
|
|
|
15
15
|
|
|
16
16
|
def __str__(self):
|
|
17
17
|
return (
|
|
18
|
-
f"[{self.site}] ({self.rinex_version.label}) "
|
|
19
|
-
f"{self.rate.label} {self.last}"
|
|
18
|
+
f"[{self.site}] ({self.rinex_version.label}) {self.rate.label} {self.last}"
|
|
20
19
|
)
|
|
21
20
|
|
|
22
21
|
class Meta:
|
slm/models/equipment.py
CHANGED
|
@@ -141,7 +141,7 @@ class Antenna(Equipment):
|
|
|
141
141
|
def full(self):
|
|
142
142
|
return (
|
|
143
143
|
f"{self.model} "
|
|
144
|
-
f
|
|
144
|
+
f"{self.reference_point.label if self.reference_point else ''} "
|
|
145
145
|
f"{self.features.label}"
|
|
146
146
|
)
|
|
147
147
|
|
slm/models/fields.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# upstream/fields.py
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.core.validators import RegexValidator
|
|
4
|
+
from django.db import models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StationNameField(models.CharField):
|
|
8
|
+
"""
|
|
9
|
+
The StationNameField allows validation and help text to be configured in settings.
|
|
10
|
+
These values will not be deconstructed into the migration files allowing users of
|
|
11
|
+
the SLM to define their own semantics and validation logic around station names.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
class StationNameValidator(RegexValidator):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
if regex := getattr(settings, "SLM_STATION_NAME_REGEX", None):
|
|
19
|
+
kwargs.setdefault("validators", [])
|
|
20
|
+
kwargs["validators"].append(self.StationNameValidator(regex))
|
|
21
|
+
self._help_text = kwargs.pop("help_text", None)
|
|
22
|
+
help_text = getattr(settings, "SLM_STATION_NAME_HELP", self._help_text)
|
|
23
|
+
if help_text:
|
|
24
|
+
kwargs["help_text"] = help_text
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
def deconstruct(self):
|
|
28
|
+
name, path, args, kwargs = super().deconstruct()
|
|
29
|
+
# Ensure the regex pattern doesn't get serialized if it came from settings
|
|
30
|
+
validators = [
|
|
31
|
+
validator
|
|
32
|
+
for validator in kwargs.pop("validators", [])
|
|
33
|
+
if not isinstance(validator, self.StationNameValidator)
|
|
34
|
+
]
|
|
35
|
+
if validators:
|
|
36
|
+
kwargs["validators"] = validators
|
|
37
|
+
|
|
38
|
+
# we want the cannonical help text to be used in migration files
|
|
39
|
+
if self._help_text is not None:
|
|
40
|
+
kwargs["help_text"] = self._help_text
|
|
41
|
+
return name, path, args, kwargs
|