igs-slm 0.1.5b3__py3-none-any.whl → 0.2.0b1__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 (47) hide show
  1. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/METADATA +2 -2
  2. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/RECORD +47 -34
  3. slm/__init__.py +1 -1
  4. slm/admin.py +40 -3
  5. slm/api/edit/views.py +37 -2
  6. slm/api/public/serializers.py +1 -1
  7. slm/defines/CoordinateMode.py +9 -0
  8. slm/defines/SiteLogFormat.py +19 -6
  9. slm/defines/__init__.py +24 -22
  10. slm/file_views/apps.py +7 -0
  11. slm/file_views/config.py +253 -0
  12. slm/file_views/settings.py +124 -0
  13. slm/file_views/static/slm/file_views/banner_header.png +0 -0
  14. slm/file_views/static/slm/file_views/css/listing.css +82 -0
  15. slm/file_views/templates/slm/file_views/listing.html +70 -0
  16. slm/file_views/urls.py +47 -0
  17. slm/file_views/views.py +472 -0
  18. slm/forms.py +22 -4
  19. slm/jinja2/slm/sitelog/ascii_9char.log +1 -1
  20. slm/jinja2/slm/sitelog/legacy.log +1 -1
  21. slm/management/commands/check_upgrade.py +25 -19
  22. slm/management/commands/generate_sinex.py +9 -7
  23. slm/map/settings.py +0 -0
  24. slm/migrations/0001_alter_archivedsitelog_size_and_more.py +44 -0
  25. slm/migrations/0032_archiveindex_valid_range_and_more.py +8 -1
  26. slm/migrations/simplify_daily_index_files.py +86 -0
  27. slm/models/index.py +73 -6
  28. slm/models/sitelog.py +6 -0
  29. slm/models/system.py +35 -2
  30. slm/parsing/__init__.py +10 -0
  31. slm/parsing/legacy/binding.py +3 -2
  32. slm/receivers/cache.py +25 -0
  33. slm/settings/root.py +22 -0
  34. slm/settings/routines.py +2 -0
  35. slm/settings/slm.py +58 -0
  36. slm/settings/urls.py +1 -1
  37. slm/settings/validation.py +5 -4
  38. slm/signals.py +3 -4
  39. slm/static/slm/js/enums.js +7 -6
  40. slm/static/slm/js/form.js +25 -14
  41. slm/static/slm/js/slm.js +4 -2
  42. slm/templatetags/slm.py +1 -1
  43. slm/utils.py +161 -36
  44. slm/validators.py +51 -0
  45. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/WHEEL +0 -0
  46. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/entry_points.txt +0 -0
  47. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -22,7 +22,7 @@ from typing_extensions import Annotated
22
22
 
23
23
  from slm.defines import ISOCountry
24
24
  from slm.models import Network, Site, SiteAntenna, SiteReceiver
25
- from slm.utils import dddmmss_ss_parts, transliterate, xyz2llh
25
+ from slm.utils import dddmmss_ss_parts, lon_180_to_360, transliterate, xyz2llh
26
26
 
27
27
  DEFAULT_ANTEX = "https://files.igs.org/pub/station/general/igs20.atx.gz"
28
28
 
@@ -275,15 +275,13 @@ class Command(TyperCommand):
275
275
  continue
276
276
 
277
277
  # is sinex longitude 0-360 or -180 to 180?
278
- lon_deg, lon_min, lon_sec = dddmmss_ss_parts(
279
- llh[1] if llh[1] > 0 else llh[1] + 360
280
- )
278
+ lon_deg, lon_min, lon_sec = dddmmss_ss_parts(lon_180_to_360(llh[1]))
281
279
 
282
280
  yield (
283
281
  f" {site.four_id.lower()} A "
284
282
  f"{site.iers_domes_number:>9} P "
285
- f"{location:<21} {lon_deg:3d} {lon_min:2d} "
286
- f"{lon_sec:>4.1f} {lat_deg:3d} {lat_min:2d} "
283
+ f"{location:<21} {lon_deg:3.0f} {lon_min:2d} "
284
+ f"{lon_sec:>4.1f} {lat_deg:3.0f} {lat_min:2d} "
287
285
  f"{lat_sec:>4.1f} {llh[2]:>7.1f}"
288
286
  )
