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.
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/METADATA +2 -2
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/RECORD +47 -34
- slm/__init__.py +1 -1
- slm/admin.py +40 -3
- slm/api/edit/views.py +37 -2
- slm/api/public/serializers.py +1 -1
- slm/defines/CoordinateMode.py +9 -0
- slm/defines/SiteLogFormat.py +19 -6
- slm/defines/__init__.py +24 -22
- slm/file_views/apps.py +7 -0
- slm/file_views/config.py +253 -0
- slm/file_views/settings.py +124 -0
- slm/file_views/static/slm/file_views/banner_header.png +0 -0
- slm/file_views/static/slm/file_views/css/listing.css +82 -0
- slm/file_views/templates/slm/file_views/listing.html +70 -0
- slm/file_views/urls.py +47 -0
- slm/file_views/views.py +472 -0
- slm/forms.py +22 -4
- slm/jinja2/slm/sitelog/ascii_9char.log +1 -1
- slm/jinja2/slm/sitelog/legacy.log +1 -1
- slm/management/commands/check_upgrade.py +25 -19
- slm/management/commands/generate_sinex.py +9 -7
- slm/map/settings.py +0 -0
- slm/migrations/0001_alter_archivedsitelog_size_and_more.py +44 -0
- slm/migrations/0032_archiveindex_valid_range_and_more.py +8 -1
- slm/migrations/simplify_daily_index_files.py +86 -0
- slm/models/index.py +73 -6
- slm/models/sitelog.py +6 -0
- slm/models/system.py +35 -2
- slm/parsing/__init__.py +10 -0
- slm/parsing/legacy/binding.py +3 -2
- slm/receivers/cache.py +25 -0
- slm/settings/root.py +22 -0
- slm/settings/routines.py +2 -0
- slm/settings/slm.py +58 -0
- slm/settings/urls.py +1 -1
- slm/settings/validation.py +5 -4
- slm/signals.py +3 -4
- slm/static/slm/js/enums.js +7 -6
- slm/static/slm/js/form.js +25 -14
- slm/static/slm/js/slm.js +4 -2
- slm/templatetags/slm.py +1 -1
- slm/utils.py +161 -36
- slm/validators.py +51 -0
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/WHEEL +0 -0
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/entry_points.txt +0 -0
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/licenses/LICENSE +0 -0
slm/file_views/config.py
ADDED
@@ -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
|
+
)
|
Binary file
|
@@ -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
|
+
)
|