import os
import plistlib
from codecs import getreader
from typing import Optional
from django import forms
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
from django.core.management import call_command
from django.forms.widgets import RadioSelect
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views.generic import (
CreateView,
DetailView,
FormView,
ListView,
TemplateView,
View,
)
from django.views.generic.base import TemplateResponseMixin
from . import TrackSpecificMixin
from .js import JSApiMixin
from ..forms import LibraryUploadForm, MyriadExportUploadForm, NoteForm
from ..managers import TrackQuerySet
from ..models import Block, Note, Profile, Show, Track, TwitterUser, Vote
from ..myriad_export import entries_for_file
from ..update_library import MetadataChange, update_library
[docs]
class AdminMixin(LoginRequiredMixin):
"""
A mixin we should apply to all admin views.
"""
[docs]
def handle_validation_error(self, error):
context = {
'action': self.__doc__.strip('\n .'),
'error': error.error_dict,
}
return TemplateResponse(self.request, 'admin_error.html', context)
[docs]
@classmethod
def as_view(cls, **kw):
return user_passes_test(lambda u: u.is_staff)(super().as_view(**kw))
[docs]
class TrackSpecificAdminMixin(TrackSpecificMixin, AdminMixin):
pass
[docs]
class AdminActionMixin(AdminMixin):
url = reverse_lazy('vote:index')
[docs]
def get_redirect_url(self):
referer = self.request.META.get('HTTP_REFERER')
next_param = self.request.GET.get('next')
if next_param is not None:
self.url = next_param
elif referer is not None:
self.url = referer
return self.url
[docs]
def get_context_data(self, *args, **kwargs):
return {
**super().get_context_data(*args, **kwargs),
'next': self.get_redirect_url(),
}
[docs]
def get_ajax_success_message(self):
self.object = self.get_object()
context = self.get_context_data()
context.update(
{
'track': self.object,
'cache_invalidator': os.urandom(16),
}
)
return TemplateResponse(
self.request,
'include/track.html',
context,
)
[docs]
def do_thing_and_redirect(self):
try:
self.do_thing()
except ValidationError as e:
return self.handle_validation_error(e)
else:
if self.request.GET.get('ajax'):
return self.get_ajax_success_message()
return redirect(self.get_redirect_url())
[docs]
class AdminAction(AdminActionMixin):
"""
A view for an admin action that we can be comfortable doing immediately.
"""
[docs]
def get(self, request, *args, **kwargs):
return self.do_thing_and_redirect()
[docs]
class DestructiveAdminAction(AdminActionMixin, TemplateResponseMixin):
"""
A view for an admin action that's worth asking if our host is sure.
"""
template_name = 'confirm.html'
deets: Optional[str] = None
[docs]
def get_deets(self) -> Optional[str]:
return self.deets
[docs]
def get_cancel_url(self):
referer = self.request.META.get('HTTP_REFERER')
if referer:
cancel_url = referer
else:
cancel_url = reverse('vote:index')
return cancel_url
[docs]
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update(
{
'deets': self.get_deets(),
'action': self.__doc__.strip('\n .'),
'cancel_url': self.get_cancel_url(),
}
)
return context
[docs]
def get(self, request, *args, **kwargs):
self.request = request
if request.GET.get('confirm') == 'true':
return self.do_thing_and_redirect()
else:
return super().get(request, *args, **kwargs)
[docs]
class SelectionAdminAction(AdminAction, View):
"""
Do something with the current selection and wipe it.
"""
model = Track
fmt = u'{} modified'
[docs]
def get_queryset(self):
pks = self.request.session['selection'] or []
return self.model.objects.filter(pk__in=pks)
[docs]
def do_thing_and_redirect(self):
# we only want to clear the selection if the rest of this process is
# successful, or shillito will get understandably mad
resp = super().do_thing_and_redirect()
count = self.get_queryset().count()
tracks = (u'{} track' if count == 1 else u'{} tracks').format(count)
messages.success(self.request, self.fmt.format(tracks))
self.request.session['selection'] = []
return resp
[docs]
class Play(DestructiveAdminAction, DetailView):
"""
Mark this track as played.
"""
model = Track
[docs]
def get_deets(self) -> str:
return str(self.get_object())
[docs]
def do_thing(self) -> None:
self.get_object().play()
[docs]
def get_redirect_url(self) -> str:
return reverse(
'vote:admin:post_about_play', kwargs={'pk': self.get_object().pk}
)
[docs]
class PostAboutPlay(TrackSpecificAdminMixin, TemplateView):
template_name = 'post_about_play.html'
[docs]
class Archive(AdminAction, DetailView):
model = Track
[docs]
def do_thing(self):
self.get_object().archive()
messages.success(self.request, u"'{}' archived".format(self.get_object().title))
[docs]
class Unarchive(AdminAction, DetailView):
model = Track
[docs]
def do_thing(self):
self.get_object().unarchive()
messages.success(
self.request, u"'{}' unarchived".format(self.get_object().title)
)
[docs]
class Hide(AdminAction, DetailView):
model = Track
[docs]
def do_thing(self):
self.get_object().hide()
messages.success(self.request, u"'{}' hidden".format(self.get_object().title))
[docs]
class Unhide(AdminAction, DetailView):
model = Track
[docs]
def do_thing(self):
self.get_object().unhide()
messages.success(self.request, u"'{}' unhidden".format(self.get_object().title))
[docs]
class ManualVote(TrackSpecificAdminMixin, CreateView):
model = Vote
fields = ['text', 'name', 'kind']
template_name = 'manual_vote.html'
[docs]
class MakeBlock(TrackSpecificAdminMixin, CreateView):
"""
Block a track.
"""
model = Block
fields = ['reason']
template_name = 'block.html'
[docs]
class Unblock(AdminAction, DetailView):
model = Track
[docs]
def do_thing(self):
block = get_object_or_404(Block, track=self.get_object(), show=Show.current())
messages.success(
self.request, u"'{}' unblocked".format(self.get_object().title)
)
block.delete()
[docs]
class MakeBlockWithReason(AdminAction, DetailView):
"""
Block a track for a particular reason.
"""
model = Track
[docs]
def do_thing(self):
Block(
reason=self.request.GET.get('reason'),
track=self.get_object(),
show=Show.current(),
).save()
messages.success(self.request, u"'{}' blocked".format(self.get_object().title))
[docs]
class MakeShortlist(AdminAction, DetailView):
"""
Add a track to the shortlist.
"""
model = Track
[docs]
def do_thing(self):
self.get_object().shortlist()
messages.success(
self.request, u"'{}' shortlisted".format(self.get_object().title)
)
[docs]
class MakeDiscard(AdminAction, DetailView):
"""
Discard a track.
"""
model = Track
[docs]
def do_thing(self):
self.get_object().discard()
messages.success(
self.request, u"'{}' discarded".format(self.get_object().title)
)
[docs]
class OrderShortlist(AdminMixin, JSApiMixin, View):
[docs]
def do_thing(self, post):
current = Show.current()
indescriminate_shortlist = current.shortlist_set.all()
shortlisted = current.shortlisted()
pks = post.getlist('shortlist[]')
min_placeholder_index = max([s.index for s in indescriminate_shortlist]) + 1
if set(pks) != set([t.pk for t in shortlisted]):
return HttpResponse('reload')
# delete shortlists that have been played
indescriminate_shortlist.exclude(track__in=shortlisted).filter(
track__in=current.playlist()
).delete()
for delta in [min_placeholder_index, 0]:
for index, pk in enumerate(pks):
shortlist = indescriminate_shortlist.get(track__pk=pk)
shortlist.index = index + delta
shortlist.save()
[docs]
class ResetShortlistAndDiscard(AdminAction, DetailView):
model = Track
[docs]
def do_thing(self):
self.get_object().reset_shortlist_discard()
messages.success(self.request, u"'{}' reset".format(self.get_object().title))
[docs]
class LibraryUploadView(AdminMixin, FormView):
template_name = 'upload.html'
form_class = LibraryUploadForm
[docs]
class LibraryUploadConfirmView(DestructiveAdminAction, TemplateView):
"""
Update the library.
"""
template_name = 'library_update.html'
[docs]
def update_library(self, dry_run: bool) -> list[MetadataChange]:
library_update = self.request.session['library_update']
return update_library(
plistlib.loads(library_update['library_xml'].encode()),
inudesu=library_update['inudesu'],
dry_run=dry_run,
)
[docs]
def get_deets(self):
return self.update_library(dry_run=True)
[docs]
def do_thing(self):
changes = self.update_library(dry_run=False)
messages.success(self.request, 'library updated')
return changes
[docs]
class MyriadExportUploadView(AdminMixin, FormView):
template_name = 'upload_myriad_export.html'
form_class = MyriadExportUploadForm
[docs]
class ToggleAbuser(AdminAction, DetailView):
[docs]
def do_thing(self) -> None:
user = self.get_object()
user.is_abuser = not user.is_abuser
fmt = u"{} condemned" if user.is_abuser else u"{} redeemed"
messages.success(self.request, fmt.format(self.get_object()))
user.save()
[docs]
class ToggleLocalAbuser(ToggleAbuser):
model = Profile
[docs]
def get_object(self):
return self.model.objects.get(pk=self.kwargs['user_id'])
[docs]
class HiddenTracks(AdminMixin, ListView):
model = Track
template_name = 'hidden.html'
context_object_name = 'tracks'
[docs]
def get_queryset(self):
qs = self.model.objects.filter(hidden=True, inudesu=False)
return qs.order_by('-added')
[docs]
class ArchivedTracks(AdminMixin, ListView):
model = Track
template_name = 'archived.html'
context_object_name = 'tracks'
[docs]
def get_queryset(self):
qs = self.model.objects.filter(archived=True, inudesu=False)
return qs.order_by('-added')
[docs]
class InuDesuTracks(AdminMixin, ListView):
model = Track
template_name = 'inudesu.html'
context_object_name = 'tracks'
[docs]
def get_queryset(self):
return self.model.objects.filter(hidden=False, inudesu=True)
[docs]
class ArtlessTracks(AdminMixin, ListView):
model = Track
template_name = 'artless.html'
context_object_name = 'tracks'
paginate_by = 20
[docs]
def get_queryset(self):
return self.model.objects.filter(background_art='')
[docs]
class ShortlistSelection(SelectionAdminAction):
fmt = u'{} shortlisted'
[docs]
def do_thing(self) -> None:
for track in self.get_queryset():
track.shortlist()
[docs]
class ArchiveSelection(SelectionAdminAction):
fmt = u'{} archived'
[docs]
def do_thing(self) -> None:
for track in self.get_queryset():
track.archive()
[docs]
class UnarchiveSelection(SelectionAdminAction):
fmt = u'{} unarchived'
[docs]
def do_thing(self) -> None:
for track in self.get_queryset():
track.unarchive()
[docs]
class HideSelection(SelectionAdminAction):
fmt = u'{} hidden'
[docs]
def do_thing(self) -> None:
for track in self.get_queryset():
track.hide()
[docs]
class UnhideSelection(SelectionAdminAction):
fmt = u'{} unhidden'
[docs]
def do_thing(self) -> None:
for track in self.get_queryset():
track.unhide()
[docs]
class DiscardSelection(SelectionAdminAction):
fmt = u'{} discarded'
[docs]
def do_thing(self) -> None:
for track in self.get_queryset():
track.discard()
[docs]
class ResetShortlistAndDiscardSelection(SelectionAdminAction):
fmt = u'{} reset'
[docs]
def do_thing(self) -> None:
for track in self.get_queryset():
track.reset_shortlist_discard()
[docs]
class MakeNote(TrackSpecificAdminMixin, FormView):
template_name = 'note.html'
form_class = NoteForm
[docs]
class RemoveNote(DestructiveAdminAction, DetailView):
"""
Remove this note.
"""
model = Note
[docs]
def get_deets(self) -> str:
obj = self.get_object()
return '"{content}"; {track}'.format(
track=obj.track,
content=obj.content,
)
[docs]
def do_thing(self) -> None:
self.get_object().delete()
messages.success(self.request, 'note removed')
[docs]
class MigrateAwayFrom(TrackSpecificAdminMixin, FormView):
template_name = 'migrate_away_from.html'
[docs]
def get_possible_targets(self) -> TrackQuerySet:
return Track.objects.filter(hidden=True, inudesu=False).exclude(
pk=self.kwargs['pk']
)
[docs]
class Throw500(AdminMixin, DetailView):
[docs]
def dispatch(self, *args, **kwargs):
raise RuntimeError('throwing a 500 for you')