django-dh-map 0.0.0__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.
- django_dh_map/__init__.py +0 -0
- django_dh_map/admin.py +408 -0
- django_dh_map/api.py +103 -0
- django_dh_map/apps.py +85 -0
- django_dh_map/fields.py +29 -0
- django_dh_map/font_icons.py +3683 -0
- django_dh_map/forms.py +102 -0
- django_dh_map/helpers.py +26 -0
- django_dh_map/migrations/0001_initial.py +379 -0
- django_dh_map/migrations/__init__.py +0 -0
- django_dh_map/models.py +642 -0
- django_dh_map/serializers.py +222 -0
- django_dh_map/settings.py +36 -0
- django_dh_map/signals.py +69 -0
- django_dh_map/tasks.py +259 -0
- django_dh_map/urls.py +9 -0
- django_dh_map/validators.py +31 -0
- django_dh_map/widgets.py +49 -0
- django_dh_map-0.0.0.dist-info/METADATA +157 -0
- django_dh_map-0.0.0.dist-info/RECORD +23 -0
- django_dh_map-0.0.0.dist-info/WHEEL +5 -0
- django_dh_map-0.0.0.dist-info/licenses/LICENSE +674 -0
- django_dh_map-0.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
django_dh_map/admin.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from django.contrib import admin
|
|
3
|
+
from django.db import models
|
|
4
|
+
from django.utils.safestring import mark_safe
|
|
5
|
+
from django_rq import enqueue
|
|
6
|
+
from django.contrib import messages
|
|
7
|
+
from django.forms import widgets
|
|
8
|
+
from django.utils.html import escape
|
|
9
|
+
from tinymce.widgets import TinyMCE
|
|
10
|
+
from js_asset import JS
|
|
11
|
+
from adminsortable2.admin import SortableAdminMixin
|
|
12
|
+
from solo.admin import SingletonModelAdmin
|
|
13
|
+
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, \
|
|
14
|
+
PolymorphicChildModelFilter
|
|
15
|
+
from nested_admin.nested import NestedModelAdmin, NestedStackedInline
|
|
16
|
+
from nested_admin.polymorphic import NestedPolymorphicModelAdmin, NestedStackedPolymorphicInline
|
|
17
|
+
|
|
18
|
+
from .tasks import task_overhead_map_tiles_generator, task_panorama_map_tiles_generator, \
|
|
19
|
+
task_video_stream_generator, task_video_snapshot_generator, task_video_thumbnails_vtt_generator, \
|
|
20
|
+
task_audio_stream_generator
|
|
21
|
+
from .helpers import get_success_failure_status_tick
|
|
22
|
+
from .models import Map, OverheadImageMap, PanoramaImageMap, AbstractImageMap, XyzMap, \
|
|
23
|
+
InfoPage, Feature, ContentBlock, ContentBlockRichText, ContentBlockImage, ContentBlockImageGallery, \
|
|
24
|
+
ContentBlockImageGalleryImage, ContentBlockImageBeforeAndAfter, ContentBlockVideo, ContentBlockAudio, \
|
|
25
|
+
Icon, IconImage, IconNumbered
|
|
26
|
+
# IconFont
|
|
27
|
+
from .font_icons import FontIcon
|
|
28
|
+
from .serializers import ContentBlockImageGalleryImageSerializer
|
|
29
|
+
from .widgets import TextDataListWidget
|
|
30
|
+
from .forms import BaseMapAdminForm, XyzMapForm, OverheadImageMapForm, PanoramaImageMapForm
|
|
31
|
+
from .settings import DH_MAP_DEFAULT_PROPERTIES_SCHEMA, DH_MAP_CI_FEATURE_PROPERTIES_SCHEMA
|
|
32
|
+
|
|
33
|
+
def video_preview_app_tag(video, title, thumbnail, thumbnails_vtt):
|
|
34
|
+
return f'<div class="admin-video-preview-app" data-video="{video.url}" data-title="{title}" data-thumbnail="{thumbnail.url if thumbnail else ''}" data-thumbnails-vtt="{thumbnails_vtt.url if thumbnails_vtt else ''}"></div>'
|
|
35
|
+
|
|
36
|
+
class DjangoDhMapAdminMixin:
|
|
37
|
+
class Media:
|
|
38
|
+
css = { 'all': ['django_dh_map/admin/dist/admin.css'] }
|
|
39
|
+
js = [JS('django_dh_map/admin/dist/admin.js', {'type': 'module'})]
|
|
40
|
+
|
|
41
|
+
# based of SortableHiddenMixin
|
|
42
|
+
class SortableHiddenMixinTinyMceFix:
|
|
43
|
+
sortable_field_name = "position"
|
|
44
|
+
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
45
|
+
if db_field.name == self.sortable_field_name:
|
|
46
|
+
kwargs["widget"] = widgets.HiddenInput(attrs={'class': '_sortable_field_'})
|
|
47
|
+
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
48
|
+
|
|
49
|
+
class SortablePolymorphicParentModelAdmin(PolymorphicParentModelAdmin):
|
|
50
|
+
@property
|
|
51
|
+
def change_list_template(self):
|
|
52
|
+
return "admin/change_list.html"
|
|
53
|
+
|
|
54
|
+
class MapAdminChildAdmin(DjangoDhMapAdminMixin, PolymorphicChildModelAdmin):
|
|
55
|
+
base_form = BaseMapAdminForm
|
|
56
|
+
|
|
57
|
+
@admin.register(XyzMap)
|
|
58
|
+
class XyzMapChildAdmin(MapAdminChildAdmin):
|
|
59
|
+
base_form = XyzMapForm
|
|
60
|
+
base_model = XyzMap
|
|
61
|
+
|
|
62
|
+
@admin.register(OverheadImageMap)
|
|
63
|
+
class OverheadImageMapChildAdmin(MapAdminChildAdmin):
|
|
64
|
+
base_form = OverheadImageMapForm
|
|
65
|
+
base_model = OverheadImageMap
|
|
66
|
+
|
|
67
|
+
def get_fields(self, request, obj=None):
|
|
68
|
+
fields = super().get_fields(request, obj)
|
|
69
|
+
if not obj or not obj.has_tiles():
|
|
70
|
+
fields.remove('map_geojson')
|
|
71
|
+
fields.remove('properties')
|
|
72
|
+
return fields
|
|
73
|
+
|
|
74
|
+
@admin.register(PanoramaImageMap)
|
|
75
|
+
class PanoramaImageMapChildAdmin(MapAdminChildAdmin):
|
|
76
|
+
base_form = PanoramaImageMapForm
|
|
77
|
+
base_model = PanoramaImageMap
|
|
78
|
+
|
|
79
|
+
def get_fields(self, request, obj=None):
|
|
80
|
+
fields = super().get_fields(request, obj)
|
|
81
|
+
if not obj or not obj.has_tiles():
|
|
82
|
+
fields.remove('map_geojson')
|
|
83
|
+
fields.remove('properties')
|
|
84
|
+
return fields
|
|
85
|
+
|
|
86
|
+
@admin.register(Map)
|
|
87
|
+
class MapParentAdmin(DjangoDhMapAdminMixin, SortableAdminMixin, SortablePolymorphicParentModelAdmin):
|
|
88
|
+
child_models = (XyzMap, OverheadImageMap, PanoramaImageMap)
|
|
89
|
+
list_filter = [PolymorphicChildModelFilter]
|
|
90
|
+
polymorphic_list = True
|
|
91
|
+
list_per_page = 5
|
|
92
|
+
ordering = ['position']
|
|
93
|
+
|
|
94
|
+
list_display = ('position', 'id', '_type', 'label', 'date_taken', '_preview', '_status')
|
|
95
|
+
list_display_links = ('id', '_type', 'label', 'date_taken')
|
|
96
|
+
actions = ['reprocess_maps']
|
|
97
|
+
|
|
98
|
+
@admin.action(description="(Re)process selected map(s)")
|
|
99
|
+
def reprocess_maps(self, request, queryset):
|
|
100
|
+
job_count = 0
|
|
101
|
+
for map in queryset:
|
|
102
|
+
if isinstance(map, OverheadImageMap) and map.has_image():
|
|
103
|
+
enqueue(task_overhead_map_tiles_generator, map.pk)
|
|
104
|
+
# task_overhead_map_tiles_generator(map.pk)
|
|
105
|
+
job_count+=1
|
|
106
|
+
elif isinstance(map, PanoramaImageMap) and map.has_image():
|
|
107
|
+
enqueue(task_panorama_map_tiles_generator, map.pk)
|
|
108
|
+
# task_panorama_map_tiles_generator(map.pk)
|
|
109
|
+
job_count+=1
|
|
110
|
+
self.message_user(request, f'Created {job_count} jobs.', messages.SUCCESS)
|
|
111
|
+
|
|
112
|
+
def _type(self, obj):
|
|
113
|
+
return obj._meta.verbose_name
|
|
114
|
+
_type.short_description = 'Map Type'
|
|
115
|
+
|
|
116
|
+
def _preview(self, obj):
|
|
117
|
+
if isinstance(obj, XyzMap) and obj.url:
|
|
118
|
+
return mark_safe(f'<div class="openlayers-preview-app" data-map-id="{obj.pk}"></div>')
|
|
119
|
+
elif isinstance(obj, OverheadImageMap) and obj.tiles_dir:
|
|
120
|
+
return mark_safe(f'<div class="openlayers-preview-app" data-map-id="{obj.pk}"></div>')
|
|
121
|
+
elif isinstance(obj, PanoramaImageMap) and obj.tiles_dir:
|
|
122
|
+
return mark_safe(f'<div class="pannellum-preview-app" data-map-id="{obj.pk}"></div>')
|
|
123
|
+
return ''
|
|
124
|
+
_preview.short_description = 'Preview'
|
|
125
|
+
|
|
126
|
+
def _status(self, obj):
|
|
127
|
+
tiles_status_str = ''
|
|
128
|
+
if isinstance(obj, AbstractImageMap):
|
|
129
|
+
tiles_status_str = mark_safe(f'{get_success_failure_status_tick(obj.has_tiles())} Map Tiles <br />')
|
|
130
|
+
|
|
131
|
+
return mark_safe(f'''
|
|
132
|
+
{get_success_failure_status_tick(obj.published)} Published<br />
|
|
133
|
+
{tiles_status_str}
|
|
134
|
+
''')
|
|
135
|
+
_status.short_description = "Status"
|
|
136
|
+
|
|
137
|
+
class ContentBlockInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline):
|
|
138
|
+
model = ContentBlock
|
|
139
|
+
|
|
140
|
+
class ContentBlockRichTextInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
141
|
+
model = ContentBlockRichText
|
|
142
|
+
formfield_overrides = { models.TextField: { 'widget': TinyMCE } }
|
|
143
|
+
|
|
144
|
+
class ContentBlockImageInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
145
|
+
model = ContentBlockImage
|
|
146
|
+
fields = [('_thumbnail_tag', 'original'), ('name', 'description', 'license'), 'position']
|
|
147
|
+
readonly_fields = ['_thumbnail_tag']
|
|
148
|
+
def _thumbnail_tag(self, obj):
|
|
149
|
+
return mark_safe(f'<img class="admin-image-preview" src="{obj.thumbnail.url}" />') if obj.thumbnail else 'N/A'
|
|
150
|
+
_thumbnail_tag.short_description = 'Preview'
|
|
151
|
+
|
|
152
|
+
class ContentBlockImageGalleryInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
153
|
+
model = ContentBlockImageGallery
|
|
154
|
+
fields = ['_gallery_preview_tag', 'position']
|
|
155
|
+
|
|
156
|
+
readonly_fields = ['_gallery_preview_tag']
|
|
157
|
+
def _gallery_preview_tag(self, obj):
|
|
158
|
+
images = obj.images.all()
|
|
159
|
+
images_json = json.dumps(ContentBlockImageGalleryImageSerializer(images, many=True).data) if len(images) > 0 else None
|
|
160
|
+
return mark_safe(f'<div class="admin-gallery-preview-app" data-gallery-images-json="{escape(images_json)}"></div>') if images_json else 'N/A'
|
|
161
|
+
_gallery_preview_tag.short_description = 'Preview'
|
|
162
|
+
|
|
163
|
+
class ContentBlockImageGalleryImageInline(SortableHiddenMixinTinyMceFix, NestedStackedInline):
|
|
164
|
+
model = ContentBlockImageGalleryImage
|
|
165
|
+
min_num = 1
|
|
166
|
+
extra = 0
|
|
167
|
+
classes = ['collapse']
|
|
168
|
+
fields = [('_thumbnail_tag', 'original'), ('name', 'description', 'license'), 'position']
|
|
169
|
+
readonly_fields = ['_thumbnail_tag']
|
|
170
|
+
def _thumbnail_tag(self, obj):
|
|
171
|
+
return mark_safe(f'<img class="admin-image-preview" src="{obj.thumbnail.url}" />') if obj.thumbnail else 'N/A'
|
|
172
|
+
_thumbnail_tag.short_description = 'Preview'
|
|
173
|
+
|
|
174
|
+
inlines = [ContentBlockImageGalleryImageInline]
|
|
175
|
+
|
|
176
|
+
class ContentBlockImageBeforeAndAfterInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
177
|
+
model = ContentBlockImageBeforeAndAfter
|
|
178
|
+
fieldsets = (
|
|
179
|
+
(None, { 'fields': [
|
|
180
|
+
('_before_after_preview_tag', 'caption',), 'position',
|
|
181
|
+
], }),
|
|
182
|
+
('Before Image', { 'classes': ('collapse', 'expanded'), 'fields': [
|
|
183
|
+
('_before_thumbnail_tag', 'before_original'),
|
|
184
|
+
('before_name', 'before_description', 'before_license'),
|
|
185
|
+
], }),
|
|
186
|
+
('After Image', { 'classes': ('collapse', 'expanded'), 'fields': [
|
|
187
|
+
('_after_thumbnail_tag', 'after_original'),
|
|
188
|
+
('after_name', 'after_description', 'after_license'),
|
|
189
|
+
], }),
|
|
190
|
+
)
|
|
191
|
+
readonly_fields = ['_before_after_preview_tag', '_before_thumbnail_tag', '_after_thumbnail_tag']
|
|
192
|
+
def _before_after_preview_tag(self, obj):
|
|
193
|
+
return mark_safe(f'<div class="admin-before-after-preview-app" data-before="{obj.before_thumbnail.url}" data-after="{obj.after_thumbnail.url}"></div>') if obj.before_thumbnail and obj.after_thumbnail else 'N/A'
|
|
194
|
+
_before_after_preview_tag.short_description = 'Preview'
|
|
195
|
+
def _before_thumbnail_tag(self, obj):
|
|
196
|
+
return mark_safe(f'<img class="admin-image-preview" src="{obj.before_thumbnail.url}" />') if obj.before_thumbnail else 'N/A'
|
|
197
|
+
_before_thumbnail_tag.short_description = 'Preview'
|
|
198
|
+
def _after_thumbnail_tag(self, obj):
|
|
199
|
+
return mark_safe(f'<img class="admin-image-preview" src="{obj.after_thumbnail.url}" />') if obj.after_thumbnail else 'N/A'
|
|
200
|
+
_after_thumbnail_tag.short_description = 'Preview'
|
|
201
|
+
|
|
202
|
+
class ContentBlockVideoInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
203
|
+
model = ContentBlockVideo
|
|
204
|
+
fields = [('_video_tag', 'original'), ('name', 'license'), 'position']
|
|
205
|
+
readonly_fields = ['_video_tag']
|
|
206
|
+
|
|
207
|
+
def _video_tag(self, obj):
|
|
208
|
+
return mark_safe(video_preview_app_tag(obj.video, f'{obj}', obj.thumbnail, obj.thumbnails_vtt)) if obj.video else 'N/A'
|
|
209
|
+
_video_tag.short_description = 'Preview'
|
|
210
|
+
|
|
211
|
+
class ContentBlockAudioInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
212
|
+
model = ContentBlockAudio
|
|
213
|
+
fields = [('_audio_tag', 'original'), ('name', 'license'), 'position']
|
|
214
|
+
readonly_fields = ['_audio_tag']
|
|
215
|
+
def _audio_tag(self, obj):
|
|
216
|
+
return mark_safe(f'<audio src="{obj.audio.url}" controls preload="metadata" />') if obj.audio else 'N/A'
|
|
217
|
+
_audio_tag.short_description = 'Preview'
|
|
218
|
+
|
|
219
|
+
child_inlines = (
|
|
220
|
+
ContentBlockRichTextInline,
|
|
221
|
+
ContentBlockImageInline,
|
|
222
|
+
ContentBlockImageGalleryInline,
|
|
223
|
+
ContentBlockImageBeforeAndAfterInline,
|
|
224
|
+
ContentBlockVideoInline,
|
|
225
|
+
ContentBlockAudioInline,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
@admin.register(ContentBlockVideo)
|
|
229
|
+
class ContentBlockVideoAdmin(DjangoDhMapAdminMixin, PolymorphicChildModelAdmin):
|
|
230
|
+
base_model = ContentBlockVideo
|
|
231
|
+
fields = [('_video_tag', 'original'), ('name', 'license')]
|
|
232
|
+
readonly_fields = ['_video_tag']
|
|
233
|
+
show_in_index = True
|
|
234
|
+
list_display = ('id', '_name', '_status')
|
|
235
|
+
list_display_links = ('id', '_name')
|
|
236
|
+
ordering = ['id']
|
|
237
|
+
actions = ['reprocess_snapshot', 'reprocess_video_stream', 'reprocess_thumbnails_vtt']
|
|
238
|
+
|
|
239
|
+
def has_add_permission(self, request):
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
def has_delete_permission(self, request, obj=None):
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
def _name(self, obj):
|
|
246
|
+
return f'{obj}'
|
|
247
|
+
_name.short_description = 'Name'
|
|
248
|
+
|
|
249
|
+
def _status(self, obj):
|
|
250
|
+
return mark_safe(f'''
|
|
251
|
+
{get_success_failure_status_tick(obj.has_video())} Streamable<br />
|
|
252
|
+
{get_success_failure_status_tick(obj.has_snapshot())} Poster Image<br />
|
|
253
|
+
{get_success_failure_status_tick(obj.has_thumbnails_vtt())} Preview Thumbnails
|
|
254
|
+
''')
|
|
255
|
+
_status.short_description = 'Status'
|
|
256
|
+
|
|
257
|
+
def _video_tag(self, obj):
|
|
258
|
+
return mark_safe(video_preview_app_tag(obj.video, f'{obj}', obj.thumbnail, obj.thumbnails_vtt)) if obj.video else 'N/A'
|
|
259
|
+
_video_tag.short_description = 'Preview'
|
|
260
|
+
|
|
261
|
+
@admin.action(description="(Re)process poster images for selected video(s)")
|
|
262
|
+
def reprocess_snapshot(self, request, queryset):
|
|
263
|
+
job_count = 0
|
|
264
|
+
for video in queryset:
|
|
265
|
+
enqueue(task_video_snapshot_generator, video.pk)
|
|
266
|
+
# task_video_snapshot_generator(video.pk)
|
|
267
|
+
job_count+=1
|
|
268
|
+
self.message_user(request, f'Created {job_count} jobs.', messages.SUCCESS)
|
|
269
|
+
|
|
270
|
+
@admin.action(description="(Re)process video stream for selected video(s)")
|
|
271
|
+
def reprocess_video_stream(self, request, queryset):
|
|
272
|
+
job_count = 0
|
|
273
|
+
for video in queryset:
|
|
274
|
+
enqueue(task_video_stream_generator, video.pk)
|
|
275
|
+
# task_video_stream_generator(video.pk)
|
|
276
|
+
job_count+=1
|
|
277
|
+
self.message_user(request, f'Created {job_count} jobs.', messages.SUCCESS)
|
|
278
|
+
|
|
279
|
+
@admin.action(description="(Re)process preview thumbnails for selected video(s)")
|
|
280
|
+
def reprocess_thumbnails_vtt(self, request, queryset):
|
|
281
|
+
job_count = 0
|
|
282
|
+
for video in queryset:
|
|
283
|
+
enqueue(task_video_thumbnails_vtt_generator, video.pk)
|
|
284
|
+
# task_video_thumbnails_vtt_generator(video.pk)
|
|
285
|
+
job_count+=1
|
|
286
|
+
self.message_user(request, f'Created {job_count} jobs.', messages.SUCCESS)
|
|
287
|
+
|
|
288
|
+
@admin.register(ContentBlockAudio)
|
|
289
|
+
class ContentBlockAudioAdmin(DjangoDhMapAdminMixin, PolymorphicChildModelAdmin):
|
|
290
|
+
base_model = ContentBlockAudio
|
|
291
|
+
fields = [('_audio_tag', 'original'), ('name', 'license')]
|
|
292
|
+
readonly_fields = ['_audio_tag']
|
|
293
|
+
show_in_index = True
|
|
294
|
+
list_display = ('id', '_name', '_status')
|
|
295
|
+
list_display_links = ('id', '_name')
|
|
296
|
+
ordering = ['id']
|
|
297
|
+
actions = ['reprocess_audio_stream']
|
|
298
|
+
|
|
299
|
+
def has_add_permission(self, request):
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
def has_delete_permission(self, request, obj=None):
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
def _name(self, obj):
|
|
306
|
+
return f'{obj}'
|
|
307
|
+
_name.short_description = 'Name'
|
|
308
|
+
|
|
309
|
+
def _status(self, obj):
|
|
310
|
+
return mark_safe(f'''
|
|
311
|
+
{get_success_failure_status_tick(obj.has_audio())} Streamable
|
|
312
|
+
''')
|
|
313
|
+
_status.short_description = 'Status'
|
|
314
|
+
|
|
315
|
+
def _audio_tag(self, obj):
|
|
316
|
+
return mark_safe(f'<audio src="{obj.audio.url}" controls preload="metadata" />') if obj.audio else 'N/A'
|
|
317
|
+
_audio_tag.short_description = 'Preview'
|
|
318
|
+
|
|
319
|
+
@admin.action(description="(Re)process audio stream for selected audio(s)")
|
|
320
|
+
def reprocess_audio_stream(self, request, queryset):
|
|
321
|
+
job_count = 0
|
|
322
|
+
for audio in queryset:
|
|
323
|
+
# enqueue(task_audio_stream_generator, audio.pk)
|
|
324
|
+
task_audio_stream_generator(audio.pk)
|
|
325
|
+
job_count+=1
|
|
326
|
+
self.message_user(request, f'Created {job_count} jobs.', messages.SUCCESS)
|
|
327
|
+
|
|
328
|
+
class AbstractContentItemAdmin(DjangoDhMapAdminMixin, NestedPolymorphicModelAdmin):
|
|
329
|
+
inlines = [ContentBlockInline]
|
|
330
|
+
|
|
331
|
+
def _status(self, obj):
|
|
332
|
+
video_status = [
|
|
333
|
+
f'- {get_success_failure_status_tick(video.has_video())} Streamable | {get_success_failure_status_tick(video.has_snapshot())} Poster Image | {get_success_failure_status_tick(video.has_thumbnails_vtt())} Preview Thumbnails | {video.name if video.name else video.original.name}' for video in obj.content_blocks.instance_of(ContentBlockVideo).all()
|
|
334
|
+
]
|
|
335
|
+
video_status_str = '' if len(video_status) == 0 else 'Videos: <br />' + '<br />'.join(video_status) + '<br />'
|
|
336
|
+
audio_status = [
|
|
337
|
+
f'- {get_success_failure_status_tick(audio.has_audio())} Streamable | {audio.name if audio.name else audio.original.name}' for audio in obj.content_blocks.instance_of(ContentBlockAudio).all()
|
|
338
|
+
]
|
|
339
|
+
audio_status_str = '' if len(audio_status) == 0 else 'Audio: <br />' + '<br />'.join(audio_status) + '<br />'
|
|
340
|
+
|
|
341
|
+
return mark_safe(f'''
|
|
342
|
+
{get_success_failure_status_tick(obj.published)} Published<br />
|
|
343
|
+
{video_status_str}
|
|
344
|
+
{audio_status_str}
|
|
345
|
+
''')
|
|
346
|
+
_status.short_description = "Status"
|
|
347
|
+
|
|
348
|
+
@admin.register(InfoPage)
|
|
349
|
+
class InfoPageAdmin(SortableAdminMixin, AbstractContentItemAdmin):
|
|
350
|
+
list_display = ('position', 'id', 'title', '_status')
|
|
351
|
+
list_display_links = ('id', 'title')
|
|
352
|
+
ordering = ['position']
|
|
353
|
+
fields = ['title', 'published']
|
|
354
|
+
inlines = [ContentBlockInline]
|
|
355
|
+
|
|
356
|
+
class IconInline(NestedStackedPolymorphicInline):
|
|
357
|
+
model = Icon
|
|
358
|
+
min_num = 0
|
|
359
|
+
max_num = 1
|
|
360
|
+
extra = 0
|
|
361
|
+
|
|
362
|
+
# # TODO: FIX: doesn't work well at the moment
|
|
363
|
+
# class IconFontInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
364
|
+
# model = IconFont
|
|
365
|
+
# fields = ['class_value']
|
|
366
|
+
# def formfield_for_dbfield(self, db_field, **kwargs):
|
|
367
|
+
# if db_field.name == 'class_value':
|
|
368
|
+
# kwargs['widget'] = Select2Widget(attrs={
|
|
369
|
+
# 'data-placeholder': 'Click to select a Bootstrap or Fontawesome icon',
|
|
370
|
+
# },
|
|
371
|
+
# choices=FontIcon.choices,
|
|
372
|
+
# )
|
|
373
|
+
# return super().formfield_for_dbfield(db_field, **kwargs)
|
|
374
|
+
|
|
375
|
+
class IconImageInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
376
|
+
model = IconImage
|
|
377
|
+
fields = [('_thumbnail_tag', 'original')]
|
|
378
|
+
readonly_fields = ['_thumbnail_tag']
|
|
379
|
+
def _thumbnail_tag(self, obj):
|
|
380
|
+
return mark_safe(f'<img class="admin-image-preview" style="border-radius: 50%" src="{obj.icon_thumbnail.url}" />') if obj.icon_thumbnail else 'N/A'
|
|
381
|
+
_thumbnail_tag.short_description = 'Preview'
|
|
382
|
+
|
|
383
|
+
class IconNumberedInline(SortableHiddenMixinTinyMceFix, NestedStackedPolymorphicInline.Child):
|
|
384
|
+
model = IconNumbered
|
|
385
|
+
fields = ['number']
|
|
386
|
+
|
|
387
|
+
child_inlines = (
|
|
388
|
+
# IconFontInline,
|
|
389
|
+
IconImageInline,
|
|
390
|
+
IconNumberedInline,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
@admin.register(Feature)
|
|
394
|
+
class FeatureAdmin(AbstractContentItemAdmin):
|
|
395
|
+
list_display = ('id', 'title', '_status')
|
|
396
|
+
list_display_links = ('id', 'title')
|
|
397
|
+
ordering = ['id']
|
|
398
|
+
fields = ['title', 'published', 'properties']
|
|
399
|
+
inlines = [
|
|
400
|
+
IconInline,
|
|
401
|
+
ContentBlockInline,
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
def get_fields(self, request, obj=None):
|
|
405
|
+
fields = super().get_fields(request, obj)
|
|
406
|
+
if obj and 'properties' in fields and DH_MAP_CI_FEATURE_PROPERTIES_SCHEMA == DH_MAP_DEFAULT_PROPERTIES_SCHEMA:
|
|
407
|
+
fields.remove('properties')
|
|
408
|
+
return fields
|
django_dh_map/api.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from django.views.decorators.cache import cache_page
|
|
2
|
+
from django.utils.decorators import method_decorator
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.db.models import Q
|
|
5
|
+
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
|
|
6
|
+
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
|
|
7
|
+
from rest_framework.routers import DefaultRouter
|
|
8
|
+
from rest_framework.permissions import IsAdminUser
|
|
9
|
+
from rest_framework.renderers import JSONRenderer
|
|
10
|
+
from rest_framework.decorators import action
|
|
11
|
+
from rest_framework.response import Response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from .serializers import MapGeoJsonPolymorphicSerializer, MapPolymorphicSerializer, \
|
|
15
|
+
FeatureStubSerializer, FeatureSerializer, InfoPageSerializer
|
|
16
|
+
from .models import Map, MapGeoJson, Feature, InfoPage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReadOnlyModelListViewSet(ListModelMixin, GenericViewSet):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class ReadOnlyModelRetrieveViewSet(RetrieveModelMixin, GenericViewSet):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class GeoJsonRenderer(JSONRenderer):
|
|
26
|
+
media_type = 'application/geo+json'
|
|
27
|
+
|
|
28
|
+
class BaseMapGeoJsonViewSet(ReadOnlyModelListViewSet):
|
|
29
|
+
renderer_classes = [GeoJsonRenderer]
|
|
30
|
+
queryset = MapGeoJson.objects
|
|
31
|
+
serializer_class = MapGeoJsonPolymorphicSerializer
|
|
32
|
+
# turn off pagination for GeoJson (send whole thing)
|
|
33
|
+
pagination_class = None
|
|
34
|
+
|
|
35
|
+
def get_queryset(self):
|
|
36
|
+
return super().get_queryset().filter(map=self.kwargs['map_id'])
|
|
37
|
+
|
|
38
|
+
class AdminMapGeoJsonViewSet(BaseMapGeoJsonViewSet):
|
|
39
|
+
permission_classes = [IsAdminUser]
|
|
40
|
+
|
|
41
|
+
class PublicMapGeoJsonViewSet(BaseMapGeoJsonViewSet):
|
|
42
|
+
queryset = MapGeoJson.objects \
|
|
43
|
+
.filter(Q(mapgeojsonfeature__feature__published=True) | Q(mapgeojsontransition__destination_map__published=True) | Q(mapgeojsonlabel__isnull=False)) \
|
|
44
|
+
.filter(map__published=True)
|
|
45
|
+
|
|
46
|
+
@method_decorator(cache_page(settings.CACHE_SECONDS))
|
|
47
|
+
def list(self, request, *args, **kwargs):
|
|
48
|
+
return super().list(request, *args, **kwargs)
|
|
49
|
+
|
|
50
|
+
class BaseFeatureViewSet(ReadOnlyModelViewSet):
|
|
51
|
+
renderer_classes = [JSONRenderer]
|
|
52
|
+
queryset = Feature.objects.prefetch_related('icon').all()
|
|
53
|
+
serializer_class = FeatureSerializer
|
|
54
|
+
|
|
55
|
+
class AdminFeatureViewSet(BaseFeatureViewSet):
|
|
56
|
+
permission_classes = [IsAdminUser]
|
|
57
|
+
serializer_class = FeatureStubSerializer
|
|
58
|
+
|
|
59
|
+
class PublicFeatureViewSet(BaseFeatureViewSet):
|
|
60
|
+
queryset = Feature.objects.prefetch_related('content_blocks', 'icon').filter(published=True).all()
|
|
61
|
+
|
|
62
|
+
@method_decorator(cache_page(settings.CACHE_SECONDS))
|
|
63
|
+
def list(self, request, *args, **kwargs):
|
|
64
|
+
return super().list(request, *args, **kwargs)
|
|
65
|
+
|
|
66
|
+
@method_decorator(cache_page(settings.CACHE_SECONDS))
|
|
67
|
+
def retrieve(self, request, *args, **kwargs):
|
|
68
|
+
return super().retrieve(request, *args, **kwargs)
|
|
69
|
+
|
|
70
|
+
class BaseMapViewSet(ReadOnlyModelViewSet):
|
|
71
|
+
renderer_classes = [JSONRenderer]
|
|
72
|
+
queryset = Map.objects.all()
|
|
73
|
+
serializer_class = MapPolymorphicSerializer
|
|
74
|
+
|
|
75
|
+
class AdminMapViewSet(BaseMapViewSet):
|
|
76
|
+
permission_classes = [IsAdminUser]
|
|
77
|
+
|
|
78
|
+
class PublicMapViewSet(BaseMapViewSet):
|
|
79
|
+
queryset = Map.objects.filter(published=True).all()
|
|
80
|
+
|
|
81
|
+
@method_decorator(cache_page(settings.CACHE_SECONDS))
|
|
82
|
+
def list(self, request, *args, **kwargs):
|
|
83
|
+
return super().list(request, *args, **kwargs)
|
|
84
|
+
|
|
85
|
+
@method_decorator(cache_page(settings.CACHE_SECONDS))
|
|
86
|
+
def retrieve(self, request, *args, **kwargs):
|
|
87
|
+
return super().retrieve(request, *args, **kwargs)
|
|
88
|
+
|
|
89
|
+
class InfoPageViewSet(ReadOnlyModelViewSet):
|
|
90
|
+
renderer_classes = [JSONRenderer]
|
|
91
|
+
queryset = InfoPage.objects.prefetch_related('content_blocks').filter(published=True).all()
|
|
92
|
+
serializer_class = InfoPageSerializer
|
|
93
|
+
|
|
94
|
+
admin_router = DefaultRouter(use_regex_path=False, trailing_slash=False)
|
|
95
|
+
admin_router.register('maps/<int:map_id>/geojson', AdminMapGeoJsonViewSet)
|
|
96
|
+
admin_router.register('features', AdminFeatureViewSet)
|
|
97
|
+
admin_router.register('maps', AdminMapViewSet)
|
|
98
|
+
|
|
99
|
+
public_router = DefaultRouter(use_regex_path=False, trailing_slash=False)
|
|
100
|
+
public_router.register('maps/<int:map_id>/geojson', PublicMapGeoJsonViewSet)
|
|
101
|
+
public_router.register('features', PublicFeatureViewSet)
|
|
102
|
+
public_router.register('maps', PublicMapViewSet)
|
|
103
|
+
public_router.register('pages', InfoPageViewSet)
|
django_dh_map/apps.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from django.core.checks import Error, Warning as CheckWarning, register, Tags
|
|
7
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
8
|
+
|
|
9
|
+
REQUIRED_APPS = [
|
|
10
|
+
'admin_interface',
|
|
11
|
+
'colorfield',
|
|
12
|
+
'nested_admin',
|
|
13
|
+
'django_jsonform',
|
|
14
|
+
'adminsortable2',
|
|
15
|
+
'solo',
|
|
16
|
+
'tinymce',
|
|
17
|
+
'admin_async_upload',
|
|
18
|
+
'imagekit',
|
|
19
|
+
'django_rq',
|
|
20
|
+
'django.contrib.gis',
|
|
21
|
+
'django.contrib.postgres',
|
|
22
|
+
'django.contrib.admin',
|
|
23
|
+
'django.contrib.auth',
|
|
24
|
+
'django.contrib.contenttypes',
|
|
25
|
+
'django.contrib.sessions',
|
|
26
|
+
'django.contrib.messages',
|
|
27
|
+
'django.contrib.staticfiles',
|
|
28
|
+
'polymorphic',
|
|
29
|
+
'rest_framework',
|
|
30
|
+
'rest_framework_gis',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DjangoDhMapConfig(AppConfig):
|
|
35
|
+
name = 'django_dh_map'
|
|
36
|
+
verbose_name = 'Digital Humanities Maps'
|
|
37
|
+
|
|
38
|
+
def ready(self):
|
|
39
|
+
# Import settings inside ready() to avoid circular import issues
|
|
40
|
+
from django.conf import settings
|
|
41
|
+
for app in REQUIRED_APPS:
|
|
42
|
+
if app not in settings.INSTALLED_APPS:
|
|
43
|
+
raise ImproperlyConfigured(
|
|
44
|
+
f"'{self.name}' requires '{app}' to be present in INSTALLED_APPS. "
|
|
45
|
+
f"Please add '{app}' to your INSTALLED_APPS in settings.py."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
import django_dh_map.signals
|
|
49
|
+
register(check_ffmpeg_availability, Tags.compatibility)
|
|
50
|
+
register(check_gdal_availability, Tags.compatibility)
|
|
51
|
+
|
|
52
|
+
def check_ffmpeg_availability(app_configs, **kwargs) -> List[Error]:
|
|
53
|
+
from .settings import DH_MAP_FFMPEG, DH_MAP_FFPROBE
|
|
54
|
+
errors = []
|
|
55
|
+
if not shutil.which(DH_MAP_FFMPEG):
|
|
56
|
+
errors += [Error(
|
|
57
|
+
"FFmpeg not found",
|
|
58
|
+
hint=(f'FFmpeg binary not found at "{DH_MAP_FFMPEG}". Install FFmpeg or set DH_MAP_FFMPEG setting to correct path.')
|
|
59
|
+
)]
|
|
60
|
+
if not shutil.which(DH_MAP_FFPROBE):
|
|
61
|
+
errors += [Error(
|
|
62
|
+
"FFprobe not found",
|
|
63
|
+
hint=(f'FFprobe binary not found at "{DH_MAP_FFPROBE}". Install FFmpeg or set DH_MAP_FFPROBE setting to correct path.')
|
|
64
|
+
)]
|
|
65
|
+
return errors
|
|
66
|
+
|
|
67
|
+
def check_gdal_availability(app_configs, **kwargs) -> List[Error]:
|
|
68
|
+
from .settings import GDAL_LIBRARY_PATH, GEOS_LIBRARY_PATH, DH_MAP_GDAL2TILES
|
|
69
|
+
errors = []
|
|
70
|
+
if not shutil.which(GDAL_LIBRARY_PATH):
|
|
71
|
+
errors += [Error(
|
|
72
|
+
"gdal not found",
|
|
73
|
+
hint=(f'gdal binary not found at "{GDAL_LIBRARY_PATH}". Install gdal or set GDAL_LIBRARY_PATH setting to correct path.')
|
|
74
|
+
)]
|
|
75
|
+
if not shutil.which(GEOS_LIBRARY_PATH):
|
|
76
|
+
errors += [Error(
|
|
77
|
+
"geos not found",
|
|
78
|
+
hint=(f'geos binary not found at "{GEOS_LIBRARY_PATH}". Install geos or set GEOS_LIBRARY_PATH setting to correct path.')
|
|
79
|
+
)]
|
|
80
|
+
if not shutil.which(DH_MAP_GDAL2TILES):
|
|
81
|
+
errors += [Error(
|
|
82
|
+
"gdal2tiles not found",
|
|
83
|
+
hint=(f'gdal2tiles binary not found at "{DH_MAP_GDAL2TILES}". Install gdal-tools or set DH_MAP_GDAL2TILES setting to correct path.')
|
|
84
|
+
)]
|
|
85
|
+
return errors
|
django_dh_map/fields.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django_ffield.fields import FileFField
|
|
3
|
+
from admin_async_upload.models import AsyncFileField
|
|
4
|
+
|
|
5
|
+
from .validators import FileTypeValidator, FixFileTypeValidator
|
|
6
|
+
|
|
7
|
+
class AsyncFFileField(FileFField, AsyncFileField):
|
|
8
|
+
# use custom validator
|
|
9
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
# remove FileFField's problematic validator (file needs to be reopened when async)
|
|
12
|
+
self.validators = [validator for validator in self.validators if not isinstance(validator, FileTypeValidator)]
|
|
13
|
+
if self.allowd_types or self.disallowed_types:
|
|
14
|
+
self.validators.append(
|
|
15
|
+
FixFileTypeValidator(allowd_types=self.allowd_types, disallowed_types=self.disallowed_types,)
|
|
16
|
+
)
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class ImageFFileField(models.ImageField, FileFField):
|
|
20
|
+
# use custom validator
|
|
21
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
# remove FileFField's problematic validator (file needs to be reopened when async)
|
|
24
|
+
self.validators = [validator for validator in self.validators if not isinstance(validator, FileTypeValidator)]
|
|
25
|
+
if self.allowd_types or self.disallowed_types:
|
|
26
|
+
self.validators.append(
|
|
27
|
+
FixFileTypeValidator(allowd_types=self.allowd_types, disallowed_types=self.disallowed_types,)
|
|
28
|
+
)
|
|
29
|
+
pass
|