289
287
  yield "-SITE/ID"
@@ -334,7 +332,11 @@ class Command(TyperCommand):
334
332
  f"{antenna.antenna_type.model:<15.15} "
335
333
  f"{antenna.radome_type.model:4.4} "
336
334
  f"{antenna.serial_number:<5.5} "
337
- f"{antenna.alignment if antenna.alignment else 0.0:>4.0f}"
335
+ + (
336
+ f"{antenna.alignment:>4.0f}"
337
+ if antenna.alignment is not None
338
+ else " " * 4
339
+ )
338
340
  )
339
341
  # todo: add antenna offset defaulting to zero if its unknown?
340
342
  # seems wrong
slm/map/settings.py ADDED
File without changes
@@ -0,0 +1,44 @@
1
+ # Generated by Django 4.2.23 on 2025-09-03 23:05
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("slm", "simplify_daily_index_files"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AlterField(
13
+ model_name="archivedsitelog",
14
+ name="size",
15
+ field=models.PositiveIntegerField(
16
+ blank=True, db_index=True, default=None, null=True
17
+ ),
18
+ ),
19
+ migrations.AlterField(
20
+ model_name="geodesymlinvalid",
21
+ name="size",
22
+ field=models.PositiveIntegerField(
23
+ blank=True, db_index=True, default=None, null=True
24
+ ),
25
+ ),
26
+ migrations.AlterField(
27
+ model_name="sitefileupload",
28
+ name="size",
29
+ field=models.PositiveIntegerField(
30
+ blank=True, db_index=True, default=None, null=True
31
+ ),
32
+ ),
33
+ migrations.AlterField(
34
+ model_name="sitemoreinformation",
35
+ name="more_info",
36
+ field=models.URLField(
37
+ blank=True,
38
+ db_index=True,
39
+ default="",
40
+ max_length=8000,
41
+ verbose_name="URL for More Information",
42
+ ),
43
+ ),
44
+ ]
@@ -1,5 +1,7 @@
1
1
  # Generated by Django 4.2.20 on 2025-05-08 19:52
2
2
 
3
+ from datetime import timedelta
4
+
3
5
  import django.contrib.postgres.constraints
4
6
  import django.contrib.postgres.fields.ranges
5
7
  from django.contrib.postgres.fields.ranges import DateTimeTZRange
@@ -10,8 +12,13 @@ def populate_valid_range(apps, schema_editor):
10
12
  ArchiveIndex = apps.get_model("slm", "ArchiveIndex")
11
13
 
12
14
  for row in ArchiveIndex.objects.all():
15
+ begin = row.begin
16
+ end = row.end
17
+ if begin == end:
18
+ # add a little salt
19
+ end = end + timedelta(seconds=1)
13
20
  ArchiveIndex.objects.filter(pk=row.pk).update(
14
- valid_range=DateTimeTZRange(row.begin, row.end)
21
+ valid_range=DateTimeTZRange(begin, end)
15
22
  )
16
23
 
17
24
 
