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.
Files changed (149) hide show
  1. igs_slm-0.1.5b0.dist-info/METADATA +115 -0
  2. {igs_slm-0.1.2b0.dist-info → igs_slm-0.1.5b0.dist-info}/RECORD +192 -172
  3. {igs_slm-0.1.2b0.dist-info → igs_slm-0.1.5b0.dist-info}/WHEEL +1 -1
  4. igs_slm-0.1.5b0.dist-info/entry_points.txt +3 -0
  5. {igs_slm-0.1.2b0.dist-info → igs_slm-0.1.5b0.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 +10 -8
  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 }}/validation.py +29 -0
  19. slm/context.py +5 -0
  20. slm/defines/AlertLevel.py +10 -2
  21. slm/defines/AntennaCalibration.py +6 -4
  22. slm/defines/AntennaFeatures.py +12 -9
  23. slm/defines/AntennaReferencePoint.py +12 -10
  24. slm/defines/Aspiration.py +4 -2
  25. slm/defines/CardinalDirection.py +6 -4
  26. slm/defines/CollocationStatus.py +3 -1
  27. slm/defines/EquipmentState.py +6 -12
  28. slm/defines/FlagSeverity.py +4 -2
  29. slm/defines/FractureSpacing.py +7 -5
  30. slm/defines/FrequencyStandardType.py +6 -4
  31. slm/defines/GeodesyMLVersion.py +2 -0
  32. slm/defines/Instrumentation.py +9 -7
  33. slm/defines/LogEntryType.py +17 -15
  34. slm/defines/SLMFileType.py +4 -2
  35. slm/defines/SiteFileUploadStatus.py +7 -24
  36. slm/defines/SiteLogFormat.py +8 -32
  37. slm/defines/SiteLogStatus.py +9 -28
  38. slm/defines/TectonicPlates.py +18 -16
  39. slm/manage.py +24 -0
  40. slm/management/commands/check_upgrade.py +142 -0
  41. slm/management/commands/generate_sinex.py +110 -92
  42. slm/management/commands/head_from_index.py +11 -8
  43. slm/management/commands/import_archive.py +27 -18
  44. slm/management/commands/import_equipment.py +1 -1
  45. slm/management/commands/sitelog.py +1 -3
  46. slm/management/commands/synchronize.py +1 -1
  47. slm/management/commands/validate_db.py +6 -4
  48. slm/map/defines.py +18 -14
  49. slm/map/templates/slm/map.html +4 -6
  50. slm/migrations/0001_remove_archiveindex_no_overlapping_ranges_per_site_and_more.py +26 -0
  51. slm/migrations/0002_alter_archivedsitelog_file_and_more.py +44 -0
  52. slm/migrations/0003_alter_archivedsitelog_name_and_more.py +35 -0
  53. slm/migrations/0004_alter_site_name.py +24 -0
  54. slm/migrations/0005_slmversion.py +30 -0
  55. slm/migrations/0017_alter_logentry_unique_together_and_more.py +3 -1
  56. slm/migrations/0018_afix_deleted.py +3 -1
  57. slm/migrations/0018_alter_siteantenna_options_and_more.py +87 -56
  58. slm/migrations/0019_remove_siteantenna_marker_enu_siteantenna_marker_une_and_more.py +1 -1
  59. slm/migrations/0023_archivedsitelog_gml_version_and_more.py +1 -1
  60. slm/migrations/0031_alter_antenna_features.py +44 -0
  61. slm/migrations/0032_archiveindex_valid_range_and_more.py +84 -0
  62. slm/migrations/add_index_order_index.py +54 -0
  63. slm/migrations/normalize_index.py +147 -0
  64. slm/migrations/simplify_index.py +48 -0
  65. slm/migrations/verify_index.py +67 -0
  66. slm/models/__init__.py +2 -0
  67. slm/models/alerts.py +7 -10
  68. slm/models/data.py +1 -2
  69. slm/models/equipment.py +1 -1
  70. slm/models/fields.py +41 -0
  71. slm/models/index.py +183 -53
  72. slm/models/sitelog.py +35 -38
  73. slm/models/system.py +72 -31
  74. slm/models/user.py +1 -1
  75. slm/parsing/__init__.py +34 -17
  76. slm/parsing/legacy/binding.py +65 -34
  77. slm/parsing/legacy/parser.py +2 -2
  78. slm/parsing/xsd/binding.py +1 -1
  79. slm/parsing/xsd/parser.py +1 -2
  80. slm/receivers/__init__.py +2 -2
  81. slm/receivers/index.py +2 -1
  82. slm/receivers/migration.py +21 -0
  83. slm/settings/__init__.py +192 -4
  84. slm/settings/assets.py +26 -0
  85. slm/settings/auth.py +18 -14
  86. slm/settings/ckeditor.py +12 -6
  87. slm/settings/debug.py +2 -2
  88. slm/settings/emails.py +50 -0
  89. slm/settings/internationalization.py +8 -6
  90. slm/settings/logging.py +100 -88
  91. slm/settings/platform/darwin.py +16 -6
  92. slm/settings/rest.py +20 -15
  93. slm/settings/root.py +192 -98
  94. slm/settings/routines.py +5 -1
  95. slm/settings/secrets.py +20 -31
  96. slm/settings/security.py +7 -5
  97. slm/settings/slm.py +35 -16
  98. slm/settings/static_templates.py +12 -9
  99. slm/settings/templates.py +31 -25
  100. slm/settings/uploads.py +33 -5
  101. slm/settings/urls.py +1 -1
  102. slm/settings/validation.py +165 -165
  103. slm/signals.py +3 -2
  104. slm/static/slm/css/style.css +37 -36
  105. slm/static/slm/js/autocomplete.js +6 -4
  106. slm/static/slm/js/file_modal.js +62 -0
  107. slm/static/slm/js/form.js +3 -3
  108. slm/static/slm/js/formWidget.js +3 -3
  109. slm/static/slm/js/persistable.js +5 -1
  110. slm/templates/admin/base.html +1 -0
  111. slm/templates/rest_framework/base.html +23 -11
  112. slm/templates/slm/base.html +27 -22
  113. slm/templates/slm/forms/widgets/auto_complete.html +12 -11
  114. slm/templates/slm/forms/widgets/auto_complete_multiple.html +8 -7
  115. slm/templates/slm/station/download.html +6 -6
  116. slm/templates/slm/station/edit.html +9 -17
  117. slm/templates/slm/station/review.html +5 -3
  118. slm/templates/slm/station/upload.html +4 -1
  119. slm/templates/slm/station/uploads/legacy.html +1 -1
  120. slm/templates/slm/widgets/alert_scroll.html +4 -8
  121. slm/templates/slm/widgets/filelist.html +0 -5
  122. slm/templates/slm/widgets/log_scroll.html +2 -13
  123. slm/templates/slm/widgets/stationlist.html +2 -8
  124. slm/templatetags/slm.py +70 -9
  125. slm/utils.py +13 -4
  126. slm/validators.py +14 -14
  127. slm/views.py +6 -6
  128. slm/wsgi.py +16 -0
  129. igs_slm-0.1.2b0.dist-info/METADATA +0 -151
  130. igs_slm-0.1.2b0.dist-info/entry_points.txt +0 -3
  131. slm/bin/templates/{{ project_dir }}/sites/{{ site }}/validation.py +0 -11
  132. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/__init__.py +0 -0
  133. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/__init__.py +0 -0
  134. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/develop/local.py +0 -0
  135. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/develop/wsgi.py +0 -0
  136. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/manage.py +0 -0
  137. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/production/wsgi.py +0 -0
  138. /slm/bin/templates/{{ project_dir }}/{sites → src/sites}/{{ site }}/urls.py +0 -0
  139. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/__init__.py +0 -0
  140. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/admin.py +0 -0
  141. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/apps.py +0 -0
  142. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/__init__.py +0 -0
  143. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/commands/__init__.py +0 -0
  144. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/management/commands/import_archive.py +0 -0
  145. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/migrations/__init__.py +0 -0
  146. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/models.py +0 -0
  147. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/templates/slm/base.html +0 -0
  148. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/urls.py +0 -0
  149. /slm/bin/templates/{{ project_dir }}/{{{ extension_app }} → src/{{ extension_app }}}/views.py +0 -0
