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.
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
@@ -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