Drafts system implemented
This commit is contained in:
+118
-39
@@ -4,6 +4,7 @@ import posixpath
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -707,25 +708,31 @@ def _serialize_temp(upload):
|
||||
}
|
||||
|
||||
|
||||
def _pending_uploads(user):
|
||||
return list(
|
||||
TempUpload.objects.filter(user=user, status='pending')
|
||||
.select_related('file').order_by('created_at')
|
||||
)
|
||||
def _pending_uploads(user, draft_uuid=None):
|
||||
qs = TempUpload.objects.filter(user=user, status='pending')
|
||||
if draft_uuid is not None:
|
||||
qs = qs.filter(draft_uuid=draft_uuid)
|
||||
else:
|
||||
qs = qs.filter(draft_uuid__isnull=True)
|
||||
return list(qs.select_related('file').order_by('created_at'))
|
||||
|
||||
|
||||
def _pending_kind(user, kind):
|
||||
return list(
|
||||
TempUpload.objects.filter(user=user, status='pending', kind=kind)
|
||||
.select_related('file').order_by('created_at')
|
||||
)
|
||||
def _pending_kind(user, kind, draft_uuid=None):
|
||||
qs = TempUpload.objects.filter(user=user, status='pending', kind=kind)
|
||||
if draft_uuid is not None:
|
||||
qs = qs.filter(draft_uuid=draft_uuid)
|
||||
else:
|
||||
qs = qs.filter(draft_uuid__isnull=True)
|
||||
return list(qs.select_related('file').order_by('created_at'))
|
||||
|
||||
|
||||
def _latest_pending(user, kind):
|
||||
return (
|
||||
TempUpload.objects.filter(user=user, status='pending', kind=kind)
|
||||
.select_related('file').order_by('created_at').last()
|
||||
)
|
||||
def _latest_pending(user, kind, draft_uuid=None):
|
||||
qs = TempUpload.objects.filter(user=user, status='pending', kind=kind)
|
||||
if draft_uuid is not None:
|
||||
qs = qs.filter(draft_uuid=draft_uuid)
|
||||
else:
|
||||
qs = qs.filter(draft_uuid__isnull=True)
|
||||
return qs.select_related('file').order_by('created_at').last()
|
||||
|
||||
|
||||
def _adopt_temp(upload, project_pk, subdir, kind):
|
||||
@@ -1123,7 +1130,48 @@ def _form_values(*forms):
|
||||
|
||||
|
||||
@login_required
|
||||
def project_create(request):
|
||||
@login_required
|
||||
def my_projects(request):
|
||||
"""The user's own projects and saved drafts (two-column page)."""
|
||||
projects = Project.objects.filter(owner=request.user).order_by('-created_at')
|
||||
drafts = ProjectDraft.objects.filter(user=request.user)
|
||||
return render(request, 'library/my_projects.html', {
|
||||
'projects': projects,
|
||||
'drafts': drafts,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def project_create_new(request):
|
||||
"""Entry point for a new project: create a fresh draft UUID, then edit it."""
|
||||
if request.method != 'GET':
|
||||
return HttpResponse(status=405)
|
||||
draft = ProjectDraft.objects.create(user=request.user)
|
||||
return redirect('library:project_create', uuid=draft.uuid)
|
||||
|
||||
|
||||
@login_required
|
||||
def project_draft_delete(request, uuid):
|
||||
"""Delete one draft (owner only) and purge its pending uploads."""
|
||||
draft = ProjectDraft.objects.filter(uuid=uuid, user=request.user).first()
|
||||
if draft is None:
|
||||
return redirect('library:my_projects')
|
||||
if request.method == 'POST':
|
||||
for temp in TempUpload.objects.filter(draft_uuid=draft.uuid):
|
||||
delete_file_index(temp.file)
|
||||
TempUpload.objects.filter(draft_uuid=draft.uuid).delete()
|
||||
draft.delete()
|
||||
messages.success(request, 'Draft deleted.')
|
||||
return redirect('library:my_projects')
|
||||
|
||||
|
||||
@login_required
|
||||
def project_create(request, uuid):
|
||||
"""Create-page for one specific draft. Only its owner may access it."""
|
||||
draft = ProjectDraft.objects.filter(uuid=uuid, user=request.user).first()
|
||||
if draft is None:
|
||||
return redirect('library:my_projects')
|
||||
|
||||
project_form = ProjectForm()
|
||||
version_form = VersionForm()
|
||||
|
||||
@@ -1132,7 +1180,7 @@ def project_create(request):
|
||||
version_form = VersionForm(request.POST)
|
||||
if project_form.is_valid() and version_form.is_valid():
|
||||
data = project_form.cleaned_data
|
||||
version_files = _pending_kind(request.user, 'version')
|
||||
version_files = _pending_kind(request.user, 'version', draft.uuid)
|
||||
if not version_files:
|
||||
version_form.add_error('version_name', 'Upload at least one version file before creating the project.')
|
||||
else:
|
||||
@@ -1145,7 +1193,7 @@ def project_create(request):
|
||||
owner=request.user,
|
||||
)
|
||||
|
||||
thumb = _latest_pending(request.user, 'thumbnail')
|
||||
thumb = _latest_pending(request.user, 'thumbnail', draft.uuid)
|
||||
if thumb is not None:
|
||||
project.thumbnail = _adopt_temp(thumb, project.pk, '', 'thumbnail')
|
||||
thumb.status = 'used'
|
||||
@@ -1166,7 +1214,7 @@ def project_create(request):
|
||||
messages.warning(request, ANIMATION_API_WARNING)
|
||||
|
||||
for temp in TempUpload.objects.filter(
|
||||
user=request.user, status='pending', kind='media',
|
||||
user=request.user, status='pending', kind='media', draft_uuid=draft.uuid,
|
||||
).select_related('file').order_by('created_at'):
|
||||
index = _adopt_temp(temp, project.pk, 'gallery', 'asset')
|
||||
ProjectAsset.objects.create(
|
||||
@@ -1179,24 +1227,23 @@ def project_create(request):
|
||||
|
||||
_apply_tags(project, data['tags'], request.user)
|
||||
project.sync_creator_tags(actor=request.user)
|
||||
ProjectDraft.objects.filter(user=request.user).delete()
|
||||
draft.delete()
|
||||
messages.success(request, 'Project created.')
|
||||
return redirect('library:project_detail', slug=project.slug)
|
||||
else:
|
||||
draft = ProjectDraft.objects.filter(user=request.user).first()
|
||||
if draft:
|
||||
initial = {k: draft.data.get(k, '') for k in DRAFT_KEYS}
|
||||
project_form = ProjectForm(initial=initial)
|
||||
version_form = VersionForm(
|
||||
initial={
|
||||
k: initial[k] for k in ('version_name', 'changelog') if k in initial
|
||||
},
|
||||
)
|
||||
initial = {k: draft.data.get(k, '') for k in DRAFT_KEYS}
|
||||
project_form = ProjectForm(initial=initial)
|
||||
version_form = VersionForm(
|
||||
initial={
|
||||
k: initial[k] for k in ('version_name', 'changelog') if k in initial
|
||||
},
|
||||
)
|
||||
|
||||
return render(request, 'library/project_create.html', {
|
||||
'project_form': project_form,
|
||||
'version_form': version_form,
|
||||
'draft_uploads': [_serialize_temp(u) for u in _pending_uploads(request.user)],
|
||||
'draft_uuid': draft.uuid,
|
||||
'draft_uploads': [_serialize_temp(u) for u in _pending_uploads(request.user, draft.uuid)],
|
||||
'draft_data': _form_values(project_form, version_form),
|
||||
'tag_categories': list(TagCategory.objects.order_by('slug').values('slug', 'color')),
|
||||
})
|
||||
@@ -1689,15 +1736,30 @@ def api_upload_temp(request):
|
||||
if kind == 'thumbnail' and not content_type.startswith('image/'):
|
||||
return JsonResponse({'error': 'The thumbnail must be an image.'}, status=400)
|
||||
|
||||
# Thumbnail is a single slot: discard any earlier pending one. Version and
|
||||
# media accept multiple files (one Version can hold several files).
|
||||
draft_uuid_raw = request.POST.get('draft_uuid', '').strip()
|
||||
draft_uuid = draft_uuid_raw or None
|
||||
if draft_uuid is not None:
|
||||
try:
|
||||
draft_uuid = UUID(draft_uuid_raw)
|
||||
except (ValueError, TypeError):
|
||||
draft_uuid = None
|
||||
|
||||
# Thumbnail is a single slot: discard any earlier pending one for this
|
||||
# draft (or non-draft flow). Version and media accept multiple files.
|
||||
if kind == 'thumbnail':
|
||||
for old in TempUpload.objects.filter(user=request.user, status='pending', kind='thumbnail'):
|
||||
old_qs = TempUpload.objects.filter(user=request.user, status='pending', kind='thumbnail')
|
||||
if draft_uuid is not None:
|
||||
old_qs = old_qs.filter(draft_uuid=draft_uuid)
|
||||
else:
|
||||
old_qs = old_qs.filter(draft_uuid__isnull=True)
|
||||
for old in old_qs:
|
||||
delete_file_index(old.file)
|
||||
old.delete()
|
||||
|
||||
index = store_temp_file(request.user, kind, uploaded)
|
||||
upload = TempUpload.objects.create(user=request.user, kind=kind, file=index)
|
||||
upload = TempUpload.objects.create(
|
||||
user=request.user, kind=kind, file=index, draft_uuid=draft_uuid,
|
||||
)
|
||||
return JsonResponse(_serialize_temp(upload))
|
||||
|
||||
|
||||
@@ -1714,20 +1776,31 @@ def api_delete_temp(request, upload_uuid):
|
||||
|
||||
@login_required
|
||||
def api_draft(request):
|
||||
"""Autosave endpoint for a single draft, keyed by its UUID."""
|
||||
def _draft_for(uuid_raw):
|
||||
try:
|
||||
return ProjectDraft.objects.filter(uuid=UUID(uuid_raw), user=request.user).first()
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
if request.method == 'GET':
|
||||
draft = ProjectDraft.objects.filter(user=request.user).first()
|
||||
draft = _draft_for(request.GET.get('draft', ''))
|
||||
if draft is None:
|
||||
return JsonResponse({'error': 'Draft not found'}, status=404)
|
||||
return JsonResponse({
|
||||
'data': draft.data if draft else {},
|
||||
'uploads': [_serialize_temp(u) for u in _pending_uploads(request.user)],
|
||||
'data': draft.data,
|
||||
'uploads': [_serialize_temp(u) for u in _pending_uploads(request.user, draft.uuid)],
|
||||
})
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
payload = json.loads(request.body or b'{}')
|
||||
except json.JSONDecodeError:
|
||||
payload = {}
|
||||
draft = _draft_for(payload.get('draft', ''))
|
||||
if draft is None:
|
||||
return JsonResponse({'error': 'Draft not found'}, status=404)
|
||||
data = payload.get('data') or {}
|
||||
clean = {k: str(data.get(k, '')) for k in DRAFT_KEYS if k in data}
|
||||
draft, _ = ProjectDraft.objects.get_or_create(user=request.user)
|
||||
draft.data = clean
|
||||
draft.save()
|
||||
return JsonResponse({
|
||||
@@ -1735,6 +1808,12 @@ def api_draft(request):
|
||||
'updated_at': draft.updated_at.isoformat(),
|
||||
})
|
||||
if request.method == 'DELETE':
|
||||
ProjectDraft.objects.filter(user=request.user).delete()
|
||||
draft = _draft_for(request.GET.get('draft', ''))
|
||||
if draft is None:
|
||||
return JsonResponse({'error': 'Draft not found'}, status=404)
|
||||
for temp in TempUpload.objects.filter(draft_uuid=draft.uuid):
|
||||
delete_file_index(temp.file)
|
||||
TempUpload.objects.filter(draft_uuid=draft.uuid).delete()
|
||||
draft.delete()
|
||||
return JsonResponse({'success': True})
|
||||
return JsonResponse({'error': 'Method not allowed'}, status=405)
|
||||
|
||||
Reference in New Issue
Block a user