slm/models/system.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import os
2
+ import typing as t
2
3
  from datetime import datetime, timezone
3
4
  from io import BytesIO
4
5
  from logging import getLogger
6
+ from pathlib import Path
5
7
 
6
8
  from dateutil import parser
7
9
  from django.conf import settings
@@ -14,6 +16,8 @@ from django.urls import reverse
14
16
  from django.utils.timezone import is_naive, make_aware, now
15
17
  from django.utils.translation import gettext as _
16
18
  from django_enum import EnumField
19
+ from packaging.version import Version
20
+ from packaging.version import parse as parse_version
17
21
  from PIL import Image
18
22
  from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet
19
23
  from polymorphic.models import PolymorphicModel
@@ -27,9 +31,46 @@ from slm.defines import (
27
31
  SLMFileType,
28
32
  )
29
33
  from slm.models.sitelog import DefaultToStrEncoder
34
+ from slm.singleton import SingletonModel
30
35
  from slm.utils import get_exif_tags
31
36
 
32
37
 
38
+ def site_upload_path(instance: "SiteFile", filename: str) -> str:
39
+ """
40
+ file will be saved to:
41
+ MEDIA_ROOT/uploads/<site name>/filename
42
+
43
+ :param filename: The name of the file
44
+ :return: The path where the site file should reside.
45
+ """
46
+ from .index import ArchivedSiteLog
47
+
48
+ prefix = Path()
49
+ if instance.SUB_DIRECTORY:
50
+ prefix = Path(instance.SUB_DIRECTORY)
51
+ dest = prefix / instance.site.name / filename
52
+ timestamp = (
53
+ instance.index.begin
54
+ if isinstance(instance, ArchivedSiteLog)
55
+ else instance.timestamp
56
+ )
57
+ if (Path(settings.MEDIA_ROOT) / dest).exists():
58
+ stem, suffix = dest.stem, dest.suffix
59
+ dest = dest.with_name(f"{stem}_{timestamp.strftime('%H%M%S')}{suffix}")
60
+ return dest.as_posix()
61
+
62
+
63
+ def site_thumbnail_path(instance: "SiteFile", filename: str) -> str:
64
+ """
65
+ Return the path for the thumbnail image for the given filename.
66
+
67
+ :param filename: The name of the file
68
+ :return: The path where the thumbnail image should reside.
69
+ """
70
+ path = Path(instance.upload_path(filename))
71
+ return (path.parent / "thumbnails" / path.name).as_posix()
72
+
73
+
33
74
  class AgencyManager(models.Manager):