@@ -0,0 +1,86 @@
1
+ """
2
+ If multiple index files exist for a given station on a given day we remove the timestamp
3
+ from the name of the latest daily file and add the timestamp to the earliest daily file.
4
+ """
5
+
6
+ from datetime import datetime, time, timedelta, timezone
7
+ from pathlib import Path
8
+
9
+ from django.db import migrations
10
+ from django.db.models import Count, Value
11
+ from django.db.models.functions import Length, Replace
12
+
13
+
14
+ def simplify_index(apps, schema_editor):
15
+ ArchivedSiteLog = apps.get_model("slm", "ArchivedSiteLog")
16
+ ArchiveIndex = apps.get_model("slm", "ArchiveIndex")
17
+
18
+ multi_day_indexes = ArchiveIndex.objects.filter(
19
+ files__in=ArchivedSiteLog.objects.annotate(
20
+ ucount=Length("file") - Length(Replace("file", Value("_"), Value("")))
21
+ ).filter(ucount__gte=2)
22
+ ).distinct()
23
+
24
+ multi_day_tuples = list(
25
+ multi_day_indexes.extra(select={"range_date": "DATE(lower(valid_range))"})
26
+ .values("site__name", "range_date")
27
+ .annotate(count=Count("id"))
28
+ .filter(count__gt=1)
29
+ .values_list("site__name", "range_date", flat=False)
30
+ )
31
+
32
+ for station, day in multi_day_tuples:
33
+ lower = datetime.combine(day, time.min, tzinfo=timezone.utc)
34
+ upper = lower + timedelta(days=1)
35
+ indexes = ArchiveIndex.objects.filter(
36
+ site__name=station,
37
+ valid_range__startswith__gte=lower,
38
+ valid_range__startswith__lte=upper,
39
+ ).order_by("valid_range")
40
+ assert indexes.count() >= 2
41
+ first = indexes.first()
42
+ last = indexes.last()
43
+
44
+ for file in first.files.all():
45
+ current_path = Path(file.file.path)
46
+ new_path = Path(file.file.path)
47
+ if current_path.stem.count("_") < 2:
48
+ new_path = current_path.with_name(
49
+ f"{current_path.stem}_{first.valid_range.lower.strftime('%H%M%S')}{current_path.suffix}"
50
+ )
51
+ file.file.name = file.file.name.replace(
52
+ current_path.name, new_path.name
53
+ )
54
+ file.name = Path(file.file.name).name
55
+ current_path.rename(new_path)
56
+ file.save(update_fields=["file", "name"])
57
+ print(f"Updated {current_path.name} -> {new_path.name}")
58
+
59
+ for file in last.files.all():
60
+ current_path = Path(file.file.path)
61
+ new_path = Path(file.file.path)
62
+ if current_path.stem.count("_") >= 2:
63
+ new_path = current_path.with_name(
64
+ "_".join(current_path.stem.split("_")[:-1]) + current_path.suffix
65
+ )
66
+ file.file.name = file.file.name.replace(
67
+ current_path.name, new_path.name
68
+ )
69
+ file.name = Path(file.file.name).name
70
+ current_path.rename(new_path)
71
+ file.save(update_fields=["file", "name"])
72
+ print(f"Updated {current_path.name} -> {new_path.name}")
73
+
74
+
75
+ def noop(apps, schema_editor):
76
+ pass
77
+
78
+
79
+ class Migration(migrations.Migration):
80
+ dependencies = [
81
+ ("slm", "0005_slmversion"),
82
+ ]
83
+
84
+ operations = [
85
+ migrations.RunPython(simplify_index, reverse_code=noop, atomic=True),
86
+ ]
slm/models/index.py CHANGED
@@ -2,6 +2,7 @@ import os
2
2
  import typing as t
3
3
  from datetime import datetime
4
4
 
5
+ from django.conf import settings
5
6
  from django.contrib.postgres.constraints import ExclusionConstraint
6
7
  from django.contrib.postgres.fields import DateTimeRangeField
7
8
  from django.contrib.postgres.fields.ranges import DateTimeTZRange, RangeOperators
@@ -9,15 +10,19 @@ from django.core.exceptions import ValidationError
9
10
  from django.core.files.base import ContentFile
10
11
  from django.db import models, transaction
11
12
  from django.db.models import (
13
+ Case,
12
14
  CharField,
13
15
  DateTimeField,
14
16
  Deferrable,
15
17
  F,
16
18
  Func,
19
+ IntegerField,
17
20
  OuterRef,
18
21
  Q,
19
22
  Subquery,
20
23
  Value,
24
+ When,
25
+ Window,
21
26
  )
22
27
  from django.db.models.functions import (
23
28
  Cast,
@@ -28,7 +33,9 @@ from django.db.models.functions import (
28
33
  Lower,
29
34
  LPad,
30
35
  Now,
36
+ RowNumber,
31
37
  Substr,
38
+ TruncDate,
32
39
  )
33
40
  from django.urls import reverse
34
41
  from django.utils.functional import cached_property
@@ -274,7 +281,7 @@ class ArchiveIndexQuerySet(models.QuerySet):
274
281
  name_len: t.Optional[int] = None,
275
282
  field_name: str = "filename",
276
283
  lower_case: bool = False,
277
- log_format: bool = None,
284
+ log_format: SiteLogFormat = None,
278
285
  ):
