diff --git a/catalog/templates/tvepisode.html b/catalog/templates/tvepisode.html new file mode 100644 index 00000000..6ee5ff36 --- /dev/null +++ b/catalog/templates/tvepisode.html @@ -0,0 +1,15 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} +{% block head %} + +{% endblock %} diff --git a/catalog/views_edit.py b/catalog/views_edit.py new file mode 100644 index 00000000..a56b2c07 --- /dev/null +++ b/catalog/views_edit.py @@ -0,0 +1,250 @@ +import logging +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.decorators import login_required, permission_required +from django.utils.translation import gettext_lazy as _ +from django.http import HttpResponseRedirect +from django.core.exceptions import BadRequest, PermissionDenied, ObjectDoesNotExist +from django.utils import timezone +from django.contrib import messages +from .common.models import ExternalResource, IdType, IdealIdTypes +from .sites.imdb import IMDB +from .models import * +from .forms import * +from .search.views import * +from journal.models import update_journal_for_merged_item +from common.utils import get_uuid_or_404 + +_logger = logging.getLogger(__name__) + + +def _add_error_map_detail(e): + e.additonal_detail = [] + for f, v in e.as_data().items(): + for validation_error in v: + if hasattr(validation_error, "error_map") and validation_error.error_map: + for f2, v2 in validation_error.error_map.items(): + e.additonal_detail.append(f"{f}§{f2}: {'; '.join(v2)}") + return e + + +@login_required +def create(request, item_model): + form_cls = CatalogForms.get(item_model) + if not form_cls: + raise BadRequest("Invalid item type") + if request.method == "GET": + form = form_cls( + initial={ + "title": request.GET.get("title", ""), + } + ) + return render( + request, + "catalog_edit.html", + { + "form": form, + }, + ) + elif request.method == "POST": + form = form_cls(request.POST, request.FILES) + parent = None + if request.GET.get("parent", ""): + parent = get_object_or_404( + Item, uid=get_uuid_or_404(request.GET.get("parent", "")) + ) + if parent.child_class != form.instance.__class__.__name__: + raise BadRequest( + f"Invalid parent type: {form.instance.__class__} -> {parent.__class__}" + ) + if form.is_valid(): + form.instance.last_editor = request.user + form.instance.edited_time = timezone.now() + if parent: + form.instance.set_parent_item(parent) + form.instance.save() + return redirect(form.instance.url) + else: + raise BadRequest(_add_error_map_detail(form.errors)) + else: + raise BadRequest("Invalid request method") + + +@login_required +def history(request, item_path, item_uuid): + if request.method == "GET": + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + return render(request, "catalog_history.html", {"item": item}) + else: + raise BadRequest() + + +@login_required +def edit(request, item_path, item_uuid): + if request.method == "GET": + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + form_cls = CatalogForms[item.__class__.__name__] + form = form_cls(instance=item) + if ( + item.external_resources.all().count() > 0 + and item.primary_lookup_id_value + and item.primary_lookup_id_type in IdealIdTypes + ): + form.fields["primary_lookup_id_type"].disabled = True + form.fields["primary_lookup_id_value"].disabled = True + return render(request, "catalog_edit.html", {"form": form, "item": item}) + elif request.method == "POST": + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + form_cls = CatalogForms[item.__class__.__name__] + form = form_cls(request.POST, request.FILES, instance=item) + if ( + item.external_resources.all().count() > 0 + and item.primary_lookup_id_value + and item.primary_lookup_id_type in IdealIdTypes + ): + form.fields["primary_lookup_id_type"].disabled = True + form.fields["primary_lookup_id_value"].disabled = True + if form.is_valid(): + form.instance.last_editor = request.user + form.instance.edited_time = timezone.now() + form.instance.save() + return redirect(form.instance.url) + else: + raise BadRequest(_add_error_map_detail(form.errors)) + else: + raise BadRequest() + + +@login_required +def delete(request, item_path, item_uuid): + if request.method != "POST": + raise BadRequest() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if not request.user.is_staff and item.journal_exists(): + raise PermissionDenied() + item.delete() + return ( + redirect(item.url + "?skipcheck=1") if request.user.is_staff else redirect("/") + ) + + +@login_required +def recast(request, item_path, item_uuid): + if request.method != "POST": + raise BadRequest() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + cls = request.POST.get("class") + # TODO move some of the logic to model + douban_movie_to_tvseason = False + if cls == "tvshow": + if item.external_resources.filter(id_type=IdType.DoubanMovie).exists(): + cls = "tvseason" + douban_movie_to_tvseason = True + model = ( + TVShow + if cls == "tvshow" + else (Movie if cls == "movie" else (TVSeason if cls == "tvseason" else None)) + ) + if not model: + raise BadRequest("Invalid target type") + if isinstance(item, model): + raise BadRequest("Same target type") + _logger.warn(f"{request.user} recasting {item} to {model}") + if isinstance(item, TVShow): + for season in item.seasons.all(): + _logger.warn(f"{request.user} recast orphaning season {season}") + season.show = None + season.save(update_fields=["show"]) + new_item = item.recast_to(model) + if douban_movie_to_tvseason: + for res in item.external_resources.filter( + id_type__in=[IdType.IMDB, IdType.TMDB_TV] + ): + res.item = None + res.save(update_fields=["item"]) + return redirect(new_item.url) + + +@login_required +def unlink(request): + if request.method != "POST": + raise BadRequest() + if not request.user.is_staff: + raise PermissionDenied() + res_id = request.POST.get("id") + if not res_id: + raise BadRequest() + resource = get_object_or_404(ExternalResource, id=res_id) + resource.unlink_from_item() + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + +@login_required +def assign_parent(request, item_path, item_uuid): + if request.method != "POST": + raise BadRequest() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + parent_item = Item.get_by_url(request.POST.get("parent_item_url")) + if not parent_item or parent_item.is_deleted or parent_item.merged_to_item_id: + raise BadRequest("Can't assign parent to a deleted or redirected item") + if parent_item.child_class != item.__class__.__name__: + raise BadRequest("Incompatible child item type") + if not request.user.is_staff and item.parent_item: + raise BadRequest("Already assigned to a parent item") + _logger.warn(f"{request.user} assign {item} to {parent_item}") + item.set_parent_item(parent_item) + item.save() + return redirect(item.url) + + +@login_required +def remove_unused_seasons(request, item_path, item_uuid): + if request.method != "POST": + raise BadRequest() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + l = list(item.seasons.all()) + for s in l: + if not s.journal_exists(): + s.delete() + l = [s.id for s in l] + l2 = [s.id for s in item.seasons.all()] + item.log_action({"!remove_unused_seasons": [l, l2]}) + return redirect(item.url) + + +@login_required +def fetch_tvepisodes(request, item_path, item_uuid): + if request.method != "POST": + raise BadRequest() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if item.class_name != "tvseason" or not item.imdb or item.season_number is None: + raise BadRequest() + item.log_action({"!fetch_tvepisodes": ["", ""]}) + django_rq.get_queue("crawl").enqueue(IMDB.fetch_episodes_for_season, item.uuid) + messages.add_message(request, messages.INFO, _("已开始更新单集信息")) + return redirect(item.url) + + +@login_required +def merge(request, item_path, item_uuid): + if request.method != "POST": + raise BadRequest() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if not request.user.is_staff and item.journal_exists(): + raise PermissionDenied() + if request.POST.get("new_item_url"): + new_item = Item.get_by_url(request.POST.get("new_item_url")) + if not new_item or new_item.is_deleted or new_item.merged_to_item_id: + raise BadRequest(_("不能合并到一个被删除或合并过的条目。")) + if new_item.class_name != item.class_name: + raise BadRequest( + _("不能合并不同类的条目") + f" ({item.class_name} to {new_item.class_name})" + ) + _logger.warn(f"{request.user} merges {item} to {new_item}") + item.merge_to(new_item) + update_journal_for_merged_item(item_uuid) + return redirect(new_item.url) + else: + if item.merged_to_item: + _logger.warn(f"{request.user} cancels merge for {item}") + item.merge_to(None) + return redirect(item.url)