Drafts system implemented

This commit is contained in:
JakeBreath
2026-08-05 17:16:09 -05:00
parent 203ff88af1
commit 49c4ff681f
13 changed files with 419 additions and 75 deletions
+118 -39
View File
@@ -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)