igs-slm 0.1.5b3__py3-none-any.whl → 0.2.0b0__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.0b0.dist-info}/METADATA +2 -2
  2. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b0.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 +11 -11
  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 +1 -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.0b0.dist-info}/WHEEL +0 -0
  46. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b0.dist-info}/entry_points.txt +0 -0
  47. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,253 @@
1
+ import inspect
2
+ import typing as t
3
+ from abc import abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from django.urls import path
9
+ from django.urls.resolvers import URLPattern
10
+ from django.utils.module_loading import import_string
11
+
12
+ from slm.defines import SiteLogFormat, SiteLogStatus
13
+
14
+
15
+ @dataclass
16
+ class Listing:
17
+ """
18
+ This is the interface for a file/directory listing that the template expects to render.
19
+ """
20
+
21
+ display: str
22
+ """
23
+ The name to display.
24
+ """
25
+
26
+ modified: t.Optional[datetime] = None
27
+ """
28
+ The last time this file or directory was modified.
29
+ """
30
+
31
+ size: t.Optional[int] = None
32
+ """
33
+ The size in bytes of the file if available.
34
+ """
35
+
36
+ is_dir: bool = False
37
+ """
38
+ True if this listing is a directory, False otherwise
39
+ """
40
+
41
+ on_disk: t.Optional[Path] = None
42
+ """
43
+ The path to the file or directory on disk.
44
+ """
45
+
46
+ def from_glob(
47
+ pattern: t.Optional[str], filter: t.Callable[[str], bool] = lambda _: True
48
+ ) -> t.Generator["Listing", None, None]:
49
+ """
50
+ Yield :class:`~slm.file_views.config.Listing` objects from a glob
51
+ pattern.
52
+
53
+ :param pattern: A glob pattern
54
+ :param filter: A callable that takes the name of the file or directory
55
+ and returns True if it should be included and False otherwise. Includes
56
+ everything by default.
57
+ :return: An iterable of :class:`~slm.file_views.config.Listing`
58
+ objects.
59
+ """
60
+ if pattern:
61
+ from glob import iglob
62
+
63
+ for path_str in iglob(pattern):
64
+ on_disk = Path(path_str)
65
+ stat = on_disk.stat()
66
+ if filter(on_disk.name):
67
+ yield Listing(
68
+ display=on_disk.name,
69
+ modified=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc),
70
+ size=None if on_disk.is_dir() else stat.st_size,
71
+ is_dir=on_disk.is_dir(),
72
+ on_disk=on_disk,
73
+ )
74
+
75
+
76
+ @dataclass
77
+ class Entry:
78
+ view: str
79
+ """
80
+ The import path to the view function or class.
81
+ """
82
+
83
+ download: t.Optional[bool] = None
84
+ """
85
+ If true, clicking on a file will download it, otherwise it will be opened in the browser
86
+ if it is a supported mime type.
87
+ """
88
+
89
+ name: t.Optional[str] = None
90
+ """
91
+ The name used to reverse the view. By default the name will be the path parts separated
92
+ by __
93
+ """
94
+
95
+ decorator: t.Optional[t.Callable[..., t.Any]] = None
96
+ """
97
+ A decorator to apply to the view. For example, to require login to access this
98
+ view set this to :func:`~django.contrib.auth.decorators.login_required`
99
+ """
100
+
101
+ options: t.Dict[str, t.Any] = field(default_factory=dict)
102
+ """
103
+ Extra kwargs to pass to path().
104
+ """
105
+
106
+ @abstractmethod
107
+ def urls(self, pth: Path, **kwargs) -> t.Sequence[URLPattern]: ...
108
+
109
+ def kwargs(self, **kwargs) -> t.Dict[str, t.Any]:
110
+ from django.conf import settings
111
+
112
+ return {
113
+ **self.options,
114
+ "download": self.download
115
+ if self.download is not None
116
+ else getattr(settings, "SLM_FILE_VIEW_DOWNLOAD", False),
117
+ **kwargs,
118
+ }
119
+
120
+ @property
121
+ def view_callback(self) -> t.Callable[..., t.Any]:
122
+ view = import_string(self.view)
123
+ view = view.as_view() if inspect.isclass(view) else view
124
+ if self.decorator:
125
+ return self.decorator(view)
126
+ return view
127
+
128
+ def view_name(self, pth: Path) -> str:
129
+ return self.name or self.path_str(pth).replace("/", "__")
130
+
131
+ @staticmethod
132
+ def path_str(pth: Path) -> str:
133
+ return f"{'/'.join(part for part in pth.parts if part != pth.root)}/"
134
+
135
+
136
+ @dataclass
137
+ class Directory(Entry):
138
+ order_key: t.Sequence[str] = ("is_dir", "display")
139
+
140
+ @abstractmethod
141
+ def urls(self, pth: Path, **kwargs) -> t.Sequence[URLPattern]: ...
142
+
143
+
144
+ @dataclass
145
+ class FileSystemDirectory(Directory):
146
+ view: str = "slm.file_views.views.FileSystemView"
147
+ glob: t.Optional[str] = None
148
+
149
+ def kwargs(self, **kwargs) -> t.Dict[str, t.Any]:
150
+ return {**super().kwargs(), "glob": self.glob, **kwargs}
151
+
152
+ def urls(self, pth: Path, **kwargs) -> t.Sequence[URLPattern]:
153
+ path_str = self.path_str(pth)
154
+ return (
155
+ path(
156
+ path_str,
157
+ self.view_callback,
158
+ kwargs=self.kwargs(**kwargs),
159
+ name=self.view_name(pth),
160
+ ),
161
+ path(
162
+ f"{path_str}<str:filename>",
163
+ self.view_callback,
164
+ kwargs=self.kwargs(**kwargs),
165
+ name=self.view_name(pth),
166
+ ),
167
+ )
168
+
169
+
170
+ @dataclass
171
+ class ArchivedSiteLogs(FileSystemDirectory):
172
+ view: str = "slm.file_views.views.ArchivedSiteLogView"
173
+
174
+ log_formats: t.Sequence[SiteLogFormat] = field(default_factory=list)
175
+ log_status: t.Sequence[SiteLogStatus] = field(
176
+ default_factory=SiteLogStatus.active_states
177
+ )
178
+ best_format: bool = False
179
+ most_recent: bool = False
180
+ non_current: bool = False
181
+
182
+ name_len: t.Optional[int] = None
183
+ lower_case: t.Optional[bool] = None
184
+
185
+ def kwargs(self, **kwargs) -> t.Dict[str, t.Any]:
186
+ return {
187
+ **super().kwargs(),
188
+ "log_formats": self.log_formats,
189
+ "log_status": self.log_status,
190
+ "best_format": self.best_format,
191
+ "most_recent": self.most_recent,
192
+ "non_current": self.non_current,
193
+ "name_len": self.name_len,
194
+ "lower_case": self.lower_case,
195
+ **kwargs,
196
+ }
197
+
198
+
199
+ class File(Entry):
200
+ """
201
+ All file types must inherit from this class.
202
+ """
203
+
204
+ pass
205
+
206
+
207
+ @dataclass
208
+ class _Command:
209
+ command: str
210
+ """
211
+ The name of the management command that generates the file. This command
212
+ must write the file to standard out.
213
+ """
214
+
215
+ args: t.List[str] = field(default_factory=list)
216
+ """
217
+ CLI string arguments to pass to the command.
218
+ """
219
+
220
+
221
+ @dataclass
222
+ class GeneratedFile(File, _Command):
223
+ """
224
+ A view that generates a file from a management command.
225
+ """
226
+
227
+ view: str = "slm.file_views.views.command_output_view"
228
+ mimetype: t.Optional[str] = None
229
+ """
230
+ Give an explicit mime type for the generated file.
231
+ """
232
+
233
+ def path_str(self, pth: Path) -> str:
234
+ return super().path_str(pth).rstrip("/")
235
+
236
+ def kwargs(self, **kwargs) -> t.Dict[str, t.Any]:
237
+ return {
238
+ **super().kwargs(),
239
+ "command": self.command,
240
+ "mimetype": self.mimetype,
241
+ "args": self.args,
242
+ **kwargs,
243
+ }
244
+
245
+ def urls(self, pth: Path, **kwargs) -> t.Sequence[URLPattern]:
246
+ return (
247
+ path(
248
+ self.path_str(pth),
249
+ self.view_callback,
250
+ kwargs=self.kwargs(**kwargs),
251
+ name=self.view_name(pth),
252
+ ),
253
+ )
@@ -0,0 +1,124 @@
1
+ from pathlib import Path
2
+
3
+ from slm.defines import SiteLogFormat, SiteLogStatus
4
+ from slm.file_views.config import ArchivedSiteLogs, GeneratedFile
5
+ from slm.settings import env as settings_environment
6
+ from slm.settings import get_setting
7
+
8
+ env = settings_environment()
9
+
10
+ SLM_SINEX_FILENAME = env(
11
+ "SLM_SINEX_FILENAME",
12
+ str,
13
+ default=get_setting(
14
+ "SLM_SINEX_FILENAME",
15
+ get_setting("SLM_ORG_NAME", "stations").lower().replace(" ", "_"),
16
+ ),
17
+ )
18
+ if SLM_SINEX_FILENAME and "." not in SLM_SINEX_FILENAME:
19
+ SLM_SINEX_FILENAME = f"{SLM_SINEX_FILENAME}.snx"
20
+
21
+ SLM_FILE_VIEW_STRUCTURE = get_setting(
22
+ "SLM_FILE_VIEW_STRUCTURE",
23
+ [
24
+ (
25
+ "archive",
26
+ ArchivedSiteLogs(
27
+ order_key=("index__site__name", "timestamp"),
28
+ best_format=True,
29
+ non_current=True,
30
+ ),
31
+ ),
32
+ (
33
+ "current",
34
+ [
35
+ (
36
+ "log",
37
+ ArchivedSiteLogs(
38
+ most_recent=True,
39
+ best_format=True,
40
+ log_formats={SiteLogFormat.LEGACY, SiteLogFormat.ASCII_9CHAR},
41
+ ),
42
+ ),
43
+ (
44
+ "xml",
45
+ ArchivedSiteLogs(
46
+ most_recent=True,
47
+ best_format=True,
48
+ log_formats={SiteLogFormat.GEODESY_ML},
49
+ ),
50
+ ),
51
+ ],
52
+ ),
53
+ (
54
+ "former",
55
+ ArchivedSiteLogs(
56
+ best_format=True,
57
+ most_recent=True,
58
+ log_status=[SiteLogStatus.FORMER],
59
+ log_formats={SiteLogFormat.LEGACY, SiteLogFormat.ASCII_9CHAR},
60
+ ),
61
+ ),
62
+ (SLM_SINEX_FILENAME, GeneratedFile("generate_sinex", mimetype="text/plain"))
63
+ if SLM_SINEX_FILENAME
64
+ else None,
65
+ ],
66
+ )
67
+
68
+ # should clicking the file links in the file view download the file or open it in the browser?
69
+ SLM_FILE_VIEW_DOWNLOAD = env(
70
+ "SLM_FILE_VIEW_DOWNLOAD", bool, default=get_setting("SLM_FILE_VIEW_DOWNLOAD", False)
71
+ )
72
+
73
+ SLM_FILE_VIEW_ROOT = Path(
74
+ env(
75
+ "SLM_FILE_VIEW_ROOT",
76
+ str,
77
+ default=get_setting("SLM_FILE_VIEW_ROOT", Path("files")),
78
+ )
79
+ )
80
+
81
+
82
+ BROWSER_RENDERABLE_MIMETYPES = get_setting(
83
+ "BROWSER_RENDERABLE_MIMETYPES",
84
+ {
85
+ "text/html",
86
+ "application/xhtml+xml",
87
+ "text/css",
88
+ "text/javascript",
89
+ "application/javascript",
90
+ "application/ecmascript",
91
+ "text/ecmascript",
92
+ "image/apng",
93
+ "image/avif",
94
+ "image/gif",
95
+ "image/jpeg",
96
+ "image/png",
97
+ "image/svg+xml",
98
+ "image/webp",
99
+ "image/bmp",
100
+ "image/x-icon",
101
+ "image/vnd.microsoft.icon",
102
+ "audio/midi",
103
+ "audio/x-midi",
104
+ "audio/mpeg",
105
+ "audio/ogg",
106
+ "audio/wav",
107
+ "audio/wave",
108
+ "audio/x-wav",
109
+ "audio/webm",
110
+ "audio/aac",
111
+ "video/mp4",
112
+ "video/mpeg",
113
+ "video/ogg",
114
+ "video/webm",
115
+ "video/x-msvideo",
116
+ "video/quicktime",
117
+ "application/xml",
118
+ "text/xml",
119
+ "application/rss+xml",
120
+ "application/atom+xml",
121
+ "application/pdf",
122
+ "text/plain",
123
+ },
124
+ )
@@ -0,0 +1,82 @@
1
+ body {
2
+ font-family: 'Courier New', monospace;
3
+ margin: 20px;
4
+ background-color: #f8f9fa;
5
+ }
6
+
7
+ table {
8
+ width: 100%;
9
+ max-width: 768px;
10
+ border-collapse: collapse;
11
+ font-size: 14px;
12
+ }
13
+
14
+ th {
15
+ background-color: #f1f3f4;
16
+ padding: 8px 12px;
17
+ text-align: left;
18
+ border-bottom: 2px solid #ddd;
19
+ font-weight: normal;
20
+ }
21
+
22
+ td {
23
+ padding: 4px 12px;
24
+ border-bottom: 1px solid #eee;
25
+ vertical-align: top;
26
+ }
27
+
28
+ tr:hover {
29
+ background-color: #f8f9fa;
30
+ }
31
+
32
+ .name-column {
33
+ width: 55%;
34
+ }
35
+
36
+ .modified-column {
37
+ width: 30%;
38
+ white-space: nowrap;
39
+ }
40
+
41
+ .size-column {
42
+ width: 15%;
43
+ text-align: right;
44
+ }
45
+
46
+ .parent-directory {
47
+ font-weight: bold;
48
+ }
49
+
50
+ a {
51
+ text-decoration: none;
52
+ }
53
+
54
+ a:hover {
55
+ text-decoration: underline;
56
+ }
57
+
58
+ .file-link {
59
+ text-decoration: none;
60
+ color: #1a73e8;
61
+ }
62
+
63
+ .file-link:hover {
64
+ text-decoration: underline;
65
+ }
66
+
67
+ .icon {
68
+ margin-right: 8px;
69
+ display: inline-block;
70
+ width: 16px;
71
+ }
72
+ #banner {
73
+ width: 768px;
74
+ margin-bottom: 5px;
75
+ }
76
+
77
+ #footer {
78
+ width: 768px;
79
+ height: 32px;
80
+ background-color: #5B98AF;
81
+ margin-top: 5px;
82
+ }
@@ -0,0 +1,70 @@
1
+ {% load static slm %}
2
+ <html xmlns="http://www.w3.org/1999/xhtml">
3
+ {% block head %}
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
6
+ <title>{{ title|default:"Directory Listing" }}</title>
7
+ <link href="{% static 'slm/file_views/css/listing.css' %}"
8
+ rel="stylesheet"
9
+ type="text/css">
10
+ </head>
11
+ {% endblock head %}
12
+ <body>
13
+ {% block banner %}
14
+ <img id="banner" src="{% static 'slm/file_views/banner_header.png' %}">
15
+ {% endblock banner %}
16
+ {% block listing %}
17
+ <div class="file-listing">
18
+ <table>
19
+ <thead>
20
+ <tr>
21
+ <th></th>
22
+ <th class="name-column">
23
+ <a href=".?C=N;O={{ N_ordering|default:'A' }}">Name</a>
24
+ </th>
25
+ <th class="modified-column">
26
+ <a href=".?C=M;O={{ M_ordering|default:'A' }}">Last modified</a>
27
+ </th>
28
+ <th class="size-column">
29
+ <a href=".?C=S;O={{ S_ordering|default:'A' }}">Size</a>
30
+ </th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ {% if parent %}
35
+ <tr>
36
+ <td class="icon-column">
37
+ <span class="icon">↩️</span>
38
+ </td>
39
+ <td class="name-column">
40
+ <a href="{{ parent }}" class="file-link parent-directory">Parent Directory</a>
41
+ </td>
42
+ <td class="modified-column">-</td>
43
+ <td class="size-column">-</td>
44
+ </tr>
45
+ {% endif %}
46
+ {% for listing in listings %}
47
+ <tr>
48
+ <td class="icon-column">
49
+ <span class="icon">{% if listing.is_dir %}📁{% else %}📄{% endif %}</span>
50
+ </td>
51
+ <td class="name-column">
52
+ <a href="{{listing.display}}{% if listing.is_dir %}/{% endif %}" {% if download and listing.is_dir %}download{% endif %}>{{listing.display}}</a>
53
+ </td>
54
+ <td class="modified-column">{{ listing.modified|simple_utc }}</td>
55
+ <td class="size-column">{% if listing.size %}{% widthratio listing.size 1024 1 %}K{% else %}-{% endif %}</td>
56
+ </tr>
57
+ {% endfor %}
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ {% endblock listing %}
62
+ {% block footer %}
63
+ <div id="wrapper_bottom">
64
+ <div id="footer">
65
+ <div class="footer_nav"></div>
66
+ </div>
67
+ </div>
68
+ {% endblock footer %}
69
+ </body>
70
+ </html>
slm/file_views/urls.py ADDED
@@ -0,0 +1,47 @@
1
+ import typing as t
2
+ from pathlib import Path
3
+
4
+ from django.conf import settings
5
+ from django.core.exceptions import ImproperlyConfigured
6
+ from django.urls import path
7
+
8
+ from .config import Entry, File, Listing
9
+ from .views import FileSystemView
10
+
11
+ SLM_INCLUDE = True
12
+
13
+ app_name = "slm_file_views"
14
+
15
+ urlpatterns = []
16
+
17
+ Branch = t.Sequence[t.Union[t.Tuple[str, t.Union[Entry, "Branch"]]]]
18
+
19
+
20
+ def add_paths(url_path: Path, entries: Branch):
21
+ listings = []
22
+ for entry in entries:
23
+ try:
24
+ pth, entries = entry
25
+ except (TypeError, ValueError) as err:
26
+ raise ImproperlyConfigured(
27
+ "Setting SLM_FILE_VIEW_STRUCTURE must contain sequences of (str, Entry) tuples."
28
+ ) from err
29
+ listings.append(Listing(pth, is_dir=not isinstance(entries, File)))
30
+ if isinstance(entries, Entry):
31
+ urlpatterns.extend(entries.urls(url_path / pth))
32
+ else:
33
+ add_paths(url_path / pth, entries)
34
+
35
+ urlpatterns.append(
36
+ path(
37
+ Entry.path_str(url_path),
38
+ FileSystemView.as_view(),
39
+ kwargs={"path": url_path, "listings": listings},
40
+ )
41
+ )
42
+
43
+
44
+ add_paths(
45
+ Path("/") / getattr(settings, "SLM_FILE_VIEW_ROOT", {}),
46
+ (getattr(settings, "SLM_FILE_VIEW_STRUCTURE", []) or []),
47
+ )