279
286
  """
280
287
  Add the log names (w/o) extension as a property called filename to
@@ -308,7 +315,7 @@ class ArchiveIndexQuerySet(models.QuerySet):
308
315
  LPad(Cast(ExtractDay(begin), models.CharField()), 2, fill_text=Value("0")),
309
316
  ]
310
317
  if log_format:
311
- parts.append(Value(f".{log_format.ext}"))
318
+ parts.append(Value(f".{log_format.suffix}"))
312
319
 
313
320
  return self.annotate(**{field_name: Concat(*parts, output_field=CharField())})
314
321
 
@@ -455,7 +462,7 @@ class ArchivedSiteLogQuerySet(models.QuerySet):
455
462
  name_len: t.Optional[int] = None,
456
463
  field_name: str = "filename",
457
464
  lower_case: bool = False,
458
- log_format: t.Optional[SiteLogFormat] = None,
465
+ include_ext: bool = True,
459
466
  ):
460
467
  """
461
468
  Add the log names (w/o) extension as a property called filename to
@@ -465,7 +472,7 @@ class ArchivedSiteLogQuerySet(models.QuerySet):
465
472
  the first name_len characters of the site name.
466
473
  :param field_name: Change the name of the annotated field.
467
474
  :param lower_case: Filenames will be lowercase if true.
468
- :param log_format: If given, add the extension for the given format
475
+ :param include_ext: If true (default), include the extension for the log file type.
469
476
  :return: A queryset with the filename annotation added.
470
477
  """
471
478
  name_str = F("site__name")
@@ -496,10 +503,70 @@ class ArchivedSiteLogQuerySet(models.QuerySet):
496
503
  fill_text=Value("0"),
497
504
  ),
498
505
  ]
499
- if log_format:
500
- parts.append(Value(f".{log_format.ext}"))
506
+ if include_ext:
507
+ parts.append(
508
+ Case(
509
+ *[
510
+ When(log_format=key, then=Value(f".{ext}"))
511
+ for key, ext in getattr(
512
+ settings,
513
+ "SLM_FORMAT_EXTENSIONS",
514
+ {fmt: fmt.ext for fmt in SiteLogFormat},
515
+ ).items()
516
+ ],
517
+ default=Value(""),
518
+ output_field=CharField(),
519
+ ),
520
+ )
501
521
  return self.annotate(**{field_name: Concat(*parts)})
502
522
 
523
+ def best_format(self):
524
+ """
525
+ This query fetches a linear history of site logs, but only picks the most appropriate
526
+ format from the index for each point in time. By default the most appropriate format
527
+ is the rank ordering defined in :class:`slm.defines.SiteLogFormat` unless otherwise
528
+ specified in the :setting:`SLM_FORMAT_PRIORITY` mapping.
529
+ """
530
+ if priorities := getattr(settings, "SLM_FORMAT_PRIORITY"):
531
+ return self.annotate(
532
+ log_format_order=Case(
533
+ *[When(log_format=k, then=v) for k, v in priorities.items()],
534
+ default=999,
535
+ output_field=IntegerField(),
536
+ ),
537
+ best_fmt=Window(
538
+ expression=RowNumber(),
539
+ partition_by=[
540
+ F("index__site"),
541
+ TruncDate("timestamp"),
542
+ ],
543
+ order_by=[
544
+ F("timestamp").desc(),
545
+ F("log_format_order").asc(), # use mapped priority
546
+ ],
547
+ ),
548
+ ).filter(best_fmt=1)
549
+ else:
550
+ return self.annotate(
551
+ best_fmt=Window(
552
+ expression=RowNumber(),
553
+ partition_by=[
554
+ F("index__site"),
555
+ TruncDate("timestamp"),
556
+ ],
557
+ order_by=[
558
+ F("timestamp").desc(),
559
+ F("log_format").desc(),
560
+ ], # higher format wins
561
+ ),
562
+ ).filter(best_fmt=1)
563
+
564
+ def most_recent(self):
565
+ return self.filter(index__valid_range__upper_inf=True)
566
+
567
+ def non_current(self):
568
+ return self.filter(index__valid_range__upper_inf=False)
569
+
503
570
 
504
571
  class ArchivedSiteLog(SiteFile):
505
572
  SUB_DIRECTORY = "archive"
slm/models/sitelog.py CHANGED
@@ -45,6 +45,7 @@ from slm.defines import (
45
45
  AntennaReferencePoint,
46
46
  Aspiration,
47
47
  CollocationStatus,
48
+ CoordinateMode,
48
49
  FractureSpacing,
49
50
  FrequencyStandardType,
50
51
  ISOCountry,
@@ -2255,6 +2256,10 @@ class SiteLocation(SiteSection):
2255
2256
 
2256
2257
  objects = SiteLocationManager.from_queryset(SiteLocationQueryset)()
2257
2258
 
2259
+ coordinate_mode = getattr(
2260
+ settings, "SLM_COORDINATE_MODE", CoordinateMode.INDEPENDENT
2261
+ )
2262
+
2258
2263
  @classmethod
2259
2264
  def structure(cls):
2260
2265
  return [
@@ -4219,6 +4224,7 @@ class SiteMoreInformation(SiteSection):
4219
4224
  blank=True,
4220
4225
  verbose_name=_("URL for More Information"),
4221
4226
  db_index=True,
4227
+ max_length=8000,
4222
4228
  )
4223
4229
 
4224
4230
  sitemap = models.CharField(
slm/models/system.py CHANGED
@@ -40,6 +40,11 @@ def site_upload_path(instance: "SiteFile", filename: str) -> str:
40
40
  file will be saved to:
41
41
  MEDIA_ROOT/uploads/<site name>/filename
42
42
 
43
+ If there is a name collision with a file on disk, the _HHMMSS from the file's
44
+ timestamp will be added onto the name. If the collision is with an ArchivedSiteLog
45
+ the file already on disk will have its name changed to include the timestamp. This
46
+ keeps the most recently indexed file for each day having just the date timestamp.
47
+
43
48
  :param filename: The name of the file
44
49
  :return: The path where the site file should reside.
45
50
  """
