Phase 2.9 complete
This commit is contained in:
+198
-11
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import mimetypes
|
||||
import posixpath
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -31,8 +32,9 @@ from .models import (
|
||||
VersionFile,
|
||||
slugify_tag,
|
||||
)
|
||||
from .storage import delete_file_index, move_file_index, store_temp_file
|
||||
from .storage import delete_file_index, move_file_index, refresh_file_index, store_temp_file
|
||||
from .zips import (
|
||||
inject_animationframework,
|
||||
parse_mods_manifest,
|
||||
read_animation_manifest,
|
||||
read_logical_manifest,
|
||||
@@ -523,6 +525,126 @@ def _apply_content_tags(project, tag_names, actor):
|
||||
TagList.objects.get_or_create(project=project, tag=tag, defaults={'added_by': actor})
|
||||
|
||||
|
||||
def _established_animation_id(project):
|
||||
"""The animationframework.id of the project's newest version (its identity)."""
|
||||
latest = project.versions.first()
|
||||
return latest.animation_id if latest is not None else ''
|
||||
|
||||
|
||||
def _valid_id_component(value):
|
||||
return bool(re.match(r'^[a-zA-Z0-9_.-]+$', value or ''))
|
||||
|
||||
|
||||
def _animation_fields(project, version_name, animation_id):
|
||||
return {
|
||||
'id': animation_id,
|
||||
'name': project.title,
|
||||
'author': project.owner.username,
|
||||
'version': version_name,
|
||||
'description': project.summary or project.title,
|
||||
}
|
||||
|
||||
|
||||
def _inject_animation_metadata(request, project, version_name, temp_uploads):
|
||||
"""For NoN-pack zips missing an animationframework.id, rewrite the temp zip
|
||||
to add the block. Returns (errors, injected_count)."""
|
||||
if project.category != 'non_pack':
|
||||
return [], 0
|
||||
established = _established_animation_id(project)
|
||||
default_ns = slugify(project.owner.username) or 'site'
|
||||
default_pid = f'project_{project.pk}'
|
||||
ns = (request.POST.get('af_namespace') or default_ns).strip()
|
||||
pid = (request.POST.get('af_pack_id') or default_pid).strip()
|
||||
create_pack = {
|
||||
'pack_format': 64,
|
||||
'supported_formats': [64, 81],
|
||||
'min_format': 64,
|
||||
'max_format': 81,
|
||||
'description': project.title,
|
||||
}
|
||||
|
||||
errors = []
|
||||
injected = 0
|
||||
for temp in temp_uploads:
|
||||
if not (temp.file.original_filename or '').lower().endswith('.zip'):
|
||||
continue
|
||||
path = _stored_path(temp.file)
|
||||
meta = read_pack_meta(path)
|
||||
current = meta.get('animation_id') if meta else None
|
||||
|
||||
if current:
|
||||
if established and current != established and not request.POST.get('confirm_replace_identity'):
|
||||
errors.append(
|
||||
'The uploaded pack has a different animationframework.id than this project. '
|
||||
'Check "replace pack identity" to accept it.'
|
||||
)
|
||||
continue
|
||||
|
||||
desired = established or f'{ns}:{pid}'
|
||||
if not _valid_id_component(ns) or not _valid_id_component(pid):
|
||||
errors.append('Animation ID must only use letters, numbers, underscores, dots or dashes.')
|
||||
break
|
||||
if inject_animationframework(
|
||||
path, _animation_fields(project, version_name, desired), create_pack,
|
||||
):
|
||||
refresh_file_index(temp.file)
|
||||
injected += 1
|
||||
return errors, injected
|
||||
|
||||
|
||||
def _animation_meta_for_upload(request, project):
|
||||
"""Render hints for the version-upload page's Animation ID section."""
|
||||
if project.category != 'non_pack':
|
||||
return {'show': False}
|
||||
established = _established_animation_id(project)
|
||||
default_ns = slugify(project.owner.username) or 'site'
|
||||
default_pid = f'project_{project.pk}'
|
||||
show_fields = False
|
||||
show_confirm = False
|
||||
for temp in _pending_kind(request.user, 'version'):
|
||||
if not (temp.file.original_filename or '').lower().endswith('.zip'):
|
||||
continue
|
||||
meta = read_pack_meta(_stored_path(temp.file))
|
||||
current = meta.get('animation_id') if meta else None
|
||||
if not current and not established:
|
||||
show_fields = True
|
||||
if current and established and current != established:
|
||||
show_confirm = True
|
||||
if established:
|
||||
ns, _, pid = established.partition(':')
|
||||
default_ns = ns or default_ns
|
||||
default_pid = pid or default_pid
|
||||
return {
|
||||
'show': show_fields or show_confirm,
|
||||
'show_fields': show_fields,
|
||||
'show_confirm': show_confirm,
|
||||
'established': established,
|
||||
'namespace': default_ns,
|
||||
'pack_id': default_pid,
|
||||
}
|
||||
|
||||
|
||||
def _reparse_version(version):
|
||||
"""Re-derive a version's metadata (id + manifests) from its release zip."""
|
||||
release = version.files.filter(kind='release').first()
|
||||
if release is None:
|
||||
return
|
||||
path = _stored_path(release.file)
|
||||
meta = read_pack_meta(path)
|
||||
if meta and meta.get('animation_id'):
|
||||
version.animation_id = meta['animation_id']
|
||||
else:
|
||||
version.animation_id = ''
|
||||
version.animation_manifest = read_animation_manifest(path) or {}
|
||||
version.models_manifest = read_models_manifest(path) or {}
|
||||
version.logical_manifest = read_logical_manifest(path) or {}
|
||||
fmt, desc = read_pack_mcmeta(path)
|
||||
if fmt is not None:
|
||||
version.pack_format = fmt
|
||||
version.pack_description = desc
|
||||
version.save()
|
||||
|
||||
|
||||
def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
||||
"""Adopt pending 'version' temp uploads into a new Version as VersionFiles,
|
||||
capturing category/content-specific metadata (pack.mcmeta, mods, animation,
|
||||
@@ -648,6 +770,9 @@ def project_create(request):
|
||||
thumb.save(update_fields=['status'])
|
||||
project.save(update_fields=['thumbnail'])
|
||||
|
||||
_inject_animation_metadata(
|
||||
request, project, version_form.cleaned_data['version_name'], version_files,
|
||||
)
|
||||
_, missing_animation_id = _finalize_version(
|
||||
project,
|
||||
version_form.cleaned_data['version_name'],
|
||||
@@ -746,11 +871,20 @@ def project_edit(request, slug):
|
||||
else:
|
||||
pending_uploads = []
|
||||
|
||||
established = _established_animation_id(project)
|
||||
ns, _, pid = established.partition(':')
|
||||
anim_edit = {
|
||||
'current': established,
|
||||
'namespace': ns or (slugify(project.owner.username) or 'site'),
|
||||
'pack_id': pid or f'project_{project.pk}',
|
||||
}
|
||||
|
||||
return render(request, 'library/project_edit.html', {
|
||||
'project': project,
|
||||
'project_form': form,
|
||||
'pending_uploads': pending_uploads,
|
||||
'tag_categories': list(TagCategory.objects.order_by('slug').values('slug', 'color')),
|
||||
'anim_edit': anim_edit,
|
||||
})
|
||||
|
||||
|
||||
@@ -788,23 +922,30 @@ def version_upload(request, slug):
|
||||
if not version_files:
|
||||
form.add_error('version_name', 'Upload at least one version file before uploading.')
|
||||
else:
|
||||
_, missing_animation_id = _finalize_version(
|
||||
project,
|
||||
form.cleaned_data['version_name'],
|
||||
form.cleaned_data['changelog'],
|
||||
version_files,
|
||||
request.user,
|
||||
errors, _injected = _inject_animation_metadata(
|
||||
request, project, form.cleaned_data['version_name'], version_files,
|
||||
)
|
||||
if missing_animation_id:
|
||||
messages.warning(request, ANIMATION_API_WARNING)
|
||||
messages.success(request, f'Version {form.cleaned_data["version_name"]} uploaded.')
|
||||
return redirect('library:project_detail', slug=project.slug)
|
||||
for error in errors:
|
||||
form.add_error(None, error)
|
||||
if not errors:
|
||||
_, missing_animation_id = _finalize_version(
|
||||
project,
|
||||
form.cleaned_data['version_name'],
|
||||
form.cleaned_data['changelog'],
|
||||
version_files,
|
||||
request.user,
|
||||
)
|
||||
if missing_animation_id:
|
||||
messages.warning(request, ANIMATION_API_WARNING)
|
||||
messages.success(request, f'Version {form.cleaned_data["version_name"]} uploaded.')
|
||||
return redirect('library:project_detail', slug=project.slug)
|
||||
|
||||
version_files = _pending_kind(request.user, 'version')
|
||||
return render(request, 'library/version_upload.html', {
|
||||
'project': project,
|
||||
'version_form': form,
|
||||
'pending_uploads': [_serialize_temp(u) for u in version_files],
|
||||
'anim_meta': _animation_meta_for_upload(request, project),
|
||||
})
|
||||
|
||||
|
||||
@@ -815,6 +956,52 @@ def _bump_and_redirect(request, version, file_index):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def regenerate_metadata(request, slug):
|
||||
"""Rewrite the latest version's release zip so pack.mcmeta carries the
|
||||
chosen animationframework.id, then re-parse the version's metadata."""
|
||||
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':
|
||||
return HttpResponse(status=405)
|
||||
|
||||
latest = project.versions.first()
|
||||
if latest is None:
|
||||
messages.error(request, 'No versions to regenerate.')
|
||||
return redirect('library:project_edit', slug=slug)
|
||||
release = latest.files.filter(kind='release').first()
|
||||
if release is None:
|
||||
messages.error(request, 'No release file to regenerate.')
|
||||
return redirect('library:project_edit', slug=slug)
|
||||
|
||||
ns = request.POST.get('af_namespace', '').strip()
|
||||
pid = request.POST.get('af_pack_id', '').strip()
|
||||
if not _valid_id_component(ns) or not _valid_id_component(pid):
|
||||
messages.error(request, 'Animation ID must only use letters, numbers, underscores, dots or dashes.')
|
||||
return redirect('library:project_edit', slug=slug)
|
||||
|
||||
animation_id = f'{ns}:{pid}'
|
||||
create_pack = {
|
||||
'pack_format': 64,
|
||||
'supported_formats': [64, 81],
|
||||
'min_format': 64,
|
||||
'max_format': 81,
|
||||
'description': project.title,
|
||||
}
|
||||
path = _stored_path(release.file)
|
||||
if not inject_animationframework(
|
||||
path, _animation_fields(project, latest.version_name, animation_id), create_pack,
|
||||
):
|
||||
messages.error(request, 'Could not regenerate pack metadata.')
|
||||
return redirect('library:project_edit', slug=slug)
|
||||
|
||||
refresh_file_index(release.file)
|
||||
_reparse_version(latest)
|
||||
messages.success(request, f'Pack metadata regenerated with id {animation_id}.')
|
||||
return redirect('library:project_edit', slug=slug)
|
||||
|
||||
|
||||
def version_download(request, slug, version_id):
|
||||
"""Download the first release file of a version (kept for API/mods)."""
|
||||
version = get_object_or_404(
|
||||
|
||||
Reference in New Issue
Block a user