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.
Files changed (151) hide show
  1. igs_slm-0.1.5b1.dist-info/METADATA +115 -0
  2. {igs_slm-0.1.4b0.dist-info → igs_slm-0.1.5b1.dist-info}/RECORD +193 -173
  3. {igs_slm-0.1.4b0.dist-info → igs_slm-0.1.5b1.dist-info}/WHEEL +1 -1
  4. igs_slm-0.1.5b1.dist-info/entry_points.txt +3 -0
  5. {igs_slm-0.1.4b0.dist-info → igs_slm-0.1.5b1.dist-info/licenses}/LICENSE +1 -1
  6. slm/__init__.py +17 -14
  7. slm/admin.py +32 -5
  8. slm/api/edit/views.py +22 -9
  9. slm/api/public/views.py +9 -7
  10. slm/api/views.py +45 -6
  11. slm/apps.py +28 -6
  12. slm/authentication.py +3 -2
  13. slm/bin/startproject.py +102 -31
  14. slm/bin/templates/{{ project_dir }}/pyproject.toml +30 -21
  15. slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/base.py +12 -1
  16. slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/develop/__init__.py +5 -27
  17. slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/production/__init__.py +6 -27
  18. slm/bin/templates/{{ project_dir }}/src/sites/{{ site }}/urls.py +15 -0
  19. slm/bin/templates/{{ project_dir }}/src/sites/{{ site }}/validation.py +29 -0
  20. slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/urls.py +1 -0
  21. slm/context.py +5 -0
  22. slm/defines/AlertLevel.py +10 -2
  23. slm/defines/AntennaCalibration.py +6 -4
  24. slm/defines/AntennaFeatures.py +12 -9
  25. slm/defines/AntennaReferencePoint.py +12 -10
  26. slm/defines/Aspiration.py +4 -2
  27. slm/defines/CardinalDirection.py +6 -4
  28. slm/defines/CollocationStatus.py +3 -1
  29. slm/defines/EquipmentState.py +6 -12
  30. slm/defines/FlagSeverity.py +4 -2
  31. slm/defines/FractureSpacing.py +7 -5
  32. slm/defines/FrequencyStandardType.py +6 -4
  33. slm/defines/GeodesyMLVersion.py +2 -0
  34. slm/defines/Instrumentation.py +9 -7
  35. slm/defines/LogEntryType.py +17 -15
  36. slm/defines/SLMFileType.py +4 -2
  37. slm/defines/SiteFileUploadStatus.py +7 -24
  38. slm/defines/SiteLogFormat.py +8 -32
  39. slm/defines/SiteLogStatus.py +9 -28
  40. slm/defines/TectonicPlates.py +18 -16
  41. slm/manage.py +24 -0
  42. slm/management/commands/check_upgrade.py +142 -0
  43. slm/management/commands/generate_sinex.py +110 -92
  44. slm/management/commands/head_from_index.py +11 -8
  45. slm/management/commands/import_archive.py +27 -18
  46. slm/management/commands/import_equipment.py +1 -1
  47. slm/management/commands/sitelog.py +1 -3
  48. slm/management/commands/synchronize.py +1 -1
  49. slm/management/commands/validate_db.py +6 -4
  50. slm/map/defines.py +18 -14
  51. slm/map/templates/slm/map.html +5 -7
  52. slm/migrations/0001_remove_archiveindex_no_overlapping_ranges_per_site_and_more.py +26 -0
  53. slm/migrations/0002_alter_archivedsitelog_file_and_more.py +44 -0
  54. slm/migrations/0003_alter_archivedsitelog_name_and_more.py +35 -0
  55. slm/migrations/0004_alter_site_name.py +24 -0
  56. slm/migrations/0005_slmversion.py +30 -0
  57. slm/migrations/0017_alter_logentry_unique_together_and_more.py +3 -1
  58. slm/migrations/0018_afix_deleted.py +3 -1
  59. slm/migrations/0018_alter_siteantenna_options_and_more.py +87 -56
  60. slm/migrations/0019_remove_siteantenna_marker_enu_siteantenna_marker_une_and_more.py +1 -1
  61. slm/migrations/0023_archivedsitelog_gml_version_and_more.py +1 -1
  62. slm/migrations/0031_alter_antenna_features.py +44 -0
  63. slm/migrations/0032_archiveindex_valid_range_and_more.py +84 -0
  64. slm/migrations/add_index_order_index.py +54 -0
  65. slm/migrations/normalize_index.py +147 -0
  66. slm/migrations/simplify_index.py +48 -0
  67. slm/migrations/verify_index.py +67 -0
  68. slm/models/__init__.py +2 -0
  69. slm/models/alerts.py +7 -10
  70. slm/models/data.py +1 -2
  71. slm/models/equipment.py +1 -1
  72. slm/models/fields.py +41 -0
  73. slm/models/index.py +183 -53
  74. slm/models/sitelog.py +35 -38
  75. slm/models/system.py +72 -31
  76. slm/models/user.py +1 -1
  77. slm/parsing/__init__.py +33 -16
  78. slm/parsing/legacy/binding.py +65 -34
  79. slm/parsing/legacy/parser.py +2 -2
  80. slm/parsing/xsd/binding.py +1 -1
  81. slm/parsing/xsd/parser.py +1 -2
  82. slm/receivers/__init__.py +2 -2
  83. slm/receivers/index.py +2 -1
  84. slm/receivers/migration.py +21 -0
  85. slm/settings/__init__.py +192 -4
  86. slm/settings/assets.py +26 -0
  87. slm/settings/auth.py +18 -14
  88. slm/settings/ckeditor.py +12 -6
  89. slm/settings/debug.py +2 -2
  90. slm/settings/emails.py +50 -0
  91. slm/settings/internationalization.py +8 -6
  92. slm/settings/logging.py +100 -88
  93. slm/settings/platform/darwin.py +16 -6
  94. slm/settings/rest.py +20 -15
  95. slm/settings/root.py +192 -98
  96. slm/settings/routines.py +5 -1
  97. slm/settings/secrets.py +20 -31
  98. slm/settings/security.py +7 -5
  99. slm/settings/slm.py +35 -23
  100. slm/settings/static_templates.py +12 -9
  101. slm/settings/templates.py +31 -25
  102. slm/settings/uploads.py +33 -5
  103. slm/settings/urls.py +7 -12
  104. slm/settings/validation.py +165 -165
  105. slm/signals.py +3 -2
  106. slm/static/slm/css/style.css +37 -36
  107. slm/static/slm/js/autocomplete.js +6 -4
  108. slm/static/slm/js/enums.js +6 -5
  109. slm/static/slm/js/file_modal.js +62 -0
  110. slm/static/slm/js/form.js +3 -3
  111. slm/static/slm/js/formWidget.js +3 -3
  112. slm/static/slm/js/persistable.js +5 -1
  113. slm/templates/admin/base.html +1 -0
  114. slm/templates/rest_framework/base.html +23 -11
  115. slm/templates/slm/base.html +27 -22
  116. slm/templates/slm/forms/widgets/auto_complete.html +12 -11
  117. slm/templates/slm/forms/widgets/auto_complete_multiple.html +8 -7
  118. slm/templates/slm/station/download.html +6 -6
  119. slm/templates/slm/station/edit.html +9 -17
  120. slm/templates/slm/station/review.html +5 -3
  121. slm/templates/slm/station/upload.html +4 -1
  122. slm/templates/slm/station/uploads/legacy.html +1 -1
  123. slm/templates/slm/widgets/alert_scroll.html +4 -8
  124. slm/templates/slm/widgets/filelist.html +0 -5
  125. slm/templates/slm/widgets/log_scroll.html +2 -13
  126. slm/templates/slm/widgets/stationlist.html +2 -8
  127. slm/templatetags/slm.py +70 -9
  128. slm/utils.py +13 -4
  129. slm/validators.py +14 -14
  130. slm/views.py +6 -6
  131. slm/wsgi.py +16 -0
  132. igs_slm-0.1.4b0.dist-info/METADATA +0 -154
  133. igs_slm-0.1.4b0.dist-info/entry_points.txt +0 -3
  134. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/urls.py +0 -7
  135. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/validation.py +0 -11
  136. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/__init__.py +0 -0
  137. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/__init__.py +0 -0
  138. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/develop/local.py +0 -0
  139. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/develop/wsgi.py +0 -0
  140. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/manage.py +0 -0
  141. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/production/wsgi.py +0 -0
  142. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/__init__.py +0 -0
  143. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/admin.py +0 -0
  144. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/apps.py +0 -0
  145. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/__init__.py +0 -0
  146. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/commands/__init__.py +0 -0
  147. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/commands/import_archive.py +0 -0
  148. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/migrations/__init__.py +0 -0
  149. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/models.py +0 -0
  150. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/templates/slm/base.html +0 -0
  151. /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'{reverse("slm:home")}?agency={self.target.pk}'
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 " "clear."
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 " "user."
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'{_("GeodesyML file")}</a>',
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'{self.reference_point.label if self.reference_point else ""} '
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