@@ -56,7 +61,29 @@ def site_upload_path(instance: "SiteFile", filename: str) -> str:
56
61
  )
57
62
  if (Path(settings.MEDIA_ROOT) / dest).exists():
58
63
  stem, suffix = dest.stem, dest.suffix
59
- dest = dest.with_name(f"{stem}_{timestamp.strftime('%H%M%S')}{suffix}")
64
+ if isinstance(instance, ArchivedSiteLog):
65
+ # TODO - there is potential here for the database to be out of sync with the filesytem if the outer
66
+ # transaction fails
67
+ try:
68
+ for archive in ArchivedSiteLog.objects.filter(
69
+ file__endswith=str(dest.as_posix())
70
+ ):
71
+ current_path = Path(archive.file.path)
72
+ new_path = Path(archive.file.path)
73
+ if current_path.stem.count("_") < 2:
74
+ new_path = current_path.with_name(
75
+ f"{current_path.stem}_{archive.index.valid_range.lower.strftime('%H%M%S')}{current_path.suffix}"
76
+ )
77
+ archive.file.name = archive.file.name.replace(
78
+ current_path.name, new_path.name
79
+ )
80
+ archive.name = Path(archive.file.name).name
81
+ current_path.rename(new_path)
82
+ archive.save(update_fields=["file", "name"])
83
+ except ArchivedSiteLog.DoesNotExist:
84
+ (Path(settings.MEDIA_ROOT) / dest).unlink()
85
+ else:
86
+ dest = dest.with_name(f"{stem}_{timestamp.strftime('%H%M%S')}{suffix}")
60
87
  return dest.as_posix()
61
88
 
62
89
 
@@ -212,7 +239,9 @@ class SiteFile(models.Model):
212
239
  unique=True,
213
240
  )
214
241
 
215
- size = models.PositiveIntegerField(null=True, default=None, blank=True)
242
+ size = models.PositiveIntegerField(
243
+ null=True, default=None, blank=True, db_index=True
244
+ )
216
245
 