34
75
  pass
35
76
 
@@ -144,33 +185,6 @@ class Network(models.Model):
144
185
  return self.name
145
186
 
146
187
 
147
- def site_upload_path(instance, filename):
148
- """
149
- file will be saved to:
150
- MEDIA_ROOT/uploads/<site name>/filename
151
-
152
- :param instance: The SiteFile instance
153
- :param filename: The name of the file
154
- :return: The path where the site file should reside.
155
- """
156
- prefix = ""
157
- if instance.SUB_DIRECTORY:
158
- prefix = f"{instance.SUB_DIRECTORY}/"
159
- return f"{prefix}{instance.site.name}/{filename}"
160
-
161
-
162
- def site_thumbnail_path(instance, filename):
163
- """
164
- Return the path for the thumbnail image for the given filename.
165
-
166
- :param instance: The SiteFile instance
167
- :param filename: The name of the file
168
- :return: The path where the thumbnail image should reside.
169
- """
170
- parts = str(site_upload_path(instance, filename)).split("/")
171
- return "/".join([*parts[0:-1], "thumbnails", parts[-1]])
172
-
173
-
174
188
  class SiteFile(models.Model):
175
189
  SUB_DIRECTORY = "misc"
176
190
 
@@ -185,7 +199,9 @@ class SiteFile(models.Model):
185
199
  )
186
200
 
