Makes forms more generic and implements a set of class-based CRUD views for character objects.

This commit is contained in:
Johnny 2018-10-18 19:49:05 +00:00
parent 33727b472c
commit 892fb3e462
5 changed files with 222 additions and 165 deletions

View file

@ -1,10 +1,20 @@
from django import forms
from django.conf import settings
from django.contrib.auth.forms import UserCreationForm, UsernameField
from django.forms import ModelForm
from django.utils.html import escape
from evennia.utils import class_from_module
from random import choice, randint
class AccountForm(UserCreationForm):
class EvenniaForm(forms.Form):
def clean(self):
cleaned = super(EvenniaForm, self).clean()
# Escape all values provided by user
cleaned = {k:escape(v) for k,v in cleaned.items()}
return cleaned
class AccountForm(EvenniaForm, UserCreationForm):
class Meta:
model = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
@ -13,130 +23,34 @@ class AccountForm(UserCreationForm):
email = forms.EmailField(help_text="A valid email address. Optional; used for password resets.", required=False)
class CharacterForm(forms.Form):
name = forms.CharField(help_text="The name of your intended character.")
age = forms.IntegerField(min_value=3, max_value=99, help_text="How old your character should be once spawned.")
description = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}), max_length=2048, min_length=160, required=False)
class ObjectForm(EvenniaForm, ModelForm):
@classmethod
def assign_attributes(cls, attribute_list, points, min_points, max_points):
"""
Randomly distributes a number of points across the given attributes,
while also ensuring each attribute gets at least a certain amount
and at most a certain amount.
class Meta:
model = class_from_module(settings.BASE_OBJECT_TYPECLASS)
fields = ("db_key",)
labels = {
'db_key': 'Name',
}
class CharacterForm(ObjectForm):
class Meta:
# Get the correct object model
model = class_from_module(settings.BASE_CHARACTER_TYPECLASS)
# Allow entry of the 'key' field
fields = ("db_key",)
# Rename 'key' to something more intelligible
labels = {
'db_key': 'Name',
}
Args:
attribute_list (iterable): List or tuple of attribute names to assign
points to.
points (int): Starting number of points
min_points (int): Least amount of points each attribute should have
max_points (int): Most amount of points each attribute should have
Returns:
spread (dict): Dict of attributes and a point assignment.
"""
num_buckets = len(attribute_list)
point_spread = (x for x in self.random_distribution(points, num_buckets, min_points, max_points))
# For each field, get the point calculation for the next attribute value generated
return {attribute: next(point_spread) for k in attribute_list}
# Fields pertaining to user-configurable attributes on the Character object.
desc = forms.CharField(label='Description', widget=forms.Textarea(attrs={'rows': 3}), max_length=2048, required=False)
@classmethod
def random_distribution(cls, points, num_buckets, min_points, max_points):
"""
Distributes a set number of points randomly across a number of 'buckets'
while also attempting to ensure each bucket's value finishes within a
certain range.
If your math doesn't add up (you try to distribute 5 points across 100
buckets and insist each bucket has at least 20 points), the algorithm
will return the best spread it could achieve but will not raise an error
(so in this case, 5 random buckets would get 1 point each and that's all).
Args:
points (int): The number of points to distribute.
num_buckets (int): The number of 'buckets' (or stats, skills, etc)
you wish to distribute points to.
min_points (int): The least amount of points each bucket should have.
max_points (int): The most points each bucket should have.
Returns:
buckets (list): List of random point assignments.
"""
buckets = [0 for x in range(num_buckets)]
indices = [i for (i, value) in enumerate(buckets)]
class CharacterUpdateForm(CharacterForm):
"""
Provides a form that only allows updating of db attributes, not model
attributes.
# Do this while we have eligible buckets, points to assign and we haven't
# maxed out all the buckets.
while indices and points and sum(buckets) <= (max_points * num_buckets):
# Pick a random bucket index
index = choice(indices)
# Add to bucket
buckets[index] = buckets[index] + 1
points = points - 1
# Get the indices of eligible buckets
indices = [i for (i, value) in enumerate(buckets) if (value < min_points) or (value < max_points)]
return buckets
class ExtendedCharacterForm(CharacterForm):
GENDERS = (
('male', 'Male'),
('female', 'Female'),
('androgynous', 'Androgynous'),
('special', 'Special')
)
RACES = (
('human', 'Human'),
('elf', 'Elf'),
('orc', 'Orc'),
)
CLASSES = (
('civilian', 'Civilian'),
('warrior', 'Warrior'),
('thief', 'Thief'),
('cleric', 'Cleric')
)
PERKS = (
('strong', 'Extra strength'),
('nimble', 'Quick on their toes'),
('diplomatic', 'Fast talker')
)
name = forms.CharField(help_text="The name of your intended character.")
age = forms.IntegerField(min_value=3, max_value=99, help_text="How old your character should be once spawned.")
gender = forms.ChoiceField(choices=GENDERS, help_text="Which end of the multidimensional spectrum does your character most closely align with, in terms of gender?")
race = forms.ChoiceField(choices=RACES, help_text="What race does your character belong to?")
job = forms.ChoiceField(choices=CLASSES, help_text="What profession or role does your character fulfill or is otherwise destined to?")
perks = forms.MultipleChoiceField(choices=PERKS, help_text="What extraordinary abilities does your character possess?")
description = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}), max_length=2048, min_length=160, required=False)
strength = forms.IntegerField(min_value=1, max_value=10)
perception = forms.IntegerField(min_value=1, max_value=10)
intelligence = forms.IntegerField(min_value=1, max_value=10)
dexterity = forms.IntegerField(min_value=1, max_value=10)
charisma = forms.IntegerField(min_value=1, max_value=10)
vitality = forms.IntegerField(min_value=1, max_value=10)
magic = forms.IntegerField(min_value=1, max_value=10)
def __init__(self, *args, **kwargs):
# Do all the normal initizliation stuff that would otherwise be happening
super(ExtendedCharacterCreationForm, self).__init__(*args, **kwargs)
# Given a pool of points, let's randomly distribute them across attributes.
# First get a list of attributes
attributes = ('strength', 'perception', 'intelligence', 'dexterity', 'charisma', 'vitality', 'magic')
# Distribute a random number of points across them
attrs = self.assign_attributes(attributes, 50, 1, 10)
# Initialize the form with the results of the point distribution
for field in attrs.keys():
self.initial[field] = attrs[field]
"""
pass