217
246
  thumbnail = models.ImageField(
218
247
  upload_to=site_thumbnail_path,
@@ -254,6 +283,10 @@ class SiteFile(models.Model):
254
283
  help_text=_("The Geodesy ML version. (Only if file_type is GeodesyML)"),
255
284
  )
256
285
 
286
+ @property
287
+ def on_disk(self) -> Path:
288
+ return Path(self.file.path)
289
+
257
290
  def update_directory(self):
258
291
  for file in [self.file, self.thumbnail]:
259
292
  if not file or not file.path:
slm/parsing/__init__.py CHANGED
@@ -788,6 +788,16 @@ def to_str(value):
788
788
  return value
789
789
 
790
790
 
791
+ def concat_str(value):
792
+ """
793
+ For multi-line inputs, concatenate the lines, stripping white space at each line
794
+ break. This is necessary for things like multi-line urls.
795
+ """
796
+ if value is None:
797
+ return ""
798
+ return "".join([ln.strip() for ln in value.splitlines()])
799
+
800
+
791
801
  class BaseBinder:
792
802
  parsed: BaseParser = None
793
803
 
@@ -21,6 +21,7 @@ from slm.parsing import (
21
21
  Finding,
22
22
  _Ignored,
23
23
  _Warning,
24
+ concat_str,
24
25
  remove_from_start,
25
26
  to_alignment,
26
27
  to_antenna,
@@ -116,7 +117,7 @@ def to_temp_stab(value):
116
117
  value.lower().replace(" ", "").replace("(", "").replace(")", "")
117
118
  == "degc+/-degc"
118
119
  ):
119
- return _Ignored, _Ignored, _Ignored
120
+ return _Ignored("Looks like a placeholder."), None, None
120
121
 
121
122
  if "yes" in value.lower() or "indoors" in value.lower():
122
123
  return _Warning(value=True, msg="Interpreted as 'stabilized'"), None, None