187
201
  timestamp = models.DateTimeField(
188
- auto_now_add=True, db_index=True, help_text=_("When the file was uploaded.")
202
+ auto_now_add=True,
203
+ db_index=True,
204
+ help_text=_("When the file was created or uploaded."),
189
205
  )
190
206
 
191
207
  file = models.FileField(
@@ -193,6 +209,7 @@ class SiteFile(models.Model):
193
209
  null=False,
194
210
  max_length=255,
195
211
  help_text=_("A pointer to the uploaded file on disk."),
212
+ unique=True,
196
213
  )
197
214
 
198
215
  size = models.PositiveIntegerField(null=True, default=None, blank=True)
@@ -480,7 +497,7 @@ class SiteFileUpload(SiteFile):
480
497
  blank=True,
481
498
  db_index=True,
482
499
  help_text=_(
483
- "The status of the file. This will also depend on what type the " "file is."
500
+ "The status of the file. This will also depend on what type the file is."
484
501
  ),
485
502
  )
486
503
 
@@ -655,9 +672,9 @@ class LogEntry(PolymorphicModel):
655
672
  def __str__(self):
656
673
  return (
657
674
  f"({self.site.name} | "
658
- f'{self.user.name or self.user.email if self.user else ""}) '
675
+ f"{self.user.name or self.user.email if self.user else ''}) "
659
676
  f"[{self.timestamp}]: {self.type} -> "
660
- f'{self.section or self.file or self.site or ""}'
677
+ f"{self.section or self.file or self.site or ''}"
661
678
  )
662
679
 
663
680
  class Meta:
@@ -721,3 +738,27 @@ class SiteTideGauge(models.Model):
721
738
 
722
739
  class Meta:
723
740
  ordering = ("site", "distance")
741
+
742
+
743
+ class SLMVersion(SingletonModel):
744
+ """
745
+ We store the SLM code version in the database to enable safe upgrades.
746
+ """
747
+
748
+ version_str = models.CharField(default="", blank=True)
749
+
750
+ @classmethod
751
+ def update(cls, version: t.Optional[Version] = None):
752
+ if not version:
753
+ from slm import __version__ as slm_version
754
+
755
+ version = parse_version(slm_version)
756
+ instance = cls.load()
757
+ instance.version_str = str(version)
758
+ instance.save()
759
+
760
+ @property
761
+ def version(self) -> t.Optional[Version]:
762
+ if self.version_str:
763
+ return parse_version(self.version_str)
764
+ return None
slm/models/user.py CHANGED
@@ -285,7 +285,7 @@ class User(AbstractBaseUser, PermissionsMixin):
285
285
 
286
286
  @property
287
287
  def full_name(self):
288
- return f'{self.first_name or ""} {self.last_name or ""}'.strip()
288
+ return f"{self.first_name or ''} {self.last_name or ''}".strip()
289
289
 
290
290
  def __str__(self):
291
291
  if self.name:
slm/parsing/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import re
2
+ import typing as t
2
3
  from dataclasses import dataclass
3
4
  from datetime import date, datetime, timezone
4
5
  from functools import partial
@@ -59,7 +60,8 @@ def remove_from_start(value: str, prefixes: List[str]):
59
60
 
60
61
  @dataclass
61
62
  class _Ignored:
62
- msg: str
63
+ msg: str = ""
64
+ columns: t.Optional[t.Tuple[int, int]] = None
63
65
 
64
66
 
65
67
  @dataclass
@@ -86,10 +88,24 @@ class Finding:
86
88
  A base class for parser/binding findings.
