Source code for evennia.web.website.views.characters

"""
Views for manipulating Characters (children of Objects often used for
puppeting).

"""

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models.functions import Lower
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.encoding import iri_to_uri
from django.utils.http import url_has_allowed_host_and_scheme
from django.views.generic import ListView
from django.views.generic.base import RedirectView

from evennia.utils import class_from_module
from evennia.web.website import forms

from .mixins import TypeclassMixin
from .objects import (
    ObjectCreateView,
    ObjectDeleteView,
    ObjectDetailView,
    ObjectUpdateView,
)


[docs]class CharacterMixin(TypeclassMixin): """ This is a "mixin", a modifier of sorts. Any view class with this in its inheritance list will be modified to work with Character objects instead of generic Objects or otherwise. """ # -- Django constructs -- model = class_from_module( settings.BASE_CHARACTER_TYPECLASS, fallback=settings.FALLBACK_CHARACTER_TYPECLASS ) form_class = forms.CharacterForm success_url = reverse_lazy("character-manage")
[docs] def get_queryset(self): """ This method will override the Django get_queryset method to only return a list of characters associated with the current authenticated user. Returns: queryset (QuerySet): Django queryset for use in the given view. """ # Get IDs of characters owned by account account = self.request.user ids = [getattr(x, "id") for x in account.characters if x] # Return a queryset consisting of those characters return self.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
[docs]class CharacterListView(LoginRequiredMixin, CharacterMixin, ListView): """ This view provides a mechanism by which a logged-in player can view a list of all other characters. This view requires authentication by default as a nominal effort to prevent human stalkers and automated bots/scrapers from harvesting data on your users. """ # -- Django constructs -- template_name = "website/character_list.html" paginate_by = 100 # -- Evennia constructs -- page_title = "Character List" access_type = "view"
[docs] def get_queryset(self): """ This method will override the Django get_queryset method to return a list of all characters (filtered/sorted) instead of just those limited to the account. Returns: queryset (QuerySet): Django queryset for use in the given view. """ account = self.request.user # Return a queryset consisting of characters the user is allowed to # see. ids = [ obj.id for obj in self.typeclass.objects.all() if obj.access(account, self.access_type) ] return self.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
[docs]class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, ObjectDetailView): """ This view provides a mechanism by which a logged-in player can "puppet" one of their characters within the context of the website. It also ensures that any user attempting to puppet something is logged in, and that their intended puppet is one that they own. """
[docs] def get_redirect_url(self, *args, **kwargs): """ Django hook. This view returns the URL to which the user should be redirected after a passed or failed puppet attempt. Returns: url (str): Path to post-puppet destination. """ # Get the requested character, if it belongs to the authenticated user char = self.get_object() # Get the page the user came from next_page = self.request.GET.get("next", self.success_url) # since next_page is untrusted input from the user, we need to check it's safe to next_page = iri_to_uri(next_page) if not url_has_allowed_host_and_scheme( url=next_page, allowed_hosts={self.request.get_host()}, require_https=self.request.is_secure(), ): next_page = self.success_url if char: # If the account owns the char, store the ID of the char in the # Django request's session (different from Evennia session!). # We do this because characters don't serialize well. self.request.session["puppet"] = int(char.pk) messages.success(self.request, "You become '%s'!" % char) else: # If the puppeting failed, clear out the cached puppet value self.request.session["puppet"] = None messages.error(self.request, "You cannot become '%s'." % char) return next_page
[docs]class CharacterManageView(LoginRequiredMixin, CharacterMixin, ListView): """ This view provides a mechanism by which a logged-in player can browse, edit, or delete their own characters. """ # -- Django constructs -- paginate_by = 10 template_name = "website/character_manage_list.html" # -- Evennia constructs -- page_title = "Manage Characters"
[docs]class CharacterUpdateView(CharacterMixin, ObjectUpdateView): """ This view provides a mechanism by which a logged-in player (enforced by ObjectUpdateView) can edit the attributes of a character they own. """ # -- Django constructs -- form_class = forms.CharacterUpdateForm template_name = "website/character_form.html"
[docs]class CharacterDetailView(CharacterMixin, ObjectDetailView): """ This view provides a mechanism by which a user can view the attributes of a character, owned by them or not. """ # -- Django constructs -- template_name = "website/object_detail.html" # -- Evennia constructs -- # What attributes to display for this object attributes = ["name", "desc"] access_type = "view"
[docs] def get_queryset(self): """ This method will override the Django get_queryset method to return a list of all characters the user may access. Returns: queryset (QuerySet): Django queryset for use in the given view. """ account = self.request.user # Return a queryset consisting of characters the user is allowed to # see. ids = [ obj.id for obj in self.typeclass.objects.all() if obj.access(account, self.access_type) ] return self.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
[docs]class CharacterDeleteView(CharacterMixin, ObjectDeleteView): """ This view provides a mechanism by which a logged-in player (enforced by ObjectDeleteView) can delete a character they own. """ # using the character form fails there form_class = forms.EvenniaForm
[docs]class CharacterCreateView(CharacterMixin, ObjectCreateView): """ This view provides a mechanism by which a logged-in player (enforced by ObjectCreateView) can create a new character. """ # -- Django constructs -- template_name = "website/character_form.html"
[docs] def form_valid(self, form): """ Django hook, modified for Evennia. This hook is called after a valid form is submitted. When an character creation form is submitted and the data is deemed valid, proceeds with creating the Character object. """ # Get account object creating the character account = self.request.user character = None # Get attributes from the form self.attributes = {k: form.cleaned_data[k] for k in form.cleaned_data.keys()} charname = self.attributes.pop("db_key") description = self.attributes.pop("desc") # Create a character character, errors = self.typeclass.create(charname, account, description=description) if errors: # Echo error messages to the user [messages.error(self.request, x) for x in errors] if character: # Assign attributes from form for key, value in self.attributes.items(): setattr(character.db, key, value) # Return the user to the character management page, unless overridden messages.success(self.request, "Your character '%s' was created!" % character.name) return HttpResponseRedirect(self.success_url) else: # Call the Django "form failed" hook messages.error(self.request, "Your character could not be created.") return self.form_invalid(form)