lib.itmens/catalog/views_edit.py

381 lines
14 KiB
Python
Raw Normal View History

from auditlog.context import set_actor
from django.contrib import messages
2024-01-27 15:21:30 -05:00
from django.contrib.auth.decorators import login_required
from django.core.exceptions import BadRequest, PermissionDenied
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
2023-06-19 15:40:36 -04:00
from django.utils import timezone
2024-06-07 22:29:10 -04:00
from django.utils.translation import gettext as _
2024-01-27 15:21:30 -05:00
from django.views.decorators.http import require_http_methods
2024-05-25 23:38:11 -04:00
from loguru import logger
2024-04-07 20:18:01 -04:00
from common.utils import discord_send, get_uuid_or_404
2024-06-20 14:54:46 -04:00
from journal.models import update_journal_for_merged_item_task
from .common.models import ExternalResource, IdealIdTypes, IdType
2023-06-19 15:40:36 -04:00
from .forms import *
from .models import *
2023-06-19 15:40:36 -04:00
from .search.views import *
2023-12-29 16:03:31 -05:00
from .sites.imdb import IMDB as IMDB
2023-06-19 15:40:36 -04:00
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
2024-01-27 15:21:30 -05:00
@require_http_methods(["GET", "POST"])
2023-06-19 15:40:36 -04:00
@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.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")
2024-01-27 15:21:30 -05:00
@require_http_methods(["GET"])
2023-06-19 15:40:36 -04:00
@login_required
def history(request, item_path, item_uuid):
2024-01-27 15:21:30 -05:00
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
return render(request, "catalog_history.html", {"item": item})
2023-06-19 15:40:36 -04:00
2024-01-27 15:21:30 -05:00
@require_http_methods(["GET", "POST"])
2023-06-19 15:40:36 -04:00
@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 (
2023-07-01 13:13:39 -04:00
not request.user.is_staff
and item.external_resources.all().count() > 0
2023-06-19 15:40:36 -04:00
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})
2024-04-23 23:57:49 -04:00
else:
2023-06-19 15:40:36 -04:00
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 (
2023-07-01 13:13:39 -04:00
not request.user.is_staff
and item.external_resources.all().count() > 0
2023-06-19 15:40:36 -04:00
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.edited_time = timezone.now()
form.instance.save()
return redirect(form.instance.url)
else:
raise BadRequest(_add_error_map_detail(form.errors))
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
2023-06-19 15:40:36 -04:00
@login_required
def delete(request, item_path, item_uuid):
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
if not request.user.is_staff and item.journal_exists():
2024-07-16 17:49:10 -04:00
raise PermissionDenied(_("Item in use."))
if not item.can_soft_delete():
raise PermissionDenied(_("Item cannot be deleted."))
2024-04-07 19:44:37 -04:00
if request.POST.get("sure", 0) != "1":
return render(request, "catalog_delete.html", {"item": item})
else:
item.delete()
2024-04-07 20:18:01 -04:00
discord_send(
"audit",
f"{item.absolute_url}?skipcheck=1\nby [@{request.user.username}]({request.user.absolute_url})",
thread_name=f"[delete] {item.display_title}",
username=f"@{request.user.username}",
)
2024-04-07 19:44:37 -04:00
return (
redirect(item.url + "?skipcheck=1")
if request.user.is_staff
else redirect("/")
)
2023-06-19 15:40:36 -04:00
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
@login_required
def undelete(request, item_path, item_uuid):
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
if not request.user.is_staff:
2024-04-23 23:57:49 -04:00
raise PermissionDenied(_("Insufficient permission"))
item.is_deleted = False
item.save()
return redirect(item.url)
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
2023-06-19 15:40:36 -04:00
@login_required
def recast(request, item_path, item_uuid):
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")
2024-05-25 23:38:11 -04:00
logger.warning(f"{request.user} recasting {item} to {model}")
2024-04-07 20:18:01 -04:00
discord_send(
"audit",
f"{item.absolute_url}\n{item.__class__.__name__}{model.__name__}\nby [@{request.user.username}]({request.user.absolute_url})",
thread_name=f"[recast] {item.display_title}",
username=f"@{request.user.username}",
)
2023-06-19 15:40:36 -04:00
if isinstance(item, TVShow):
for season in item.seasons.all():
2024-05-25 23:38:11 -04:00
logger.warning(f"{request.user} recast orphaning season {season}")
2023-06-19 15:40:36 -04:00
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)
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
2023-06-19 15:40:36 -04:00
@login_required
def unlink(request):
if not request.user.is_staff:
2024-04-23 23:57:49 -04:00
raise PermissionDenied(_("Insufficient permission"))
2023-06-19 15:40:36 -04:00
res_id = request.POST.get("id")
if not res_id:
2024-04-23 23:57:49 -04:00
raise BadRequest(_("Invalid parameter"))
2023-06-19 15:40:36 -04:00
resource = get_object_or_404(ExternalResource, id=res_id)
resource.unlink_from_item()
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
2023-06-19 15:40:36 -04:00
@login_required
def assign_parent(request, item_path, item_uuid):
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"))
2023-06-25 11:48:09 -04:00
if parent_item:
if 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")
2024-05-25 23:38:11 -04:00
logger.warning(f"{request.user} assign {item} to {parent_item}")
2023-06-19 15:40:36 -04:00
item.set_parent_item(parent_item)
item.save()
return redirect(item.url)
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
2023-06-19 15:40:36 -04:00
@login_required
def remove_unused_seasons(request, item_path, item_uuid):
2024-05-27 15:44:12 -04:00
item = get_object_or_404(TVShow, uid=get_uuid_or_404(item_uuid))
2024-04-06 00:13:50 -04:00
sl = list(item.seasons.all())
for s in sl:
2023-06-19 15:40:36 -04:00
if not s.journal_exists():
s.delete()
2024-05-27 15:44:12 -04:00
ol = [s.pk for s in sl]
nl = [s.pk for s in item.seasons.all()]
2024-04-07 20:18:01 -04:00
discord_send(
"audit",
f"{item.absolute_url}\n{ol}{nl}\nby [@{request.user.username}]({request.user.absolute_url})",
thread_name=f"[cleanup] {item.display_title}",
username=f"@{request.user.username}",
)
2024-04-06 00:13:50 -04:00
item.log_action({"!remove_unused_seasons": [ol, nl]})
2023-06-19 15:40:36 -04:00
return redirect(item.url)
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
2023-06-19 15:40:36 -04:00
@login_required
def fetch_tvepisodes(request, item_path, item_uuid):
2024-05-27 15:44:12 -04:00
item = get_object_or_404(TVSeason, uid=get_uuid_or_404(item_uuid))
2023-06-19 15:40:36 -04:00
if item.class_name != "tvseason" or not item.imdb or item.season_number is None:
2024-05-28 09:58:47 -04:00
raise BadRequest(_("TV Season with IMDB id and season number required."))
2023-06-19 15:40:36 -04:00
item.log_action({"!fetch_tvepisodes": ["", ""]})
2023-06-20 12:19:10 -04:00
django_rq.get_queue("crawl").enqueue(
fetch_episodes_for_season_task, item.uuid, request.user
)
2024-03-10 20:55:50 -04:00
messages.add_message(request, messages.INFO, _("Updating episodes"))
2023-06-19 15:40:36 -04:00
return redirect(item.url)
2023-06-20 12:19:10 -04:00
def fetch_episodes_for_season_task(item_uuid, user):
with set_actor(user):
2024-05-27 15:44:12 -04:00
season = TVSeason.get_by_url(item_uuid)
if not season:
return
2023-06-20 12:19:10 -04:00
episodes = season.episode_uuids
IMDB.fetch_episodes_for_season(season)
season.log_action({"!fetch_tvepisodes": [episodes, season.episode_uuids]})
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
2023-06-19 15:40:36 -04:00
@login_required
def merge(request, item_path, item_uuid):
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
if not request.user.is_staff and item.journal_exists():
2024-04-23 23:57:49 -04:00
raise PermissionDenied(_("Insufficient permission"))
2024-04-07 19:44:37 -04:00
if request.POST.get("sure", 0) != "1":
new_item = Item.get_by_url(request.POST.get("target_item_url"))
return render(
2024-04-09 00:45:40 -04:00
request,
"catalog_merge.html",
{"item": item, "new_item": new_item, "mode": "merge"},
2024-04-07 19:44:37 -04:00
)
elif request.POST.get("target_item_url"):
new_item = Item.get_by_url(request.POST.get("target_item_url"))
2023-06-19 15:40:36 -04:00
if not new_item or new_item.is_deleted or new_item.merged_to_item_id:
2024-03-10 20:55:50 -04:00
raise BadRequest(_("Cannot be merged to an item already deleted or merged"))
2023-06-19 15:40:36 -04:00
if new_item.class_name != item.class_name:
raise BadRequest(
2024-03-10 20:55:50 -04:00
_("Cannot merge items in different categories")
+ f" ({item.class_name} to {new_item.class_name})"
2023-06-19 15:40:36 -04:00
)
2025-01-28 21:52:14 -05:00
if new_item == item:
raise BadRequest(_("Cannot merge an item to itself"))
2024-05-25 23:38:11 -04:00
logger.warning(f"{request.user} merges {item} to {new_item}")
2023-06-19 15:40:36 -04:00
item.merge_to(new_item)
2024-06-20 14:54:46 -04:00
django_rq.get_queue("crawl").enqueue(
update_journal_for_merged_item_task, request.user.pk, item.uuid
)
2024-04-07 20:18:01 -04:00
discord_send(
"audit",
f"{item.absolute_url}?skipcheck=1\n\n{new_item.absolute_url}\nby [@{request.user.username}]({request.user.absolute_url})",
thread_name=f"[merge] {item.display_title}",
username=f"@{request.user.username}",
)
2023-06-19 15:40:36 -04:00
return redirect(new_item.url)
else:
if item.merged_to_item:
2024-05-25 23:38:11 -04:00
logger.warning(f"{request.user} cancels merge for {item}")
2023-06-19 15:40:36 -04:00
item.merge_to(None)
2024-04-07 20:18:01 -04:00
discord_send(
"audit",
f"{item.absolute_url}\n\n(none)\nby [@{request.user.username}]({request.user.absolute_url})",
thread_name=f"[merge] {item.display_title}",
username=f"@{request.user.username}",
)
2023-06-19 15:40:36 -04:00
return redirect(item.url)
2024-01-27 15:21:30 -05:00
2024-04-09 00:45:40 -04:00
@require_http_methods(["POST"])
@login_required
def link_edition(request, item_path, item_uuid):
2024-05-27 15:44:12 -04:00
item = get_object_or_404(Edition, uid=get_uuid_or_404(item_uuid))
new_item = Edition.get_by_url(request.POST.get("target_item_url"))
2024-04-09 00:45:40 -04:00
if (
not new_item
or new_item.is_deleted
or new_item.merged_to_item_id
or item == new_item
):
raise BadRequest(_("Cannot be linked to an item already deleted or merged"))
if item.class_name != "edition" or new_item.class_name != "edition":
raise BadRequest(_("Cannot link items other than editions"))
if request.POST.get("sure", 0) != "1":
2024-05-27 15:44:12 -04:00
new_item = Edition.get_by_url(request.POST.get("target_item_url")) # type: ignore
2024-04-09 00:45:40 -04:00
return render(
request,
"catalog_merge.html",
{"item": item, "new_item": new_item, "mode": "link"},
)
2024-05-25 23:38:11 -04:00
logger.warning(f"{request.user} merges {item} to {new_item}")
2024-04-09 00:45:40 -04:00
item.link_to_related_book(new_item)
discord_send(
"audit",
f"{item.absolute_url}?skipcheck=1\n\n{new_item.absolute_url}\nby [@{request.user.username}]({request.user.absolute_url})",
thread_name=f"[link edition] {item.display_title}",
username=f"@{request.user.username}",
)
return redirect(item.url)
@require_http_methods(["POST"])
@login_required
def unlink_works(request, item_path, item_uuid):
2024-05-27 15:44:12 -04:00
item = get_object_or_404(Edition, uid=get_uuid_or_404(item_uuid))
2024-04-09 00:45:40 -04:00
if not request.user.is_staff and item.journal_exists():
2024-04-23 23:57:49 -04:00
raise PermissionDenied(_("Insufficient permission"))
2024-04-09 00:45:40 -04:00
item.unlink_from_all_works()
discord_send(
"audit",
f"{item.absolute_url}?skipcheck=1\nby [@{request.user.username}]({request.user.absolute_url})",
thread_name=f"[unlink works] {item.display_title}",
username=f"@{request.user.username}",
)
return (
redirect(item.url + "?skipcheck=1") if request.user.is_staff else redirect("/")
)
2024-01-27 15:21:30 -05:00
@require_http_methods(["POST"])
@login_required
def suggest(request, item_path, item_uuid):
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
2024-04-07 20:18:01 -04:00
if not discord_send(
"suggest",
2024-02-01 23:33:50 -05:00
f"{item.absolute_url}\n> {request.POST.get('detail', '<none>')}\nby [@{request.user.username}]({request.user.absolute_url})",
thread_name=f"[{request.POST.get('action', 'none')}] {item.display_title}",
username=f"@{request.user.username}",
2024-04-07 20:18:01 -04:00
):
raise Http404("Discord webhook not configured")
2024-01-27 15:21:30 -05:00
return redirect(item.url)