87
89
  """
88
90
 
91
+ lineno: int
92
+ parser: "BaseParser"
93
+ message: str
94
+ section: t.Optional["BaseSection"]
95
+ parameter: t.Optional["BaseParameter"]
96
+ line: t.Optional[str]
89
97
  priority: int = 0
98
+ columns: t.Optional[t.Tuple[int, int]]
90
99
 
91
100
  def __init__(
92
- self, lineno, parser, message, section=None, parameter=None, line=None
101
+ self,
102
+ lineno,
103
+ parser,
104
+ message,
105
+ section=None,
106
+ parameter=None,
107
+ line=None,
108
+ columns=None,
93
109
  ):
94
110
  self.lineno = lineno
95
111
  self.parser = parser
@@ -97,11 +113,12 @@ class Finding:
97
113
  self.section = section
98
114
  self.parameter = parameter
99
115
  self.line = line
116
+ self.columns = columns
100
117
 
101
118
  def __str__(self):
102
119
  return (
103
- f"({self.lineno+1: 4}) {self.parser.lines[self.lineno]}"
104
- f'{" " * (80-len(self.parser.lines[self.lineno]))}'
120
+ f"({self.lineno + 1: 4}) {self.parser.lines[self.lineno]}"
121
+ f"{' ' * (80 - len(self.parser.lines[self.lineno]))}"
105
122
  f"[{self.level.upper()}]: {self.message}"
106
123
  )
107
124
 
@@ -265,7 +282,7 @@ class BaseSection:
265
282
  section_str = f"{self.index_string} {self.header}\n"
266
283
  if self.example:
267
284
  section_str = f"{self.index_string} {self.header} (EXAMPLE)\n"
268
- for name, param in self.parameters.items():
285
+ for param in self.parameters.values():
269
286
  section_str += f"\t{param}\n"
270
287
  return section_str
271
288
 
@@ -275,7 +292,7 @@ class BaseSection:
275
292
  True if any parameter in this section contains a real value that is
276
293
  not a placeholder.
277
294
  """
278
- for name, param in self.parameters.items():
295
+ for param in self.parameters.values():
279
296
  if not (param.is_empty or param.is_placeholder):
280
297
  return True
281
298
  return False
@@ -290,7 +307,7 @@ class BaseSection:
290
307
  else:
291
308
  index += "x"
292
309
  if self.subsection_number and (self.order or self.example):
293
- index += f'.{self.order if self.order else "x"}'
310
+ index += f".{self.order if self.order else 'x'}"
294
311
  return index
295
312
 
296
313
  @property
@@ -350,7 +367,7 @@ class BaseParser:
350
367
  @property
351
368
  def findings_context(self) -> Dict[int, Tuple[str, str]]:
352
369
  return {
353
- int(line): (finding.level, finding.message)
370
+ int(line): (finding.level, finding.message, finding.columns)
354
371
  for line, finding in self.findings.items()
355
372
  }
356
373
 
@@ -476,7 +493,7 @@ def to_antenna(value):
476
493
  except Antenna.DoesNotExist:
477
494
  antennas = "\n".join([ant.model for ant in Antenna.objects.public()])
478
495
  raise ValueError(
479
- f"Unexpected antenna model {antenna}. Must be one of " f"\n{antennas}"
496
+ f"Unexpected antenna model {antenna}. Must be one of \n{antennas}"
480
497
  )
481
498
 
482
499
 
@@ -487,7 +504,7 @@ def to_radome(value):
487
504
  except Radome.DoesNotExist:
488
505
  radomes = "\n".join([rad.model for rad in Radome.objects.public()])
489
506
  raise ValueError(
490
- f"Unexpected radome model {radome}. Must be one of " f"\n{radomes}"
507
+ f"Unexpected radome model {radome}. Must be one of \n{radomes}"
491
508
  )
492
509
 
493
510
 
@@ -498,7 +515,7 @@ def to_receiver(value):
498
515
  except Receiver.DoesNotExist:
499
516
  receivers = "\n".join([rec.model for rec in Receiver.objects.public()])
500
517
  raise ValueError(
501
- f"Unexpected receiver model {receiver}. Must be one of " f"\n{receivers}"
518
+ f"Unexpected receiver model {receiver}. Must be one of \n{receivers}"
502
519
  )
503
520
 
504
521
 
@@ -546,7 +563,7 @@ def to_enum(enum_cls, value, strict=False, blank=None, ignored=None):
546
563
  return value.strip()
547
564
 
548
565
  valid_list = " \n".join(en.label for en in enum_cls)
549
- raise ValueError(f"Invalid value {value} must be one of:\n" f"{valid_list}")
566
+ raise ValueError(f"Invalid value {value} must be one of:\n{valid_list}")
550
567
  return blank
551
568
 