View file

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block titleblock %}
Character Creation
{{ view.page_title }}
{% endblock %}
{% block content %}
@ -12,7 +12,7 @@ Character Creation
<div class="col-lg-12 col-sm-12">
<div class="card mt-3">
<div class="card-body">
<h1 class="card-title">Character Creation</h1>
<h1 class="card-title">{{ view.page_title }}</h1>
<hr />
{% if form.errors %}

View file

@ -20,8 +20,8 @@ Manage Characters
<a href="{{ object.get_absolute_url }}"><img class="d-flex mr-3" src="http://placehold.jp/50x50.png" alt="" /></a>
<div class="media-body">
<p class="float-right ml-2">{{ object.db_date_created }}
<br /><a href="{{ object.get_delete_url }}">Delete</a>
<br /><a href="{{ object.get_update_url }}">Edit</a></p>
<br /><a href="{% url 'character-delete' pk=object.id slug=object.name %}">Delete</a>
<br /><a href="{% url 'character-update' pk=object.id slug=object.name %}">Edit</a></p>
<h5 class="mt-0"><a href="{{ object.get_absolute_url }}">{{ object }}</a> {% if object.subtitle %}<small class="text-muted" style="white-space:nowrap;">{{ object.subtitle }}</small>{% endif %}</h5>
<p>{{ object.db.desc }}</p>
</div>

