fixed implementation of UGC content uploader
This commit is contained in:
+599
-3
@@ -1,11 +1,33 @@
|
||||
import json
|
||||
import posixpath
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import FileResponse, Http404, HttpResponse, StreamingHttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count, F, Q, Sum
|
||||
from django.http import FileResponse, Http404, HttpResponse, HttpResponseForbidden, JsonResponse, StreamingHttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from .models import FileIndex
|
||||
from .forms import ContributorForm, ProjectForm, VersionForm
|
||||
from .models import (
|
||||
FileIndex,
|
||||
Project,
|
||||
ProjectAsset,
|
||||
ProjectContributor,
|
||||
ProjectDraft,
|
||||
Tag,
|
||||
TagCategory,
|
||||
TagList,
|
||||
TempUpload,
|
||||
Version,
|
||||
slugify_tag,
|
||||
)
|
||||
from .storage import delete_file_index, move_file_index, store_file, store_temp_file
|
||||
|
||||
|
||||
def _open_indexed_file(file_index):
|
||||
@@ -101,3 +123,577 @@ def file_request(request, file_id):
|
||||
if disposition:
|
||||
part['Content-Disposition'] = disposition
|
||||
return part
|
||||
|
||||
|
||||
def _require_can_edit(request, project):
|
||||
if not project.can_edit(request.user):
|
||||
raise HttpResponseForbidden('You do not have permission to edit this project.')
|
||||
return True
|
||||
|
||||
|
||||
def _unique_project_slug(title):
|
||||
base = slugify(title)[:100] or 'project'
|
||||
slug, n = base, 1
|
||||
while Project.objects.filter(slug=slug).exists():
|
||||
n += 1
|
||||
slug = f'{base}-{n}'
|
||||
return slug
|
||||
|
||||
|
||||
def _tags_from_text(text):
|
||||
"""Parse a raw tag string ('general:furry loader:fabric 1.20.1') into
|
||||
(category_slug, tag_name) tuples. Unprefixed names default to general."""
|
||||
pairs = []
|
||||
for part in (text or '').replace(',', ' ').split():
|
||||
if ':' in part:
|
||||
category_slug, _, name = part.partition(':')
|
||||
else:
|
||||
category_slug, name = 'general', part
|
||||
name = slugify_tag(name)
|
||||
if not name or not category_slug:
|
||||
continue
|
||||
pairs.append((category_slug, name))
|
||||
return pairs
|
||||
|
||||
|
||||
def _apply_tags(project, raw_tags, actor):
|
||||
"""Replace a project's manual tags (auto-creating new ones). Creator tags
|
||||
are managed separately by sync_creator_tags."""
|
||||
creator_cat = TagCategory.objects.filter(slug='creator').first()
|
||||
to_delete = project.tag_links.all()
|
||||
if creator_cat is not None:
|
||||
to_delete = to_delete.exclude(tag__category=creator_cat)
|
||||
to_delete.delete()
|
||||
|
||||
for category_slug, name in _tags_from_text(raw_tags):
|
||||
category = TagCategory.objects.filter(slug=category_slug).first()
|
||||
if category is None:
|
||||
category = TagCategory.objects.filter(slug='general').first()
|
||||
if category is None:
|
||||
continue
|
||||
tag, _ = Tag.objects.get_or_create(
|
||||
name=name, category=category, defaults={'created_by': actor},
|
||||
)
|
||||
TagList.objects.get_or_create(project=project, tag=tag, defaults={'added_by': actor})
|
||||
|
||||
|
||||
def _current_tags(project):
|
||||
"""Serialize a project's non-creator tags back to 'category:name' form."""
|
||||
creator_cat = TagCategory.objects.filter(slug='creator').first()
|
||||
qs = project.tag_links.select_related('tag__category').all()
|
||||
if creator_cat is not None:
|
||||
qs = qs.exclude(tag__category=creator_cat)
|
||||
return ' '.join(f'{tl.tag.category.slug}:{tl.tag.name}' for tl in qs)
|
||||
|
||||
|
||||
def browse(request):
|
||||
q = request.GET.get('q', '').strip()
|
||||
category = request.GET.get('category', '').strip()
|
||||
sort = request.GET.get('sort', 'recent')
|
||||
tag_names = [slugify_tag(t) for t in request.GET.getlist('tag') if t.strip()]
|
||||
|
||||
projects = (
|
||||
Project.objects.select_related('owner', 'thumbnail')
|
||||
.prefetch_related('tag_links__tag__category', 'versions')
|
||||
)
|
||||
|
||||
if q:
|
||||
projects = projects.filter(
|
||||
Q(title__icontains=q)
|
||||
| Q(summary__icontains=q)
|
||||
| Q(description__icontains=q)
|
||||
| Q(tag_links__tag__name__icontains=q)
|
||||
).distinct()
|
||||
|
||||
if category and category in dict(Project.CATEGORY_CHOICES):
|
||||
projects = projects.filter(category=category)
|
||||
else:
|
||||
category = ''
|
||||
|
||||
for name in tag_names:
|
||||
projects = projects.filter(tag_links__tag__name=name)
|
||||
|
||||
if sort == 'downloads':
|
||||
projects = projects.annotate(_downloads=Sum('versions__downloads')).order_by(
|
||||
F('_downloads').desc(nulls_last=True)
|
||||
)
|
||||
elif sort == 'name':
|
||||
projects = projects.order_by('title')
|
||||
else:
|
||||
projects = projects.order_by('-created_at')
|
||||
|
||||
# Preserve the query params when toggling filters.
|
||||
def _qs(**overrides):
|
||||
params = request.GET.copy()
|
||||
for key, value in overrides.items():
|
||||
if value is None:
|
||||
params.pop(key, None)
|
||||
else:
|
||||
params.setlist(key, value if isinstance(value, list) else [value])
|
||||
return params.urlencode()
|
||||
|
||||
category_menu = [('', 'All', f'?{_qs(category=None)}')]
|
||||
for value, label in Project.CATEGORY_CHOICES:
|
||||
category_menu.append((value, label, f'?{_qs(category=value)}'))
|
||||
|
||||
tag_groups = []
|
||||
for cat in TagCategory.objects.all().order_by('slug'):
|
||||
tags = (
|
||||
Tag.objects.filter(category=cat)
|
||||
.annotate(count=Count('project_links'))
|
||||
.filter(count__gt=0)
|
||||
.order_by('-count')[:25]
|
||||
)
|
||||
if not tags:
|
||||
continue
|
||||
items = []
|
||||
for tag in tags:
|
||||
active = tag.name in tag_names
|
||||
if active:
|
||||
remaining = [t for t in tag_names if t != tag.name]
|
||||
url = f'?{_qs(tag=remaining)}'
|
||||
else:
|
||||
url = f'?{_qs(tag=tag_names + [tag.name])}'
|
||||
items.append({
|
||||
'name': tag.name,
|
||||
'color': cat.color,
|
||||
'count': tag.count,
|
||||
'active': active,
|
||||
'url': url,
|
||||
})
|
||||
tag_groups.append({'category': cat, 'items': items})
|
||||
|
||||
paginator = Paginator(projects, 12)
|
||||
page_obj = paginator.get_page(request.GET.get('page'))
|
||||
|
||||
context = {
|
||||
'projects': page_obj,
|
||||
'q': q,
|
||||
'category': category,
|
||||
'sort': sort,
|
||||
'active_tags': tag_names,
|
||||
'category_menu': category_menu,
|
||||
'tag_groups': tag_groups,
|
||||
'query_string': _qs(),
|
||||
}
|
||||
return render(request, 'library/browse.html', context)
|
||||
|
||||
|
||||
def project_detail(request, slug):
|
||||
project = get_object_or_404(
|
||||
Project.objects.select_related('owner', 'thumbnail'), slug=slug,
|
||||
)
|
||||
versions = project.versions.select_related('file').all()
|
||||
assets = project.assets.select_related('file').all()
|
||||
tags = project.tag_links.select_related('tag__category').all()
|
||||
can_edit = project.can_edit(request.user)
|
||||
context = {
|
||||
'project': project,
|
||||
'versions': versions,
|
||||
'assets': assets,
|
||||
'tags': tags,
|
||||
'can_edit': can_edit,
|
||||
}
|
||||
return render(request, 'library/project_detail.html', context)
|
||||
|
||||
|
||||
DRAFT_KEYS = (
|
||||
'title', 'summary', 'category', 'description',
|
||||
'tags', 'caption', 'version_name', 'changelog',
|
||||
)
|
||||
|
||||
|
||||
def _serialize_temp(upload):
|
||||
return {
|
||||
'uuid': str(upload.uuid),
|
||||
'kind': upload.kind,
|
||||
'filename': upload.file.original_filename,
|
||||
'content_type': upload.file.content_type,
|
||||
'url': reverse('library:file_request', args=[upload.file.uuid]),
|
||||
}
|
||||
|
||||
|
||||
def _pending_uploads(user):
|
||||
return list(
|
||||
TempUpload.objects.filter(user=user, status='pending')
|
||||
.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 _adopt_temp(upload, project_pk, subdir, kind):
|
||||
"""Move a temp upload under project_<pk>/ and return the FileIndex."""
|
||||
base = posixpath.basename(upload.file.stored_path)
|
||||
new_path = f'project_{project_pk}/{subdir}/{base}' if subdir else f'project_{project_pk}/{base}'
|
||||
return move_file_index(upload.file, new_path, kind=kind)
|
||||
|
||||
|
||||
def _form_values(*forms):
|
||||
out = {}
|
||||
for form in forms:
|
||||
for name in form.fields:
|
||||
out[name] = form[name].value() or ''
|
||||
return out
|
||||
|
||||
|
||||
@login_required
|
||||
def project_create(request):
|
||||
project_form = ProjectForm()
|
||||
version_form = VersionForm(required_file=False)
|
||||
|
||||
if request.method == 'POST':
|
||||
project_form = ProjectForm(request.POST)
|
||||
version_form = VersionForm(request.POST, required_file=False)
|
||||
if project_form.is_valid() and version_form.is_valid():
|
||||
data = project_form.cleaned_data
|
||||
version_file = _latest_pending(request.user, 'version')
|
||||
if version_file is None:
|
||||
version_form.add_error('version_name', 'Upload the version file before creating the project.')
|
||||
else:
|
||||
project = Project.objects.create(
|
||||
slug=_unique_project_slug(data['title']),
|
||||
title=data['title'],
|
||||
summary=data['summary'],
|
||||
description=data['description'],
|
||||
category=data['category'],
|
||||
owner=request.user,
|
||||
)
|
||||
|
||||
thumb = _latest_pending(request.user, 'thumbnail')
|
||||
if thumb is not None:
|
||||
project.thumbnail = _adopt_temp(thumb, project.pk, '', 'thumbnail')
|
||||
thumb.status = 'used'
|
||||
thumb.save(update_fields=['status'])
|
||||
project.save(update_fields=['thumbnail'])
|
||||
|
||||
version_index = _adopt_temp(version_file, project.pk, 'versions', 'version')
|
||||
version_file.status = 'used'
|
||||
version_file.save(update_fields=['status'])
|
||||
Version.objects.create(
|
||||
project=project,
|
||||
version_name=version_form.cleaned_data['version_name'],
|
||||
file=version_index,
|
||||
changelog=version_form.cleaned_data['changelog'],
|
||||
)
|
||||
|
||||
caption = data.get('caption', '')
|
||||
for temp in TempUpload.objects.filter(
|
||||
user=request.user, status='pending', kind='media',
|
||||
).select_related('file').order_by('created_at'):
|
||||
index = _adopt_temp(temp, project.pk, 'gallery', 'asset')
|
||||
ProjectAsset.objects.create(
|
||||
project=project, file=index,
|
||||
caption=caption, uploaded_by=request.user,
|
||||
)
|
||||
temp.status = 'used'
|
||||
temp.save(update_fields=['status'])
|
||||
|
||||
_apply_tags(project, data['tags'], request.user)
|
||||
project.sync_creator_tags(actor=request.user)
|
||||
ProjectDraft.objects.filter(user=request.user).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
|
||||
},
|
||||
required_file=False,
|
||||
)
|
||||
|
||||
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_data': _form_values(project_form, version_form),
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def project_edit(request, slug):
|
||||
project = get_object_or_404(Project, slug=slug)
|
||||
if not project.can_edit(request.user):
|
||||
return HttpResponseForbidden('You do not have permission to edit this project.')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ProjectForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
project.title = data['title']
|
||||
project.summary = data['summary']
|
||||
project.description = data['description']
|
||||
project.category = data['category']
|
||||
thumbnail = data.get('thumbnail')
|
||||
if thumbnail:
|
||||
delete_file_index(project.thumbnail)
|
||||
project.thumbnail = store_file(
|
||||
request.user, 'thumbnail', thumbnail, project.pk, subdir='',
|
||||
)
|
||||
project.save()
|
||||
_apply_tags(project, data['tags'], request.user)
|
||||
project.sync_creator_tags(actor=request.user)
|
||||
messages.success(request, 'Project updated.')
|
||||
return redirect('library:project_detail', slug=project.slug)
|
||||
else:
|
||||
form = ProjectForm(initial={
|
||||
'title': project.title,
|
||||
'summary': project.summary,
|
||||
'description': project.description,
|
||||
'category': project.category,
|
||||
'tags': _current_tags(project),
|
||||
})
|
||||
return render(request, 'library/project_edit.html', {
|
||||
'project': project,
|
||||
'project_form': form,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def project_delete(request, slug):
|
||||
project = get_object_or_404(Project, slug=slug)
|
||||
if request.user != project.owner:
|
||||
return HttpResponseForbidden('Only the owner can delete this project.')
|
||||
if request.method == 'POST':
|
||||
indices = []
|
||||
if project.thumbnail_id:
|
||||
indices.append(project.thumbnail)
|
||||
indices += [v.file for v in project.versions.all() if v.file_id]
|
||||
indices += [a.file for a in project.assets.all() if a.file_id]
|
||||
project.delete()
|
||||
for index in indices:
|
||||
delete_file_index(index)
|
||||
messages.success(request, 'Project deleted.')
|
||||
return redirect('library:browse')
|
||||
return render(request, 'library/project_delete.html', {'project': project})
|
||||
|
||||
|
||||
@login_required
|
||||
def version_upload(request, slug):
|
||||
project = get_object_or_404(Project, slug=slug)
|
||||
if not project.can_edit(request.user):
|
||||
return HttpResponseForbidden('You do not have permission to upload versions.')
|
||||
|
||||
form = VersionForm()
|
||||
if request.method == 'POST':
|
||||
form = VersionForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
index = store_file(
|
||||
request.user, 'version', form.cleaned_data['file'],
|
||||
project.pk, subdir='versions',
|
||||
)
|
||||
Version.objects.create(
|
||||
project=project,
|
||||
version_name=form.cleaned_data['version_name'],
|
||||
file=index,
|
||||
changelog=form.cleaned_data['changelog'],
|
||||
)
|
||||
messages.success(request, f'Version {form.cleaned_data["version_name"]} uploaded.')
|
||||
return redirect('library:project_detail', slug=project.slug)
|
||||
return render(request, 'library/version_upload.html', {
|
||||
'project': project,
|
||||
'version_form': form,
|
||||
})
|
||||
|
||||
|
||||
def version_download(request, slug, version_id):
|
||||
version = get_object_or_404(
|
||||
Version.objects.select_related('file', 'project'),
|
||||
pk=version_id, project__slug=slug,
|
||||
)
|
||||
if version.file_id:
|
||||
Version.objects.filter(pk=version.pk).update(downloads=F('downloads') + 1)
|
||||
return redirect(
|
||||
reverse('library:file_request', args=[version.file.uuid]) + '?download=1'
|
||||
)
|
||||
raise Http404
|
||||
|
||||
|
||||
@login_required
|
||||
def asset_upload(request, slug):
|
||||
project = get_object_or_404(Project, slug=slug)
|
||||
if not project.can_edit(request.user):
|
||||
return HttpResponseForbidden('You do not have permission to add media.')
|
||||
|
||||
if request.method == 'POST':
|
||||
caption = request.POST.get('caption', '')
|
||||
media = TempUpload.objects.filter(user=request.user, status='pending', kind='media')
|
||||
if not media.exists():
|
||||
messages.error(request, 'No media uploaded yet.')
|
||||
else:
|
||||
count = 0
|
||||
for temp in media.select_related('file'):
|
||||
index = _adopt_temp(temp, project.pk, 'gallery', 'asset')
|
||||
ProjectAsset.objects.create(
|
||||
project=project, file=index,
|
||||
caption=caption, uploaded_by=request.user,
|
||||
)
|
||||
temp.status = 'used'
|
||||
temp.save(update_fields=['status'])
|
||||
count += 1
|
||||
messages.success(request, f'{count} media file{"s" if count != 1 else ""} added to the gallery.')
|
||||
return redirect('library:project_detail', slug=project.slug)
|
||||
|
||||
return render(request, 'library/asset_upload.html', {
|
||||
'project': project,
|
||||
'caption': request.POST.get('caption', ''),
|
||||
'pending_uploads': [_serialize_temp(u) for u in _pending_uploads(request.user)],
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def asset_delete(request, slug, asset_id):
|
||||
project = get_object_or_404(Project, slug=slug)
|
||||
asset = get_object_or_404(ProjectAsset, pk=asset_id, project=project)
|
||||
if not project.can_edit(request.user):
|
||||
return HttpResponseForbidden('You do not have permission to delete media.')
|
||||
delete_file_index(asset.file)
|
||||
asset.delete()
|
||||
return redirect('library:project_detail', slug=project.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def contributors(request, slug):
|
||||
project = get_object_or_404(Project, slug=slug)
|
||||
if request.user != project.owner:
|
||||
return HttpResponseForbidden('Only the owner can manage contributors.')
|
||||
|
||||
if request.method == 'POST':
|
||||
action = request.POST.get('action')
|
||||
if action == 'add':
|
||||
username = request.POST.get('username', '').strip()
|
||||
user = get_user_model().objects.filter(username__iexact=username).first()
|
||||
if user is None:
|
||||
messages.error(request, f'No account named "{username}".')
|
||||
elif user == project.owner:
|
||||
messages.error(request, 'The owner is already a contributor.')
|
||||
else:
|
||||
_, created = ProjectContributor.objects.get_or_create(
|
||||
project=project, user=user, defaults={'added_by': request.user},
|
||||
)
|
||||
project.sync_creator_tags(actor=request.user)
|
||||
if created:
|
||||
messages.success(request, f'{user.username} added as a contributor.')
|
||||
else:
|
||||
messages.info(request, f'{user.username} is already a contributor.')
|
||||
elif action == 'remove':
|
||||
contributor = ProjectContributor.objects.filter(
|
||||
pk=request.POST.get('contributor_id'), project=project,
|
||||
).first()
|
||||
if contributor is not None:
|
||||
user = contributor.user
|
||||
contributor.delete()
|
||||
project.detach_creator_tag(user)
|
||||
project.sync_creator_tags(actor=request.user)
|
||||
messages.success(request, f'{user.username} removed.')
|
||||
return redirect('library:contributors', slug=project.slug)
|
||||
|
||||
return render(request, 'library/contributors.html', {
|
||||
'project': project,
|
||||
'contributors': project.contributors.select_related('user').all(),
|
||||
'contributor_form': ContributorForm(),
|
||||
})
|
||||
|
||||
|
||||
# ---------- API (autocomplete) ----------
|
||||
|
||||
def api_tags_autocomplete(request):
|
||||
q = request.GET.get('q', '').strip()
|
||||
tags = Tag.objects.select_related('category').order_by('name')
|
||||
if q:
|
||||
tags = tags.filter(Q(name__icontains=q) | Q(category__name__icontains=q))
|
||||
data = [
|
||||
{'name': t.name, 'category': t.category.slug, 'category_name': t.category.name, 'color': t.category.color}
|
||||
for t in tags[:20]
|
||||
]
|
||||
return JsonResponse(data, safe=False)
|
||||
|
||||
|
||||
def api_users_autocomplete(request):
|
||||
q = request.GET.get('q', '').strip()
|
||||
users = get_user_model().objects.filter(username__icontains=q).order_by('username')[:20]
|
||||
return JsonResponse([{'username': u.username} for u in users], safe=False)
|
||||
|
||||
|
||||
# ---------- API (draft uploads + autosave) ----------
|
||||
|
||||
MEDIA_PREFIXES = ('image/', 'video/')
|
||||
|
||||
|
||||
@login_required
|
||||
def api_upload_temp(request):
|
||||
"""Accept one file (J621-style: the JS uploads each file individually)."""
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'error': 'Method not allowed'}, status=405)
|
||||
uploaded = request.FILES.get('file')
|
||||
if uploaded is None:
|
||||
return JsonResponse({'error': 'No file'}, status=400)
|
||||
|
||||
kind = request.POST.get('kind', 'media')
|
||||
if kind not in dict(TempUpload.KIND_CHOICES):
|
||||
kind = 'media'
|
||||
|
||||
content_type = getattr(uploaded, 'content_type', '') or ''
|
||||
if kind in ('thumbnail', 'media'):
|
||||
if not content_type.startswith(MEDIA_PREFIXES):
|
||||
return JsonResponse({'error': 'Only images, videos and gifs are allowed here.'}, status=400)
|
||||
if kind == 'thumbnail' and not content_type.startswith('image/'):
|
||||
return JsonResponse({'error': 'The thumbnail must be an image.'}, status=400)
|
||||
|
||||
# Thumbnail and version are single slots: discard any earlier pending one.
|
||||
if kind in ('thumbnail', 'version'):
|
||||
for old in TempUpload.objects.filter(user=request.user, status='pending', kind=kind):
|
||||
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)
|
||||
return JsonResponse(_serialize_temp(upload))
|
||||
|
||||
|
||||
@login_required
|
||||
def api_delete_temp(request, upload_uuid):
|
||||
upload = get_object_or_404(TempUpload, uuid=upload_uuid, user=request.user)
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'error': 'Method not allowed'}, status=405)
|
||||
if upload.status == 'pending':
|
||||
delete_file_index(upload.file)
|
||||
upload.delete()
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
|
||||
@login_required
|
||||
def api_draft(request):
|
||||
if request.method == 'GET':
|
||||
draft = ProjectDraft.objects.filter(user=request.user).first()
|
||||
return JsonResponse({
|
||||
'data': draft.data if draft else {},
|
||||
'uploads': [_serialize_temp(u) for u in _pending_uploads(request.user)],
|
||||
})
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
payload = json.loads(request.body or b'{}')
|
||||
except json.JSONDecodeError:
|
||||
payload = {}
|
||||
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({
|
||||
'saved': True,
|
||||
'updated_at': draft.updated_at.isoformat(),
|
||||
})
|
||||
if request.method == 'DELETE':
|
||||
ProjectDraft.objects.filter(user=request.user).delete()
|
||||
return JsonResponse({'success': True})
|
||||
return JsonResponse({'error': 'Method not allowed'}, status=405)
|
||||
|
||||
Reference in New Issue
Block a user