552
569
 
@@ -665,14 +682,14 @@ def to_decimal_degrees(value):
665
682
  flt = to_float(value)
666
683
  if isinstance(flt, _Warning):
667
684
  return _Warning(msg=flt.msg, value=dddmmssss_to_decimal(flt.value))
668
- if flt is _Ignored or isinstance(flt, _Ignored):
685
+ if isinstance(flt, _Ignored):
669
686
  return flt
670
687
  return dddmmssss_to_decimal(to_float(value))
671
688
 
672
689
 
673
690
  def to_int(value, units=None, prefixes=None):
674
691
  try:
675
- to_numeric(numeric_type=int, value=value, units=units, prefixes=prefixes)
692
+ return to_numeric(numeric_type=int, value=value, units=units, prefixes=prefixes)
676
693
  except ValueError:
677
694
  from_float = to_numeric(
678
695
  numeric_type=float, value=value, units=units, prefixes=prefixes
@@ -718,12 +735,12 @@ def to_pressure(value):
718
735
  def _to_date(value):
719
736
  if value.strip():
720
737
  if "CCYY-MM-DD" in value.upper():
721
- return _Ignored
738
+ return _Ignored(msg=f"{value} is a placeholder.")
722
739
  try:
723
740
  return parse_date(value, tzinfos=TZ_INFOS).date()
724
741
  except Exception as exc:
725
742
  raise ValueError(
726
- f"Unable to parse {value} into a date. Expected " f"format: CCYY-MM-DD"
743
+ f"Unable to parse {value} into a date. Expected format: CCYY-MM-DD"
727
744
  ) from exc
728
745
  return None
729
746
 
@@ -731,7 +748,7 @@ def _to_date(value):
731
748
  def _to_datetime(value):
732
749
  if value.strip():
733
750
  if "CCYY-MM-DD" in value.upper():
734
- return _Ignored
751
+ return _Ignored(msg=f"{value} is a placeholder.")
735
752
  try:
736
753
  # UT and UTUT has been seen as a timezone specifier in the wild
737
754
  dt = parse_date(value, tzinfos=TZ_INFOS)
@@ -71,7 +71,7 @@ def reg(name, header_index, bindings):
71
71
  def ignored(_, msg=""):
72
72
  if msg:
73
73
  return _Ignored(msg)
74
- return _Ignored
74
+ return _Ignored()
75
75
 
76
76
 
77
77
  def to_temp_stab(value):
@@ -129,40 +129,33 @@ def to_temp_stab(value):
129
129
  return stabilized, nominal, deviation
130
130
 
131
131
 
132
- def effective_start(value):
132
+ def effective_date(value: str, part: int, label: str):
133
133
  try:
134
- start_str = ""
134
+ dt_str = ""
135
135
  if value.strip():
136
136
  sep = "/" if "/" in value else " - "
137
- start_str = value.split(sep)[0]
138
- return to_date(start_str)
137
+ splt = value.split(sep)
138
+ if len(splt) > part:
139
+ dt_str = splt[part]
140
+ if dt_str.strip():
141
+ ret = to_date(dt_str.strip())
142
+ if isinstance(ret, _Ignored):
143
+ start = value.index(dt_str)
144
+ end = start + len(dt_str)
145
+ ret.columns = (start, end)
146
+ return ret
139
147
  return None
140
148
  except ValueError as ve:
141
- if start_str.upper() in DATE_PLACEHOLDERS:
149
+ if dt_str.upper() in DATE_PLACEHOLDERS:
142
150
  return None
143
151
  raise ValueError(
144
- f"Unable to parse {value} into an expected start date. Expected "
152
+ f"Unable to parse {value} into an expected {label} date. Expected "
145
153
  f"format: CCYY-MM-DD/CCYY-MM-DD"
146
154
  ) from ve
147
155
 
148
156
 
149
- def effective_end(value):
150
- try:
151
- end_str = ""
152
- if value.strip():
153
- sep = "/" if "/" in value else " - "
154
- splt = value.split(sep)
155
- if len(splt) > 1:
156
- end_str = value.split(sep)[1]
157
- return to_date(end_str)
158
- return None
159
- except ValueError as ve:
160
- if end_str.upper() in DATE_PLACEHOLDERS:
161
- return None
162
- raise ValueError(
163
- f"Unable to parse {value} into an expected end date. Expected "
164
- f"format: CCYY-MM-DD/CCYY-MM-DD"
165
- ) from ve
157
+ effective_start = partial(effective_date, part=0, label="start")
158
+ effective_end = partial(effective_date, part=1, label="end")
166
159
 
167
160
 
168
161
  def no_sat_warning(line_no, parser, satellites):
@@ -462,12 +455,12 @@ class SiteLogBinder(BaseBinder):
462
455
  ("Tied Marker Usage", ("usage", to_str)),
463
456
  ("Tied Marker CDP Number", ("cdp_number", to_str)),
464
457
  ("Tied Marker DOMES Number", ("domes_number", to_str)),
465
- ("dx", ("dx", to_float)),
466
- ("dy", ("dy", to_float)),
467
- ("dz", ("dz", to_float)),
468
- ("dx (m)", ("dx", to_float)),
469
- ("dy (m)", ("dy", to_float)),
470
- ("dz (m)", ("dz", to_float)),
458
+ ("dx", ("dx", partial(to_float, units=["m"]))),
459
+ ("dy", ("dy", partial(to_float, units=["m"]))),
460
+ ("dz", ("dz", partial(to_float, units=["m"]))),
461
+ ("dx (m)", ("dx", partial(to_float, units=["m"]))),
462
+ ("dy (m)", ("dy", partial(to_float, units=["m"]))),
463
+ ("dz (m)", ("dz", partial(to_float, units=["m"]))),
471
464
  (
472
465
  "Accuracy",
473
466
  (
@@ -656,13 +649,43 @@ class SiteLogBinder(BaseBinder):
656
649
  },
657
650
  }
658
651
 
652
+ MULTIPLE_ENTRIES = {
653
+ 3,
654
+ 4,
655
+ 5,
656
+ 6,
657
+ 7,
658
+ (8, 1),
659
+ (8, 2),
660
+ (8, 3),
661
+ (8, 4),
662
+ (8, 5),
663
+ (9, 1),
664
+ (9, 2),
665
+ (9, 3),
666
+ 10,
667
+ }
668
+
659
669
  def __init__(self, parsed: SiteLogParser):
660
670
  super().__init__(parsed)
661
- for _1, section in self.parsed.sections.items():
662
- if section.example or not section.contains_values:
671
+ for section in self.parsed.sections.values():
672
+ if section.example:
673
+ continue
674
+ section_depth = sum(1 for item in section.index_tuple if item is not None)
675
+ heading_depth = (
676
+ 1
677
+ if not isinstance(section.heading_index, tuple)
678
+ else len(section.heading_index)
679
+ )
680
+ is_header = (
681
+ section.heading_index in self.MULTIPLE_ENTRIES
682
+ and section_depth == heading_depth
683
+ )
684
+ if is_header and not section.contains_values:
685
+ # skip empty headers
663
686
  continue
664
687
  if section.heading_index not in self.TRANSLATION_TABLE:
665
- for line_no in range(section.line_no, section.line_end):
688
+ for _ in range(section.line_no, section.line_end):
666
689
  self.parsed.add_finding(
667
690
  Warn(
668
691
  section.line_no,
@@ -738,16 +761,24 @@ class SiteLogBinder(BaseBinder):
738
761
  if parameter.is_placeholder
739
762
  else parse(parameter.value)
740
763
  )
741
- if value == _Ignored or isinstance(value, _Ignored):
764
+ if isinstance(value, _Ignored):
765
+ cols = None
766
+ if cols := getattr(value, "columns", None):
767
+ val_start = self.lines[parameter.line_no].index(
768
+ parameter.value
769
+ )
770
+ cols = (cols[0] + val_start, cols[1] + val_start)
742
771
  self.parsed.add_finding(
743
772
  Ignored(
744
773
  parameter.line_no,
745
774
  self.parsed,
746
775
  getattr(value, "msg", _("Parameter is ignored")),
747
776
  section=section,
777
+ columns=cols,
748
778
  )
749
779
  )
750
780
  ignored.add(param)
781
+ parameter.bind(param, None)
751
782
  elif isinstance(value, _Warning):
752
783
  self.parsed.add_finding(
753
784
  Warn(
@@ -52,8 +52,8 @@ class ParsedParameter(BaseParameter):
52
52
  super().__init__(
53
53
  line_no=line_no,
54
54
  name=(
55
- f'{sub_heading if sub_heading else ""}'
56
- f'{"::" if sub_heading else ""}'
55
+ f"{sub_heading if sub_heading else ''}"
56
+ f"{'::' if sub_heading else ''}"
57
57
  f"{match.group(1).strip()}"
58
58
  ),
59
59
  values=[match.group(2).strip()],
@@ -36,7 +36,7 @@ class SiteLogBinder(BaseBinder):
36
36
  TRANSLATION_TABLE[GeodesyMLVersion.v0_5] = {
37
37
  (
38
38
  (0,),
39
- "/geo:GeodesyML/geo:siteLog/geo:formInformation/" "geo:FormInformation",
39
+ "/geo:GeodesyML/geo:siteLog/geo:formInformation/geo:FormInformation",
40
40
  ): TRANSLATION_TABLE[GeodesyMLVersion.v0_4][
41
41
  ((0,), "/geo:GeodesyML/geo:siteLog/geo:formInformation")
42
42
  ],
slm/parsing/xsd/parser.py CHANGED
@@ -99,8 +99,7 @@ class SiteLogParser(BaseParser):
99
99
  Error(
100
100
  self.doc.sourceline - 1,
101
101
  self,
102
- f"Unsupported schema: "
103
- f"{self.doc.nsmap.get(self.doc.prefix)}",
102
+ f"Unsupported schema: {self.doc.nsmap.get(self.doc.prefix)}",
104
103
  )
105
104
  )
106
105
 
slm/receivers/__init__.py CHANGED
@@ -6,6 +6,6 @@ def register():
6
6
  if not _registered:
7
7
  # the order of these imports is important, index receivers must happen
8
8
  # before alert receivers
9
- from slm.receivers import alerts, cleanup, event_loggers, index
9
+ from slm.receivers import alerts, cleanup, event_loggers, index, migration
10
10
 
11
- _registered = event_loggers and cleanup and index and alerts
11
+ _registered = event_loggers and cleanup and index and alerts and migration
slm/receivers/index.py CHANGED
@@ -5,7 +5,7 @@ from slm.defines import SiteLogStatus
5
5
 
6
6
 
7
7
  @receiver(slm_signals.site_status_changed)
8
- def index_site(sender, site, previous_status, new_status, **kwargs):
8
+ def index_site(sender, site, previous_status, new_status, reverted=False, **kwargs):
9
9
  from slm.models import ArchiveIndex
10
10
 
11
11
  if site.last_publish and (
@@ -18,6 +18,7 @@ def index_site(sender, site, previous_status, new_status, **kwargs):
18
18
  and previous_status in SiteLogStatus.active_states()
19
19
  and previous_status is not SiteLogStatus.PUBLISHED
20
20
  and new_status is SiteLogStatus.PUBLISHED
21
+ and not reverted
21
22
  ):
22
23
  # catch an edge case where a section publish triggers a whole log publish
23
24
  # these signals/edit state diagram needs to be cleaned up. this code is
@@ -0,0 +1,21 @@
1
+ """
2
+ We update our SLMVersion tag when migrate is run - which should happen everytime the
3
+ SLM software is updated.
4
+ """
5
+
6
+ from django.db.models.signals import post_migrate, pre_migrate
7
+ from django.dispatch import receiver
8
+
9
+ from slm.models import SLMVersion
10
+
11
+
12
+ @receiver(pre_migrate)
13
+ def check_safe_upgrade(**_):
14
+ from slm.management.commands.check_upgrade import Command as CheckUpgrade
15
+
16
+ CheckUpgrade()
17
+
18
+
19
+ @receiver(post_migrate)
20
+ def update_slm_version(**_):
21
+ SLMVersion.update()