View file

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block titleblock %}
Confirm Delete
{% endblock %}
{% block body %}
{% load addclass %}
<div class="container main-content mt-4" id="main-copy">
<div class="row">
<div class="col-lg-5 offset-lg-3 col-sm-12">
<div class="card mt-3 border border-danger">
<div class="card-body">
<h1 class="card-title">Confirm Delete</h1>
<hr />
<form method="post" action="?">
{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<hr />
<div class="form-group">
<input class="form-control btn btn-outline-danger" type="submit" value="Submit" />
<input type="hidden" name="next" value="{{ next }}" />
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -7,20 +7,23 @@ templates on the fly.
"""
from django.contrib.admin.sites import site
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.admin.views.decorators import staff_member_required
from django.core.exceptions import PermissionDenied
from django.db.models.functions import Lower
from django.http import HttpResponseRedirect, Http404
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import View, TemplateView, ListView, DetailView, FormView
from django.views.generic.edit import DeleteView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from evennia import SESSION_HANDLER
from evennia.objects.models import ObjectDB
from evennia.accounts.models import AccountDB
from evennia.utils import logger
from evennia.web.website.forms import AccountForm, CharacterForm
from evennia.utils import class_from_module, logger
from evennia.web.website.forms import *
from django.contrib.auth import login
from django.utils.text import slugify
@ -142,25 +145,132 @@ def admin_wrapper(request):
"""
return staff_member_required(site.index)(request)
#
# Class-based views
#
class EvenniaCreateView(CreateView):
@property
def page_title(self):
return 'Create %s' % self.model._meta.verbose_name.title()
class EvenniaUpdateView(UpdateView):
@property
def page_title(self):
return 'Update %s' % self.model._meta.verbose_name.title()
class EvenniaDeleteView(DeleteView):
@property
def page_title(self):
return 'Delete %s' % self.model._meta.verbose_name.title()
#
# Object views
#
class ObjectDetailView(DetailView):
model = ObjectDB
def get_object(self, queryset=None):
"""
Override of Django hook.
Evennia does not natively store slugs, so where a slug is provided,
calculate the same for the object and make sure it matches.
"""
obj = super(ObjectDetailView, self).get_object(queryset)
if not slugify(obj.name) == self.kwargs.get('slug'):
if slugify(obj.name) != self.kwargs.get(self.slug_url_kwarg):
raise Http404(u"No %(verbose_name)s found matching the query" %
{'verbose_name': queryset.model._meta.verbose_name})
return obj
class ObjectCreateView(LoginRequiredMixin, EvenniaCreateView):
model = ObjectDB
class ObjectDeleteView(LoginRequiredMixin, ObjectDetailView, EvenniaDeleteView):
model = ObjectDB
template_name = 'website/object_confirm_delete.html'
def delete(self, request, *args, **kwargs):
"""
Calls the delete() method on the fetched object and then
redirects to the success URL.
We extend this so we can capture the name for the sake of confirmation.
"""
obj = str(self.get_object())
response = super(ObjectDeleteView, self).delete(request, *args, **kwargs)
messages.success(request, "Successfully deleted '%s'." % obj)
return response
class ObjectUpdateView(LoginRequiredMixin, ObjectDetailView, EvenniaUpdateView):
model = ObjectDB
def get_initial(self):
"""
Override of Django hook.
Prepopulates form field values based on object db attributes as well as
model field values.
"""
# Get the object we want to update
obj = self.get_object()
# Get attributes
data = {k:getattr(obj.db, k, '') for k in self.form_class.base_fields}
# Get model fields
data.update({k:getattr(obj, k, '') for k in self.form_class.Meta.fields})
return data
def form_valid(self, form):
"""
Override of Django hook.
Updates object attributes based on values submitted.
This method is only called if all values for the fields submitted
passed form validation, so at this point we can assume the data is
validated and sanitized.
"""
# Get the values submitted after they've been cleaned and validated
data = {k:v for k,v in form.cleaned_data.items() if k not in self.form_class.Meta.fields}
# Update the object attributes
for key, value in data.items():
setattr(self.object.db, key, value)
messages.success(self.request, "Successfully updated '%s' for %s." % (key, self.object))
# Do not return super().form_valid; we don't want to update the model
# instance, just its attributes.
return HttpResponseRedirect(self.success_url)
#
# Account views
#
class AccountCreationView(FormView):
class AccountMixin(object):
model = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
form_class = AccountForm
class AccountCreateView(AccountMixin, ObjectCreateView):
template_name = 'website/registration/register.html'
success_url = reverse_lazy('login')
def form_valid(self, form):
# Check to make sure basics validated
valid = super(AccountCreationView, self).form_valid(form)
if not valid: return self.form_invalid(form)
username = form.cleaned_data['username']
password = form.cleaned_data['password1']
@ -189,39 +299,39 @@ class AccountCreationView(FormView):
messages.success(self.request, "Your account '%s' was successfully created! You may log in using it now." % account.name)
return HttpResponseRedirect(self.success_url)
class CharacterManageView(LoginRequiredMixin, ListView):
model = ObjectDB
paginate_by = 10
template_name = 'website/character_manage_list.html'
#
# Character views
#
class CharacterMixin(object):
model = class_from_module(settings.BASE_CHARACTER_TYPECLASS)
form_class = CharacterForm
def get_queryset(self):
# Get IDs of characters owned by account
ids = [getattr(x, 'id') for x in self.request.user.db._playable_characters]
ids = [getattr(x, 'id') for x in self.request.user.db._playable_characters if x]
# Return a queryset consisting of those characters
return self.model.objects.filter(id__in=ids).order_by(Lower('db_key'))
class CharacterUpdateView(LoginRequiredMixin, FormView):
form_class = CharacterForm
template_name = 'website/generic_form.html'
success_url = reverse_lazy('manage-characters')
fields = ('description',)
class CharacterDeleteView(LoginRequiredMixin, ObjectDetailView, DeleteView):
model = ObjectDB
def get_queryset(self):
# Restrict characters available for deletion to those owned by
# the authenticated account
ids = [getattr(x, 'id') for x in self.request.user.db._playable_characters]
class CharacterManageView(LoginRequiredMixin, CharacterMixin, ListView):
paginate_by = 10
template_name = 'website/character_manage_list.html'
page_title = 'Manage: Characters'
# Return a queryset consisting of those characters
return self.model.objects.filter(id__in=ids).order_by(Lower('db_key'))
class CharacterUpdateView(CharacterMixin, ObjectUpdateView):
form_class = CharacterUpdateForm
template_name = 'website/character_form.html'
class CharacterDeleteView(CharacterMixin, ObjectDeleteView):
pass
class CharacterCreateView(LoginRequiredMixin, FormView):
form_class = CharacterForm
template_name = 'website/character_create_form.html'
success_url = reverse_lazy('manage-characters')
class CharacterCreateView(CharacterMixin, ObjectCreateView):
template_name = 'website/character_form.html'
def form_valid(self, form):
# Get account ref
@ -230,12 +340,12 @@ class CharacterCreateView(LoginRequiredMixin, FormView):
# Get attributes from the form
self.attributes = {k: form.cleaned_data[k] for k in form.cleaned_data.keys()}
charname = self.attributes.pop('name')
description = self.attributes.pop('description')
charname = self.attributes.pop('db_key')
description = self.attributes.pop('desc')
# Create a character
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
typeclass = settings.BASE_CHARACTER_TYPECLASS
typeclass = self.model
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
from evennia.utils import create
@ -258,7 +368,7 @@ class CharacterCreateView(LoginRequiredMixin, FormView):
account.db._last_puppet = character
# Assign attributes from form
[setattr(character.db, field, self.attributes[field]) for field in self.attributes.keys()]
[setattr(character.db, key, value) for key,value in self.attributes.items()]
character.db.creator_id = account.id
character.save()