'''
Views for fiction_outlines.
'''
import logging
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.db import IntegrityError, transaction
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views import generic
from treebeard.exceptions import InvalidPosition, NodeAlreadySaved, PathOverflow, InvalidMoveToDescendant
from treebeard.forms import movenodeform_factory
from braces.views import SelectRelatedMixin, PrefetchRelatedMixin
from rules.contrib.views import PermissionRequiredMixin
from .models import Outline, Series, Character, CharacterInstance, Location, LocationInstance
from .models import Arc, ArcElementNode, StoryElementNode, ArcIntegrityError
from .signals import tree_manipulation
from . import forms
# Create your views here.
logger = logging.getLogger('fiction_outlines.view')
logger.setLevel(logging.DEBUG)
[docs]class SeriesListView(LoginRequiredMixin, generic.ListView):
'''
Generic view for viewing a list of series objects.
'''
model = Series
template_name = "fiction_outlines/series_list.html"
context_object_name = 'series_list'
def get_queryset(self):
return Series.objects.filter(user=self.request.user).prefetch_related(
'character_set', 'location_set', 'outline_set')
[docs]class SeriesCreateView(LoginRequiredMixin, generic.CreateView):
'''
Generic view for creating series object.
'''
model = Series
template_name = "fiction_outlines/series_create.html"
fields = ['title', 'description', 'tags']
success_url = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:series_detail', kwargs={'series': self.object.pk})
def form_valid(self, form):
'''
Override to ensure we can add the user to the record.
'''
form.instance.user = self.request.user
return super().form_valid(form)
[docs]class SeriesDetailView(LoginRequiredMixin, PermissionRequiredMixin, PrefetchRelatedMixin, generic.DetailView):
'''
Generic view to see series details.
'''
model = Series
template_name = 'fiction_outlines/series_detail.html'
prefetch_related = ['character_set', 'location_set', 'outline_set']
permission_required = 'fiction_outlines.view_series'
pk_url_kwarg = 'series'
context_object_name = 'series'
[docs]class SeriesUpdateView(LoginRequiredMixin, PermissionRequiredMixin, PrefetchRelatedMixin, generic.edit.UpdateView):
'''
Generic view for updating a series object.
'''
model = Series
template_name = 'fiction_outlines/series_update.html'
permission_required = 'fiction_outlines.edit_series'
fields = ['title', 'description', 'tags']
prefetch_related = ['character_set', 'location_set', 'outline_set']
success_url = None
pk_url_kwarg = 'series'
context_object_name = 'series'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:series_detail', kwargs={'series': self.object.id})
[docs]class SeriesDeleteView(LoginRequiredMixin, PermissionRequiredMixin, generic.edit.DeleteView):
'''
Generic view for deleting a series.
'''
model = Series
permission_required = 'fiction_outlines.delete_series'
template_name = 'fiction_outlines/series_delete.html'
success_url = reverse_lazy('fiction_outlines:series_list')
context_object_name = 'series'
pk_url_kwarg = 'series'
[docs]class CharacterListView(LoginRequiredMixin, generic.ListView):
'''
Generic view for viewing character list.
'''
model = Character
template_name = 'fiction_outlines/character_list.html'
context_object_name = 'character_list'
def get_queryset(self):
return Character.objects.filter(user=self.request.user).prefetch_related(
'characterinstance_set', 'characterinstance_set__outline', 'series')
[docs]class CharacterCreateView(LoginRequiredMixin, generic.CreateView):
'''
Generic view for creating a character.
'''
model = Character
template_name = "fiction_outlines/character_create.html"
form_class = forms.CharacterForm
success_url = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:character_detail', kwargs={'character': self.object.pk})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
[docs]class CharacterDetailView(LoginRequiredMixin, PermissionRequiredMixin,
PrefetchRelatedMixin, generic.DetailView):
'''
Generic view for character details.
'''
model = Character
template_name = 'fiction_outlines/character_detail.html'
permission_required = 'fiction_outlines.view_character'
prefetch_related = ['series', 'characterinstance_set', 'characterinstance_set__outline']
pk_url_kwarg = 'character'
context_object_name = 'character'
[docs]class CharacterUpdateView(LoginRequiredMixin, PermissionRequiredMixin, PrefetchRelatedMixin, generic.edit.UpdateView):
'''
Generic update view for character.
'''
model = Character
template_name = 'fiction_outlines/character_update.html'
permission_required = 'fiction_outlines.edit_character'
form_class = forms.CharacterForm
prefetch_related = ['series']
success_url = None
context_object_name = 'character'
pk_url_kwarg = 'character'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:character_detail', kwargs={'character': self.object.pk})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
[docs]class CharacterDeleteView(LoginRequiredMixin, PermissionRequiredMixin, PrefetchRelatedMixin, generic.edit.DeleteView):
'''
Generic view for deleting a character.
'''
model = Character
template_name = 'fiction_outlines/character_delete.html'
permission_required = 'fiction_outlines.delete_character'
success_url = reverse_lazy('fiction_outlines:character_list')
prefetch_related = ['series', 'characterinstance_set']
context_object_name = 'character'
pk_url_kwarg = 'character'
[docs]class CharacterInstanceListView(LoginRequiredMixin, PermissionRequiredMixin, generic.ListView):
'''
Generic view for seeing a list of all character instances for a particular character.
'''
model = CharacterInstance
permission_required = 'fiction_outlines.view_character'
template_name = 'fiction_outlines/character_instance_list.html'
context_object_name = 'character_instance_list'
def dispatch(self, request, *args, **kwargs):
self.character = get_object_or_404(Character, pk=kwargs['character'])
return super().dispatch(request, *args, **kwargs)
def get_permission_object(self):
return self.character
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['character'] = self.character
return context
def get_queryset(self):
return CharacterInstance.objects.filter(character=self.character).select_related(
'character', 'outline').prefetch_related('arcelementnode_set', 'storyelementnode_set')
[docs]class CharacterInstanceDetailView(LoginRequiredMixin, PermissionRequiredMixin,
SelectRelatedMixin, PrefetchRelatedMixin, generic.DetailView):
'''
Generic detail view for character instance.
'''
model = CharacterInstance
permission_required = 'fiction_outlines.view_character'
template_name = 'fiction_outlines/character_instance_detail.html'
select_related = ['character', 'outline']
prefetch_related = ['arcelementnode_set', 'storyelementnode_set']
pk_url_kwarg = 'instance'
context_object_name = 'character_instance'
def dispatch(self, request, *args, **kwargs):
self.character = get_object_or_404(Character, pk=kwargs['character'])
return super().dispatch(request, *args, **kwargs)
def get_permission_object(self):
return self.character
[docs]class CharacterInstanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, generic.CreateView):
'''
Generic create view for a character instance.
'''
model = CharacterInstance
permission_required = None
template_name = 'fiction_outlines/character_instance_create.html'
form_class = forms.CharacterInstanceForm
success_url = None
outline = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:character_instance_detail', kwargs={'characterint': self.object.pk})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['character'] = self.character
return kwargs
def dispatch(self, request, *args, **kwargs):
self.character = get_object_or_404(Character, pk=kwargs['character'])
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['character'] = self.character
return context
def has_permission(self):
if self.outline:
return (self.request.user.has_perm('fiction_outlines.edit_character', self.character) and
self.request.user.has_perm('fiction_outlines.edit_outline', self.outline))
return self.request.user.has_perm('fiction_outlines.edit_character', self.character)
def form_valid(self, form):
self.outline = form.instance.outline
form.instance.character = self.character
if not self.has_permission():
return HttpResponseForbidden()
return super().form_valid(form)
[docs]class CharacterInstanceUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
SelectRelatedMixin, generic.edit.UpdateView):
'''
Generic view for updating a character instance.
'''
model = CharacterInstance
permission_required = 'fiction_outlines.edit_character'
template_name = 'fiction_outlines/character_instance_update.html'
form_class = forms.CharacterInstanceForm
select_related = ['character', 'outline']
success_url = None
pk_url_kwarg = 'instance'
context_object_name = 'character_instance'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:character_instance_detail',
kwargs={'instance': self.object.pk, 'character': self.character})
def dispatch(self, request, *args, **kwargs):
self.character = get_object_or_404(Character, pk=kwargs['character'])
return super().dispatch(request, *args, **kwargs)
def get_permission_object(self):
return self.character
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['character'] = self.character
return kwargs
def form_valid(self, form):
outline = form.instance.outline
if not (self.request.user.has_perm('fiction_outlines.edit_character', self.character) and
self.request.user.has_perm('fiction_outlines.edit_outline', outline)):
return HttpResponseForbidden()
return super().form_valid(form)
[docs]class CharacterInstanceDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
SelectRelatedMixin, PrefetchRelatedMixin, generic.DeleteView):
'''
Generic view for deleting character instances.
'''
model = CharacterInstance
template_name = 'fiction_outlines/character_instance_delete.html'
permission_required = 'fiction_outlines.delete_character_instance'
success_url = None
select_related = ['character', 'outline']
prefetch_related = ['arcelementnode_set', 'storyelementnode_set']
context_object_name = 'character_instance'
pk_url_kwarg = 'instance'
def dispatch(self, request, *args, **kwargs):
self.character = get_object_or_404(Character, pk=kwargs['character'])
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:character_detail', kwargs={'character': self.character.pk})
[docs]class LocationListView(LoginRequiredMixin, generic.ListView):
'''
Generic view for locations.
'''
model = Location
template_name = 'fiction_outlines/location_list.html'
context_object_name = "location_list"
def get_queryset(self):
return Location.objects.filter(user=self.request.user)
[docs]class LocationDetailView(LoginRequiredMixin, PermissionRequiredMixin,
PrefetchRelatedMixin, generic.DetailView):
'''
Generic view for location details.
'''
model = Location
template_name = 'fiction_outlines/location_detail.html'
permission_required = 'fiction_outlines.view_location'
prefetch_related = ['series', 'locationinstance_set', 'locationinstance_set__outline']
pk_url_kwarg = 'location'
context_object_name = 'location'
[docs]class LocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, generic.edit.UpdateView):
'''
Generic view for updating locations.
'''
model = Location
permission_required = 'fiction_outlines.edit_location'
template_name = 'fiction_outlines/location_update.html'
form_class = forms.LocationForm
success_url = None
context_object_name = 'location'
pk_url_kwarg = 'location'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:location_detail', kwargs={'location': self.object.pk})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
[docs]class LocationCreateView(LoginRequiredMixin, generic.CreateView):
'''
Generic view for creating locations
'''
model = Location
template_name = 'fiction_outlines/location_create.html'
form_class = forms.LocationForm
success_url = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return self.object.get_absolute_url()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['allowed_series'] = Series.objects.filter(user=self.request.user)
return context
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
[docs]class LocationDeleteView(LoginRequiredMixin, PermissionRequiredMixin, generic.DeleteView):
'''
Generic view for deleting locations.
'''
model = Location
template_name = 'fiction_outlines/location_delete.html'
permission_required = 'fiction_outlines.delete_location'
success_url = None
context_object_name = 'location'
pk_url_kwarg = 'location'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:location_list')
[docs]class LocationInstanceListView(LoginRequiredMixin, PermissionRequiredMixin, generic.ListView):
'''
Generic view for looking at all location instances for a location.
'''
model = LocationInstance
template_name = 'fiction_outlines/location_instance_list.html'
permission_required = 'fiction_outlines.view_location'
context_object_name = 'location_instance_list'
def dispatch(self, request, *args, **kwargs):
self.location = get_object_or_404(Location, pk=kwargs['location'])
return super().dispatch(request, *args, **kwargs)
def get_permission_object(self):
return self.location
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['location'] = self.location
return context
def get_queryset(self):
return LocationInstance.objects.filter(location=self.location).select_related(
'location', 'outline').prefetch_related('arcelementnode_set', 'storyelementnode_set')
[docs]class LocationInstanceDetailView(LoginRequiredMixin, PermissionRequiredMixin,
SelectRelatedMixin, PrefetchRelatedMixin, generic.DetailView):
'''
Generic view for a location instance detail view.
'''
model = LocationInstance
template_name = 'fiction_outlines/location_instance_detail.html'
permission_required = 'fiction_outlines.view_location'
select_related = ['location', 'outline']
prefetch_related = ['arcelementnode_set', 'storyelementnode_set']
pk_url_kwarg = 'instance'
context_object_name = 'location_instance'
def dispatch(self, request, *args, **kwargs):
self.location = get_object_or_404(Location, pk=kwargs['location'])
return super().dispatch(request, *args, **kwargs)
def get_permission_object(self):
return self.location
[docs]class LocationInstanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, generic.CreateView):
'''
Generic view for creating a location instance on a outline.
'''
model = LocationInstance
template_name = 'fiction_outlines/location_instance_create.html'
permission_required = 'fiction_outlines.edit_location'
success_url = None
form_class = forms.LocationInstanceForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['location'] = self.location
return kwargs
def get_permission_object(self):
return self.location
def dispatch(self, request, *args, **kwargs):
self.location = get_object_or_404(Location, pk=kwargs['location'])
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['location'] = self.location
return context
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:location_instance_detail',
kwargs={'location': self.location.pk, 'instance': self.object.pk})
def form_valid(self, form):
outline = form.instance.outline
form.instance.location = self.location
if not (self.request.user.has_perm('fiction_outlines.edit_location', self.location) and
self.request.user.has_perm('fiction_outlines.edit_outline', outline)):
return HttpResponseForbidden()
return super().form_valid(form)
[docs]class LocationInstanceUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
SelectRelatedMixin, generic.edit.UpdateView):
'''
Generic view for updating a location instance. Not used since there are not details.
But it's here if you want to subclass LocationInstance and customize it.
'''
model = LocationInstance
# Blank template to override.
template_name = 'fiction_outlines/location_instance_update.html'
permission_required = 'fiction_outlines.edit_location_instance'
success_url = None
select_related = ['location', 'outline']
form_class = forms.LocationInstanceForm
context_object_name = 'location_instance'
pk_url_kwarg = 'instance'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:location_instance_detail',
kwargs={'instance': self.object.pk, 'location': self.location.pk})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['location'] = self.location
return kwargs
def dispatch(self, request, *args, **kwargs):
self.location = get_object_or_404(Location, pk=kwargs['location'])
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['location'] = self.location
return context
def form_valid(self, form):
outline = form.instance.outline
if not (self.request.user.has_perm('fiction_outlines.edit_location', self.location) and
self.request.user.has_perm('fiction_outlines.edit_outline', outline)):
return HttpResponseForbidden()
return super().form_valid(form)
[docs]class LocationInstanceDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
SelectRelatedMixin, PrefetchRelatedMixin, generic.DeleteView):
'''
Generic delete view for Location Instance.
'''
model = LocationInstance
permission_required = 'fiction_outlines.delete_location_instance'
template_name = 'fiction_outlines/location_instance_delete.html'
success_url = None
select_releated = ['location', 'outline']
prefetch_related = ['arcelementnode_set', 'storyelementnode_set']
context_object_name = 'location_instance'
pk_url_kwarg = 'instance'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:location_details', kwargs={'location': self.location_id})
def dispatch(self, request, *args, **kwargs):
self.location = get_object_or_404(Location, pk=kwargs['location'])
return super().dispatch(request, *args, **kwargs)
# TODO ArcElementNode, and StoryElementNode
[docs]class OutlineListView(LoginRequiredMixin, SelectRelatedMixin,
PrefetchRelatedMixin, generic.ListView):
'''
Generic view for Outline Outline list
'''
model = Outline
template_name = 'fiction_outlines/outline_list.html'
select_related = ['series']
prefetch_related = ['arc_set', 'storyelementnode_set', 'characterinstance_set', 'locationinstance_set']
context_object_name = 'outline_list'
def get_queryset(self):
return Outline.objects.filter(user=self.request.user)
[docs]class OutlineDetailView(LoginRequiredMixin, PermissionRequiredMixin,
SelectRelatedMixin, PrefetchRelatedMixin, generic.DetailView):
'''
Generic view for Outline detail
'''
model = Outline
template_name = 'fiction_outlines/outline_detail.html'
permission_required = 'fiction_outlines.view_outline'
select_related = ['series']
prefetch_related = ['arc_set', 'characterinstance_set', 'locationinstance_set', 'storyelementnode_set']
pk_url_kwarg = 'outline'
context_object_name = 'outline'
[docs]class OutlineCreateView(LoginRequiredMixin, generic.CreateView):
'''
Generic view for creating initial outline.
'''
model = Outline
template_name = 'fiction_outlines/outline_create.html'
form_class = forms.OutlineForm
success_url = None
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:outline_detail', kwargs={'outline': self.object.pk})
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
[docs]class OutlineUpdateView(LoginRequiredMixin, PermissionRequiredMixin, generic.edit.UpdateView):
'''
Generic update view for outline details.
'''
model = Outline
permission_required = 'fiction_outlines.edit_outline'
template_name = 'fiction_outlines/outline_update.html'
success_url = None
form_class = forms.OutlineForm
context_object_name = 'outline'
pk_url_kwarg = 'outline'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:outline_detail', kwargs={'outline': self.object.pk})
[docs]class OutlineDeleteView(LoginRequiredMixin, PermissionRequiredMixin, generic.DeleteView):
'''
Generic delete view for an outline.
'''
model = Outline
permission_required = 'fiction_outlines.delete_outline'
template_name = 'fiction_outlines/outline_delete.html'
success_url = reverse_lazy('fiction_outlines:outline_list')
context_object_name = 'outline'
pk_url_kwarg = 'outline'
[docs]class ArcListView(LoginRequiredMixin, PermissionRequiredMixin, generic.ListView):
'''
Generic list view for arcs in a outline
'''
model = Arc
permission_required = 'fiction_outlines.view_outline'
template_name = 'fiction_outlines/arc_list.html'
context_object_name = 'arc_list'
def dispatch(self, request, *args, **kwargs):
self.outline = get_object_or_404(Outline, pk=kwargs['outline'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return Arc.objects.filter(outline=self.outline).select_related(
'outline').prefetch_related('arcelementnode_set')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['outline'] = self.outline
return context
def get_permission_object(self):
return self.outline
[docs]class ArcDetailView(LoginRequiredMixin, PermissionRequiredMixin,
SelectRelatedMixin, PrefetchRelatedMixin, generic.DetailView):
'''
Generic view for arc details.
'''
model = Arc
permission_required = 'fiction_outlines.view_arc'
template_name = 'fiction_outlines/arc_detail.html'
select_related = ['outline']
prefetch_related = ['arcelementnode_set']
pk_url_kwarg = 'arc'
context_object_name = 'arc'
def dispatch(self, request, *args, **kwargs):
self.outline = get_object_or_404(Outline, pk=kwargs['outline'])
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['annotated_list'] = ArcElementNode.get_annotated_list(parent=self.object.arc_root_node)
return context
[docs]class ArcCreateView(LoginRequiredMixin, PermissionRequiredMixin, generic.CreateView):
'''
Generic view for creating an arc.
'''
model = Arc
permission_required = 'fiction_outlines.edit_outline'
template_name = 'fiction_outlines/arc_create.html'
fields = ['name', 'mace_type']
success_url = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:arc_detail', kwargs={'outline': self.outline.pk, 'arc': self.object.pk})
def dispatch(self, request, *args, **kwargs):
self.outline = get_object_or_404(Outline, pk=kwargs['outline'])
return super().dispatch(request, *args, **kwargs)
def get_permission_object(self):
return self.outline
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['outline'] = self.outline
return context
def form_valid(self, form):
try:
with transaction.atomic():
self.object = self.outline.create_arc(name=form.instance.name, mace_type=form.instance.mace_type)
return HttpResponseRedirect(self.get_success_url())
except IntegrityError as IE:
form.add_error('name', str(IE))
super().form_invalid(form)
[docs]class ArcUpdateView(LoginRequiredMixin, PermissionRequiredMixin, SelectRelatedMixin, generic.edit.UpdateView):
'''
Generic view for updating arc details
'''
model = Arc
permission_required = 'fiction_outlines.edit_arc'
template_name = 'fiction_outlines/arc_update.html'
success_url = None
fields = ['name', 'mace_type']
select_related = ['outline']
pk_url_kwarg = 'arc'
context_object_name = 'arc'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:arc_detail', kwargs={'arc': self.object.pk})
[docs]class ArcDeleteView(LoginRequiredMixin, PermissionRequiredMixin, generic.DeleteView):
'''
Generic view for deleting an arc
'''
model = Arc
permission_required = 'fiction_outlines.delete_arc'
template_name = 'fiction_outlines/arc_delete.html'
success_url = None
pk_url_kwarg = 'arc'
context_object_name = 'arc'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:outline_detail', kwargs={'outline': self.outline.pk})
def dispatch(self, request, *args, **kwargs):
arc = get_object_or_404(Arc, pk=kwargs['arc'])
self.outline = arc.outline
return super().dispatch(request, *args, **kwargs)
# Now we start dealing with Element nodes for arc and story elements.
# Generic views are fine for some small edits on individual nodes, but
# actual tree mainipulation will need to be somewhat custom.
[docs]class ArcNodeDetailView(LoginRequiredMixin, PermissionRequiredMixin, SelectRelatedMixin,
PrefetchRelatedMixin, generic.DetailView):
'''
View for looking at the details of an atomic node as opposed to the whole tree.
'''
model = ArcElementNode
permission_required = 'fiction_outlines.view_arc_node'
template_name = 'fiction_outlines/arcnode_detail.html'
pk_url_kwarg = 'arcnode'
context_object_name = 'arcnode'
select_related = ['arc', 'arc__outline', 'story_element_node']
prefetch_related = ['assoc_characters', 'assoc_locations']
[docs]class ArcNodeCreateView(LoginRequiredMixin, PermissionRequiredMixin, generic.CreateView):
'''
Create view for an arc node. Assumes that the target position has already been passed to it
via kwargs.
'''
model = ArcElementNode
permission_required = 'fiction_outlines.edit_arc'
template_name = 'fiction_outlines/arcnode_create.html'
form_class = forms.ArcNodeForm
success_url = None
def dispatch(self, request, *args, **kwargs):
self.arc = get_object_or_404(Arc, pk=kwargs['arc'])
self.target = get_object_or_404(ArcElementNode, pk=kwargs['arcnode'])
self.pos = kwargs['pos']
return super().dispatch(request, *args, **kwargs)
def get_permission_object(self):
return self.arc
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['arc'] = self.arc
return kwargs
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:arcnode_detail', kwargs={'outline': self.arc.outline.pk,
'arc': self.arc.pk, 'arcnode': self.object.pk})
def form_valid(self, form):
if self.pos == 'addchild':
try:
with transaction.atomic():
self.object = self.target.add_child(description=form.instance.description,
arc_element_type=form.instance.arc_element_type,
story_element_node=form.instance.story_element_node,
assoc_characters=form.instance.assoc_characters,
assoc_locations=form.instance.assoc_locations)
except NodeAlreadySaved: # pragma: no cover
# We don't care, as the record is already there.
pass
except ArcIntegrityError:
form.add_error('arc_element_type', _("This would duplicate an existing milestone for this arc."))
else:
try:
with transaction.atomic():
self.object = self.target.add_sibling(pos=self.pos, description=form.instance.description,
arc_element_type=form.instance.arc_element_type,
story_element_node=form.instance.story_element_node,
assoc_characters=form.instance.assoc_characters,
assoc_locations=form.instance.assoc_locations)
except NodeAlreadySaved: # pragma: no cover
# We don't care as the record is already saved.
pass
except InvalidPosition as IP:
form.add_error('description', _("You are trying to put this into an invalid position."))
logger.error(str(IP))
return super().form_invalid(form)
except IntegrityError as IE:
form.add_error('description', str(IE))
return super().form_invalid(form)
except ArcIntegrityError:
form.add_error('arc_element_type', _("This would duplicate an existing milestone for this arc."))
return super().form_invalid(form)
return HttpResponseRedirect(self.get_success_url())
[docs]class ArcNodeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, SelectRelatedMixin,
PrefetchRelatedMixin, generic.edit.UpdateView):
'''
View for editing details of an arc node (but not it's tree position).
'''
model = ArcElementNode
permission_required = 'fiction_outlines.edit_arc_node'
template_name = 'fiction_outlines/arcnode_update.html'
pk_url_kwarg = 'arcnode'
context_object_name = 'arcnode'
select_related = ['arc', 'arc__outline', 'story_element_node']
prefetch_related = ['assoc_characters', 'assoc_locations']
form_class = forms.ArcNodeForm
success_url = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return self.object.get_absolute_url()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['arc'] = self.object.arc
return kwargs
def form_valid(self, form):
try:
tree_manipulation.send(sender=ArcElementNode, instance=form.instance,
action='update', target_node=None,
target_node_type=form.instance.arc_element_type, pos=None)
except ArcIntegrityError as AE:
form.add_error('arc_element_type', str(AE))
return super().form_invalid(form)
return super().form_valid(form)
[docs]class ArcNodeMoveView(LoginRequiredMixin, PermissionRequiredMixin, generic.edit.UpdateView):
'''
View for executing a move method on an arcnode.
'''
model = ArcElementNode
permission_required = 'fiction_outlines.edit_arc_node'
pk_url_kwarg = 'arcnode'
context_object_name = 'arcnode'
form_class = movenodeform_factory(ArcElementNode, form=forms.OutlineMoveNodeForm, exclude=(
'headline',
'id',
'description',
'assoc_characters',
'assoc_locations',
'story_element_node',
'arc_element_type',
'arc'))
success_url = None
template_name = 'fiction_outlines/arcnode_move.html'
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
url = self.object.get_absolute_url()
logger.debug("Found success url of %s" % url)
return url
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['root_node'] = self.object.get_root()
logger.debug('For object %s (%s, %s), found root node of %s (%s, %s)' % (
self.object.pk,
self.object.arc.name,
self.object.arc_element_type,
kwargs['root_node'].pk,
kwargs['root_node'].arc.name,
kwargs['root_node'].arc_element_type
))
return kwargs
def form_valid(self, form):
logger.debug("Attepting move within an atomic transaction...")
try:
with transaction.atomic():
self.object = form.save()
except InvalidPosition as IP:
form.add_error('_position', _("This is not a permitted position"))
logger.error(_('This is not a permitted position. \n Details: %s' % str(IP)))
return self.form_invalid(form)
except InvalidMoveToDescendant as IMD:
form.add_error('_position', _("You cannot move an item to be a sibling or child of its own descendant."))
logger.debug(_("You cannot move item to be a sibling or child of own descendant. Details: %s" % str(IMD)))
return self.form_invalid(form)
except PathOverflow as PO:
form.add_error('_position', _('Apologies, there has been a database error. This has been logged.'))
logger.error(str(PO))
return self.form_invalid(form)
return HttpResponseRedirect(self.get_success_url())
[docs]class ArcNodeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, SelectRelatedMixin,
PrefetchRelatedMixin, generic.edit.DeleteView):
'''
View for deleting an arc node.
'''
model = ArcElementNode
permission_required = 'fiction_outlines.delete_arc_node'
template_name = 'fiction_outlines/arcnode_delete.html'
pk_url_kwarg = 'arcnode'
context_object_name = 'arcnode'
select_related = ['arc', 'arc__outline', 'story_element_node']
prefetch_related = ['assoc_characters', 'assoc_locations']
success_url = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:arc_detail',
kwargs={'outline': self.outline.pk, 'arc': self.arc.pk})
def dispatch(self, request, *args, **kwargs):
self.outline = get_object_or_404(Outline, pk=kwargs['outline'])
self.arc = get_object_or_404(Arc, pk=kwargs['arc'])
return super().dispatch(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.arc_element_type in ['mile_hook', 'mile_reso']:
return render(self.request, self.template_name, context=self.get_context_data(), content_type='text/html')
return super().delete(request, *args, **kwargs)
[docs]class StoryNodeDetailView(LoginRequiredMixin, PermissionRequiredMixin, SelectRelatedMixin,
PrefetchRelatedMixin, generic.DetailView):
'''
View for looking at the details of an atomic story node as opposed to the whole tree.
'''
model = StoryElementNode
permission_required = 'fiction_outlines.view_story_node'
template_name = 'fiction_outlines/storynode_detail.html'
pk_url_kwarg = 'storynode'
context_object_name = 'storynode'
select_related = ['outline']
prefetch_related = ['arcelementnode_set', 'assoc_characters', 'assoc_locations']
[docs]class StoryNodeCreateView(LoginRequiredMixin, PermissionRequiredMixin, generic.CreateView):
'''
Creation view for a story node. Assumes the target and pos have been passed as kwargs.
'''
model = StoryElementNode
permission_required = 'fiction_outlines.edit_outline'
template_name = 'fiction_outlines/storynode_create.html'
form_class = forms.StoryNodeForm
success_url = None
def dispatch(self, request, *args, **kwargs):
self.outline = get_object_or_404(Outline, pk=kwargs['outline'])
self.target = get_object_or_404(StoryElementNode, pk=kwargs['storynode'])
self.pos = kwargs['pos']
return super().dispatch(request, *args, **kwargs)
def get_permission_object(self):
return self.outline
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:storynode_detail', kwargs={'outline': self.outline.pk,
'storynode': self.object.pk})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['outline'] = self.outline
return kwargs
def form_valid(self, form):
if self.pos == 'addchild':
try:
with transaction.atomic():
self.object = self.target.add_child(story_element_type=form.instance.story_element_type,
name=form.instance.name,
description=form.instance.description)
if form.instance.assoc_characters:
self.object.assoc_characters.set(form.instance.assoc_characters.all())
if form.instance.assoc_locations:
self.object.assoc_locations.set(form.instance.assoc_locations.all())
except NodeAlreadySaved: # pragma: no cover
# We don't care since it means the record got saved for us already.
pass
except IntegrityError as IE:
form.add_error('story_element_type', str(IE))
return super().form_invalid(form)
else:
try:
with transaction.atomic():
self.object = self.target.add_sibling(pos=self.pos,
story_element_type=form.instance.story_element_type,
name=form.instance.name,
description=form.instance.description)
if form.instance.assoc_characters:
self.object.assoc_characters.set(form.instance.assoc_characters.all())
if form.instance.assoc_locations:
self.object.assoc_locations.set(form.instance.assoc_locations.all())
except NodeAlreadySaved: # pragma: no cover
# We don't care since we already got what we wanted.
pass
except InvalidPosition as IP:
logger.error(str(IP))
form.add_error('description', _("This item cannot be placed into this position."))
return super().form_invalid(form)
except IntegrityError as IE:
form.add_error('story_element_type', str(IE))
logger.error(str(IE))
return super().form_invalid(form)
return HttpResponseRedirect(self.get_success_url())
[docs]class StoryNodeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, SelectRelatedMixin,
PrefetchRelatedMixin, generic.edit.UpdateView):
'''
View for doing basic updates to a story node, but not regarding its position in the tree.
'''
model = StoryElementNode
permission_required = 'fiction_outlines.edit_story_node'
template_name = 'fiction_outlines/storynode_update.html'
pk_url_kwarg = 'storynode'
context_object_name = 'storynode'
select_related = ['outline']
prefetch_related = ['arcelementnode_set', 'assoc_characters', 'assoc_locations']
form_class = forms.StoryNodeForm
success_url = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:storynode_update', kwargs={'outline': self.object.outline.pk,
'storynode': self.object.pk})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['outline'] = self.object.outline
return kwargs
def form_valid(self, form):
try:
tree_manipulation.send(sender=StoryElementNode, instance=form.instance, action='update',
target_node_type=form.instance.story_element_type, target_node=None, pos=None)
except IntegrityError as IE:
form.add_error('story_element_type', str(IE))
return self.form_invalid(form)
return super().form_valid(form)
[docs]class StoryNodeMoveView(StoryNodeUpdateView):
'''
View for executing a move method on an arcnode.
'''
form_class = movenodeform_factory(StoryElementNode, form=forms.OutlineMoveNodeForm, exclude=(
'name',
'id',
'description',
'assoc_characters',
'assoc_locations',
'outline',
'story_element_type'
))
success_url = None
template_name = 'fiction_outlines/storynode_move.html'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
del kwargs['outline']
kwargs['root_node'] = self.object.get_root()
return kwargs
def form_valid(self, form):
try:
ref_node = get_object_or_404(StoryElementNode, pk=form.cleaned_data['_ref_node_id'])
tree_manipulation.send(sender=StoryElementNode, instance=form.instance, action='move',
target_node_type=ref_node.story_element_type,
target_node=ref_node, pos=form.cleaned_data['_position'])
except IntegrityError as IE:
form.add_error('_ref_node_id', str(IE))
return self.form_invalid(form)
try:
with transaction.atomic():
form.save()
except InvalidPosition as IP:
form.add_error('_position', _("This is not a permitted position"))
return self.form_invalid(form)
except InvalidMoveToDescendant as IMD:
form.add_error('_position', _("You cannot move an item to be a sibling or child of its own descendant."))
return self.form_invalid(form)
except PathOverflow as PO:
form.add_error('_position', _('Apologies, there has been a database error. This has been logged.'))
logger.error(str(PO))
return self.form_invalid(form)
return HttpResponseRedirect(self.get_success_url())
[docs]class StoryNodeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, SelectRelatedMixin,
PrefetchRelatedMixin, generic.edit.DeleteView):
'''
Genric view for deleting a story node.
'''
model = StoryElementNode
permission_required = 'fiction_outlines.delete_story_node'
template_name = 'fiction_outlines/storynode_delete.html'
pk_url_kwarg = 'storynode'
context_object_name = 'storynode'
select_related = ['outline']
prefetch_related = ['arcelementnode_set', 'assoc_characters', 'assoc_locations']
success_url = None
def get_success_url(self):
if self.success_url:
return self.success_url # pragma: no cover
return reverse_lazy('fiction_outlines:outline_detail', kwargs={'outline': self.outline.pk})
def dispatch(self, request, *args, **kwargs):
self.outline = get_object_or_404(Outline, pk=kwargs['outline'])
return super().dispatch(request, *args, **kwargs)