@@ -638,7 +639,7 @@ class SiteLogBinder(BaseBinder):
638
639
  for log_name, bindings in [
639
640
  ("Primary Data Center", ("primary", to_str)),
640
641
  ("Secondary Data Center", ("secondary", to_str)),
641
- ("URL for More Information", ("more_info", to_str)),
642
+ ("URL for More Information", ("more_info", concat_str)),
642
643
  ("Site Map", ("sitemap", to_str)),
643
644
  ("Site Diagram", ("site_diagram", to_str)),
644
645
  ("Horizon Mask", ("horizon_mask", to_str)),
slm/receivers/cache.py ADDED
@@ -0,0 +1,25 @@
1
+ from django.core.cache import cache
2
+ from django.dispatch import receiver
3
+
4
+ from slm import signals as slm_signals
5
+ from slm.defines import SiteFileUploadStatus
6
+
7
+
8
+ @receiver(slm_signals.site_published)
9
+ @receiver(slm_signals.site_file_published)
10
+ @receiver(slm_signals.site_file_unpublished)
11
+ def clear_default_cache(**_):
12
+ """
13
+ When events happen that change the public data of the SLM we clear our default cache.
14
+ """
15
+ cache.clear()
16
+
17
+
18
+ @receiver(slm_signals.site_file_deleted)
19
+ def clear_cache_if_published_file_deleted(sender, **kwargs):
20
+ """
21
+ If a site file attachment is deleted, only clear the caches if that file was published.
22
+ """
23
+ if file := kwargs.pop("upload", None):
24
+ if file.status is SiteFileUploadStatus.PUBLISHED:
25
+ cache.clear()
slm/settings/root.py CHANGED
@@ -19,6 +19,7 @@ from split_settings.tools import include, optional
19
19
  from slm.settings import env as settings_environment
20
20
  from slm.settings import (
21
21
  get_setting,
22
+ resource,
22
23
  set_default,
23
24
  slm_path_mk_dirs_must_exist,
24
25
  slm_path_must_exist,
@@ -60,9 +61,23 @@ SLM_IGS_VALIDATION = env(
60
61
  )
61
62
 
62
63
  SLM_ADMIN_MAP = env("SLM_ADMIN_MAP", bool, default=get_setting("SLM_ADMIN_MAP", True))
64
+ SLM_FILE_VIEWS = env(
65
+ "SLM_FILE_VIEWS", bool, default=get_setting("SLM_FILE_VIEWS", True)
66
+ )
63
67
  SLM_SITE_NAME = env("SLM_SITE_NAME", str, default=get_setting("SLM_SITE_NAME", ""))
64
68
  SLM_ORG_NAME = env("SLM_ORG_NAME", str, default=get_setting("SLM_ORG_NAME", "SLM"))
65
69
 
70
+ SLM_FILE_VIEW_FORMATS = env(
71
+ "SLM_FILE_VIEW_FORMATS",
72
+ list,
73
+ default=get_setting("SLM_FILE_VIEW_FORMATS", ["ASCII_9CHAR"]),
74
+ )
75
+ SLM_FILE_VIEW_FORMATS = env(
76
+ "SLM_FILE_VIEW_FORMATS",
77
+ list,
78
+ default=get_setting("SLM_FILE_VIEW_FORMATS", ["ASCII_9CHAR"]),
79
+ )
80
+
66
81
  # Quick-start development settings - unsuitable for production
67
82
  # See https://docs.djangoproject.com/en/stable/howto/deployment/checklist/
68
83
 
@@ -111,6 +126,8 @@ INSTALLED_APPS = set_default(
111
126
 
112
127
  if SLM_ADMIN_MAP:
113
128
  INSTALLED_APPS.insert(0, "slm.map")
129
+ if SLM_FILE_VIEWS:
130
+ INSTALLED_APPS.insert(0, "slm.file_views")
114
131
 
115
132
 
116
133
  SLM_DATABASE = env(
@@ -244,3 +261,8 @@ set_default(
244
261
  WSGI_APPLICATION = env(
245
262
  "WSGI_APPLICATION", default=get_setting("WSGI_APPLICATION", "slm.wsgi.application")
246
263
  )
264
+
265
+ if SLM_ADMIN_MAP or "slm.map" in INSTALLED_APPS:
266
+ include(resource("slm.map", "settings.py"))
267
+ if SLM_FILE_VIEWS or "slm.file_views" in INSTALLED_APPS:
268
+ include(resource("slm.file_views", "settings.py"))
slm/settings/routines.py CHANGED
@@ -27,8 +27,10 @@ routine(
27
27
  )
28
28
 
29
29
  command("deploy", "check", "--deploy")
30
+ command("deploy", "check_upgrade", "is-safe")
30
31
  command("deploy", "shellcompletion", "install", switches=["initial"])
31
32
  command("deploy", "migrate", priority=11)
33
+ command("deploy", "check_upgrade", "set-db-version")
32
34
  command("deploy", "renderstatic", priority=20)
33
35
  command("deploy", "collectstatic", "--no-input", priority=21)
34
36
  if get_setting("COMPRESS_OFFLINE", False) and get_setting("COMPRESS_ENABLED", False):
slm/settings/slm.py CHANGED
@@ -6,8 +6,10 @@ from django.utils.translation import gettext_lazy as _
6
6
 
7
7
  from slm.defines import (
8
8
  AlertLevel,
9
+ CoordinateMode,
9
10
  GeodesyMLVersion,
10
11
  SiteFileUploadStatus,
12
+ SiteLogFormat,
11
13
  SiteLogStatus,
12
14
  )
13
15
  from slm.settings import env as settings_environment
@@ -201,3 +203,59 @@ if SLM_IGS_STATION_NAMING:
201
203
  else:
202
204
  set_default("SLM_STATION_NAME_REGEX", None)
203
205
  set_default("SLM_STATION_NAME_HELP", _("The name of the station."))
206
+
207
+
208
+ # these settings control site log format precedence and naming
209
+ SLM_FORMAT_PRIORITY = {
210
+ SiteLogFormat(fmt): int(priority)
211
+ for fmt, priority in env(
212
+ "SLM_FORMAT_PRIORITY", dict, default=get_setting("SLM_FORMAT_PRIORITY", {})
213
+ )
214
+ }
215
+
216
+ priorities = SLM_FORMAT_PRIORITY or {fmt: fmt.value for fmt in SiteLogFormat}
217
+
218
+ SLM_DEFAULT_FORMAT = SiteLogFormat(
219
+ env(
220
+ "SLM_DEFAULT_FORMAT",
221
+ str,
222
+ max(priorities, key=priorities.get),
223
+ )
224
+ )
225
+
226
+ SLM_FORMAT_EXTENSIONS = {
227
+ **{fmt: fmt.ext for fmt in SiteLogFormat},
228
+ **{
229
+ SiteLogFormat(fmt): ext
230
+ for fmt, ext in env(
231
+ "SLM_FORMAT_EXTENSIONS",
232
+ dict,
233
+ default=get_setting(
234
+ "SLM_FORMAT_EXTENSIONS", {fmt: fmt.ext for fmt in SiteLogFormat}
235
+ ),
236
+ ).items()
237
+ },
238
+ }
239
+
240
+ # when logs are published, the following formats will be rendered to the site log index
241
+ SLM_ENABLED_FORMATS = [
242
+ SiteLogFormat(fmt)
243
+ for fmt in env(
244
+ "SLM_ENABLED_FORMATS",
245
+ list,
246
+ default=get_setting(
247
+ "SLM_ENABLED_FORMATS", [SiteLogFormat.ASCII_9CHAR, SiteLogFormat.GEODESY_ML]
248
+ ),
249
+ )
250
+ ]
251
+
252
+ if SLM_DEFAULT_FORMAT not in SLM_ENABLED_FORMATS:
253
+ SLM_ENABLED_FORMATS.insert(0, SLM_DEFAULT_FORMAT)
254
+
255
+
256
+ SLM_COORDINATE_MODE = CoordinateMode(
257
+ env(
258
+ "SLM_COORDINATE_MODE",
259
+ default=get_setting("SLM_COORDINATE_MODE", CoordinateMode.INDEPENDENT),
260
+ )
261
+ )
slm/settings/urls.py CHANGED
@@ -92,7 +92,7 @@ def bring_in_urls(urlpatterns):
92
92
  urlpatterns.insert(0, path("", include(url_module_str)))
93
93
 
94
94
  except ImportError:
95
- if app in {"slm", "slm.map", "network_map", "igs_ext"}:
95
+ if app in {"slm", "slm.map", "slm.file_views", "network_map", "igs_ext"}:
96
96
  raise
97
97
  pass
98
98
 
@@ -18,6 +18,7 @@ from slm.validators import (
18
18
  EnumValidator,
19
19
  FieldRequired,
20
20
  NonEmptyValidator,
21
+ PositionsMatchValidator,
21
22
  TimeRangeBookendValidator,
22
23
  TimeRangeValidator,
23
24
  VerifiedEquipmentValidator,
@@ -48,7 +49,7 @@ set_default(
48
49
  # 'sitemultipathsources',
49
50
  # 'sitesignalobstructions',
50
51
  # 'sitelocalepisodiceffects',
51
- "siteoperationalcontact",
52
+ # "siteoperationalcontact",
52
53
  # 'siteresponsibleagency',
53
54
  # 'sitemoreinformation'
54
55
  ],
@@ -68,8 +69,8 @@ set_default(
68
69
  "city": [FieldRequired()],
69
70
  "country": [FieldRequired(), EnumValidator()],
70
71
  "tectonic": [EnumValidator()],
71
- "xyz": [FieldRequired()],
72
- "llh": [FieldRequired()],
72
+ "xyz": [FieldRequired(), PositionsMatchValidator()],
73
+ "llh": [FieldRequired(), PositionsMatchValidator()],
73
74
  },
74
75
  "slm.SiteReceiver": {
75
76
  "receiver_type": [VerifiedEquipmentValidator(), ActiveEquipmentValidator()],
@@ -93,7 +94,7 @@ set_default(
93
94
  TimeRangeBookendValidator(),
94
95
  ],
95
96
  "marker_une": [FieldRequired(allow_legacy_nulls=True)],
96
- "alignment": [FieldRequired(allow_legacy_nulls=True)],
97
+ "alignment": [FieldRequired(allow_legacy_nulls=True, desired=True)],
97
98
  },
98
99
  "slm.SiteSurveyedLocalTies": {
99
100
  "name": [FieldRequired()],