mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Merge pull request #1671 from strikaco/modernize
Frontend CRUD enhancements (character/account creation and management)
This commit is contained in:
commit
0bbe96b119
29 changed files with 1310 additions and 70 deletions
|
|
@ -196,6 +196,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
@lazy_property
|
||||
def sessions(self):
|
||||
return AccountSessionHandler(self)
|
||||
|
||||
# Do not make this a lazy property; the web UI will not refresh it!
|
||||
@property
|
||||
def characters(self):
|
||||
# Get playable characters list
|
||||
objs = self.db._playable_characters
|
||||
|
||||
# Rebuild the list if legacy code left null values after deletion
|
||||
if None in objs:
|
||||
objs = [x for x in self.db._playable_characters if x]
|
||||
self.db._playable_characters = objs
|
||||
|
||||
return objs
|
||||
|
||||
# session-related methods
|
||||
|
||||
|
|
@ -720,7 +733,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
|
||||
if character:
|
||||
# Update playable character list
|
||||
account.db._playable_characters.append(character)
|
||||
if character not in account.characters:
|
||||
account.db._playable_characters.append(character)
|
||||
|
||||
# We need to set this to have @ic auto-connect to this character
|
||||
account.db._last_puppet = character
|
||||
|
|
|
|||
|
|
@ -1944,12 +1944,20 @@ class DefaultCharacter(DefaultObject):
|
|||
locks = kwargs.pop('locks', '')
|
||||
|
||||
try:
|
||||
# Check to make sure account does not have too many chars
|
||||
if len(account.characters) >= settings.MAX_NR_CHARACTERS:
|
||||
errors.append("There are too many characters associated with this account.")
|
||||
return obj, errors
|
||||
|
||||
# Create the Character
|
||||
obj = create.create_object(**kwargs)
|
||||
|
||||
# Record creator id and creation IP
|
||||
if ip: obj.db.creator_ip = ip
|
||||
if account: obj.db.creator_id = account.id
|
||||
if account:
|
||||
obj.db.creator_id = account.id
|
||||
if obj not in account.characters:
|
||||
account.db._playable_characters.append(obj)
|
||||
|
||||
# Add locks
|
||||
if not locks and account:
|
||||
|
|
@ -1963,7 +1971,7 @@ class DefaultCharacter(DefaultObject):
|
|||
# If no description is set, set a default description
|
||||
if description or not obj.db.desc:
|
||||
obj.db.desc = description if description else "This is a character."
|
||||
|
||||
|
||||
except Exception as e:
|
||||
errors.append("An error occurred while creating this '%s' object." % key)
|
||||
logger.log_err(e)
|
||||
|
|
|
|||
|
|
@ -755,6 +755,7 @@ TEMPLATES = [{
|
|||
'django.contrib.auth.context_processors.auth',
|
||||
'django.template.context_processors.media',
|
||||
'django.template.context_processors.debug',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'sekizai.context_processors.sekizai',
|
||||
'evennia.web.utils.general_context.general_context'],
|
||||
# While true, show "pretty" error messages for template syntax errors.
|
||||
|
|
@ -789,6 +790,7 @@ INSTALLED_APPS = (
|
|||
'django.contrib.flatpages',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.messages',
|
||||
'sekizai',
|
||||
'evennia.utils.idmapper',
|
||||
'evennia.server',
|
||||
|
|
|
|||
|
|
@ -817,6 +817,38 @@ class TypedObject(SharedMemoryModel):
|
|||
kwargs={'pk': self.pk, 'slug': slugify(self.name)})
|
||||
except:
|
||||
return '#'
|
||||
|
||||
def web_get_puppet_url(self):
|
||||
"""
|
||||
Returns the URI path for a View that allows users to puppet a specific
|
||||
object.
|
||||
|
||||
ex. Oscar (Character) = '/characters/oscar/1/puppet/'
|
||||
|
||||
For this to work, the developer must have defined a named view somewhere
|
||||
in urls.py that follows the format 'modelname-action', so in this case
|
||||
a named view of 'character-puppet' would be referenced by this method.
|
||||
|
||||
ex.
|
||||
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/puppet/$',
|
||||
CharPuppetView.as_view(), name='character-puppet')
|
||||
|
||||
If no View has been created and defined in urls.py, returns an
|
||||
HTML anchor.
|
||||
|
||||
This method is naive and simply returns a path. Securing access to
|
||||
the actual view and limiting who can view this object is the developer's
|
||||
responsibility.
|
||||
|
||||
Returns:
|
||||
path (str): URI path to object puppet page, if defined.
|
||||
|
||||
"""
|
||||
try:
|
||||
return reverse('%s-puppet' % self._meta.verbose_name.lower(),
|
||||
kwargs={'pk': self.pk, 'slug': slugify(self.name)})
|
||||
except:
|
||||
return '#'
|
||||
|
||||
def web_get_update_url(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -67,7 +67,17 @@ def general_context(request):
|
|||
Returns common Evennia-related context stuff, which
|
||||
is automatically added to context of all views.
|
||||
"""
|
||||
account = None
|
||||
if request.user.is_authenticated(): account = request.user
|
||||
|
||||
puppet = None
|
||||
if account and request.session.get('puppet'):
|
||||
pk = int(request.session.get('puppet'))
|
||||
puppet = next((x for x in account.characters if x.pk == pk), None)
|
||||
|
||||
return {
|
||||
'account': account,
|
||||
'puppet': puppet,
|
||||
'game_name': GAME_NAME,
|
||||
'game_slogan': GAME_SLOGAN,
|
||||
'evennia_userapps': ACCOUNT_RELATED,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
from mock import Mock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import RequestFactory, TestCase
|
||||
from mock import MagicMock, patch
|
||||
from . import general_context
|
||||
|
||||
|
||||
class TestGeneralContext(TestCase):
|
||||
maxDiff = None
|
||||
|
||||
|
|
@ -15,8 +13,18 @@ class TestGeneralContext(TestCase):
|
|||
@patch('evennia.web.utils.general_context.WEBSOCKET_PORT', "websocket_client_port_testvalue")
|
||||
@patch('evennia.web.utils.general_context.WEBSOCKET_URL', "websocket_client_url_testvalue")
|
||||
def test_general_context(self):
|
||||
request = Mock()
|
||||
self.assertEqual(general_context.general_context(request), {
|
||||
request = RequestFactory().get('/')
|
||||
request.user = AnonymousUser()
|
||||
request.session = {
|
||||
'account': None,
|
||||
'puppet': None,
|
||||
}
|
||||
|
||||
response = general_context.general_context(request)
|
||||
|
||||
self.assertEqual(response, {
|
||||
'account': None,
|
||||
'puppet': None,
|
||||
'game_name': "test_name",
|
||||
'game_slogan': "test_game_slogan",
|
||||
'evennia_userapps': ['Accounts'],
|
||||
|
|
|
|||
56
evennia/web/website/forms.py
Normal file
56
evennia/web/website/forms.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
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
|
||||
|
||||
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)
|
||||
fields = ("username", "email")
|
||||
field_classes = {'username': UsernameField}
|
||||
|
||||
email = forms.EmailField(help_text="A valid email address. Optional; used for password resets.", required=False)
|
||||
|
||||
class ObjectForm(EvenniaForm, ModelForm):
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
# 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)
|
||||
|
||||
class CharacterUpdateForm(CharacterForm):
|
||||
"""
|
||||
Provides a form that only allows updating of db attributes, not model
|
||||
attributes.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -39,9 +39,28 @@ folder and edit it to add/remove links to the menu.
|
|||
{% block navbar_right %}
|
||||
{% endblock %}
|
||||
{% block navbar_user %}
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link">Logged in as {{user.username}}</a>
|
||||
{% if account %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" id="user_options" aria-expanded="false">
|
||||
{% if puppet %}
|
||||
Welcome, {{ puppet }}! <span class="text-muted">({{ account.username }})</span> <span class="caret"></span>
|
||||
{% else %}
|
||||
Logged in as {{ account.username }} <span class="caret"></span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="user_options">
|
||||
<a class="dropdown-item" href="{% url 'character-create' %}">Create Character</a>
|
||||
<a class="dropdown-item" href="{% url 'character-manage' %}">Manage Characters</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
{% for character in account.characters|slice:"10" %}
|
||||
<a class="dropdown-item" href="{{ character.web_get_puppet_url }}?next={{ request.path }}">{{ character }}</a>
|
||||
{% empty %}
|
||||
<a class="dropdown-item" href="#">No characters found!</a>
|
||||
{% endfor %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url 'password_change' %}">Change Password</a>
|
||||
<a class="dropdown-item" href="{% url 'logout' %}">Log Out</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link" href="{% url 'logout' %}">Log Out</a>
|
||||
|
|
@ -51,7 +70,7 @@ folder and edit it to add/remove links to the menu.
|
|||
<a class="nav-link" href="{% url 'login' %}">Log In</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link" href="{% url 'to_be_implemented' %}">Register</a>
|
||||
<a class="nav-link" href="{% url 'register' %}">Register</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<link rel="icon" type="image/x-icon" href="/static/website/images/evennia_logo.png" />
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
|
||||
<!-- Base CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{% static "website/css/app.css" %}">
|
||||
|
|
@ -29,6 +29,8 @@
|
|||
<title>{{game_name}} - {% if flatpage %}{{flatpage.title}}{% else %}{% block titleblock %}{{page_title}}{% endblock %}{% endif %}</title>
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
|
||||
<div id="top"><a href="#main-content" class="sr-only sr-only-focusable">Skip to main content.</a></div>
|
||||
{% include "website/_menu.html" %}
|
||||
<div class="container main-content mt-4" id="main-copy">
|
||||
|
|
@ -40,8 +42,12 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<div class="{% if sidebar %}col-8{% else %}col{% endif %}">
|
||||
{% include 'website/messages.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
{% include 'website/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -53,10 +59,12 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
</footer>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<!-- jQuery first, then Tether, then Bootstrap JS. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
51
evennia/web/website/templates/website/character_form.html
Normal file
51
evennia/web/website/templates/website/character_form.html
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% load addclass %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">{{ view.page_title }}</h1>
|
||||
<hr />
|
||||
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="?">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="form-field my-3">
|
||||
{{ field.label_tag }}
|
||||
{{ field | addclass:"form-control" }}
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text|safe }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<input class="form-control btn btn-outline-secondary" type="submit" value="Submit" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% load addclass %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">{{ view.page_title }}</h1>
|
||||
<hr />
|
||||
|
||||
{% for object in object_list %}
|
||||
<div class="media mb-4">
|
||||
<a href="{{ object.web_get_detail_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.web_get_delete_url }}">Delete</a>
|
||||
<br /><a href="{{ object.web_get_update_url }}">Edit</a></p>
|
||||
<h5 class="mt-0"><a href="{{ object.web_get_detail_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>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<div class="card-block p-4">
|
||||
<h1 class="card-title">Admin</h1>
|
||||
<p class="card-text">
|
||||
Welcome to the Evennia Admin Page. Here, you can edit many facets of accounts, characters, and other parts of the game.
|
||||
|
|
|
|||
51
evennia/web/website/templates/website/generic_form.html
Normal file
51
evennia/web/website/templates/website/generic_form.html
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Form
|
||||
{% 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">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Form</h1>
|
||||
<hr />
|
||||
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="?">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="form-field my-3">
|
||||
{{ field.label_tag }}
|
||||
{{ field | addclass:"form-control" }}
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text|safe }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<input class="form-control btn btn-outline-secondary" type="submit" value="Submit" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
9
evennia/web/website/templates/website/messages.html
Normal file
9
evennia/web/website/templates/website/messages.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{% if messages %}
|
||||
<!-- messages -->
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<!-- end messages -->
|
||||
{% endif %}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
{{ view.page_title }}
|
||||
{% 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">{{ view.page_title }}</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="yes" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
29
evennia/web/website/templates/website/object_list.html
Normal file
29
evennia/web/website/templates/website/object_list.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
List
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% load addclass %}
|
||||
<div class="container main-content mt-4" id="main-copy">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">List</h1>
|
||||
<hr />
|
||||
|
||||
<ul>
|
||||
{% for object in object_list %}
|
||||
<li>{{ object }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
evennia/web/website/templates/website/pagination.html
Normal file
32
evennia/web/website/templates/website/pagination.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{% if page_obj %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<!-- Pagination -->
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
<li class="page-item {% if page_obj.has_previous %}{% else %}disabled{% endif %}">
|
||||
<a class="page-link" href="{% if page_obj.has_previous %}?{% if q %}q={{ q }}&{% endif %}page={{ page_obj.previous_page_number }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% for l in page_obj.paginator.page_range %}
|
||||
{% if l <= page_obj.number|add:5 and l >= page_obj.number|add:-5 %}
|
||||
<li class="page-item {% if forloop.counter == page_obj.number %}active{% endif %}"><a class="page-link" href="?{% if q %}q={{ q }}&{% endif %}page={{ forloop.counter }}">{{ forloop.counter }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<li class="page-item {% if page_obj.has_next %}{% else %}disabled{% endif %}">
|
||||
<a class="page-link" href="{% if page_obj.has_next %}?{% if q %}q={{ q }}&{% endif %}page={{ page_obj.next_page_number }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -4,44 +4,56 @@
|
|||
Login
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block body %}
|
||||
|
||||
{% load addclass %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Login</h1>
|
||||
<hr />
|
||||
{% if user.is_authenticated %}
|
||||
<p>You are already logged in!</p>
|
||||
{% else %}
|
||||
{% if form.has_errors %}
|
||||
<p>Your username and password didn't match. Please try again.</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action=".">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_username">Username:</label>
|
||||
{{ form.username | addclass:"form-control" }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_password">Password:</label>
|
||||
{{ form.password | addclass:"form-control" }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input class="form-control" type="submit" value="Login" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
<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">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Login</h1>
|
||||
<hr />
|
||||
{% include 'website/messages.html' %}
|
||||
{% if user.is_authenticated %}
|
||||
<div class="alert alert-info" role="alert">You are already logged in!</div>
|
||||
{% else %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger" role="alert">Your username and password are incorrect. Please try again.</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not user.is_authenticated %}
|
||||
<form method="post" action=".">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_username">Username:</label>
|
||||
{{ form.username | addclass:"form-control" }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_password">Password:</label>
|
||||
{{ form.password | addclass:"form-control" }}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-sm-12 text-center small"><a href="{% url 'password_reset' %}">Forgot Password?</a></div>
|
||||
<div class="col-lg-6 col-sm-12 text-center small"><a href="{% url 'register' %}">Create Account</a></div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<input class="form-control btn btn-outline-secondary" type="submit" value="Login" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Password Changed
|
||||
{% 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">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Password Changed</h1>
|
||||
<hr />
|
||||
|
||||
<p>Your password was changed.</p>
|
||||
|
||||
<p>Click <a href="{% url 'index' %}">here</a> to return to the index.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Password Change
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% load addclass %}
|
||||
<div class="row">
|
||||
<div class="col-lg-6 offset-lg-3 col-sm-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Password Change</h1>
|
||||
<hr />
|
||||
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="?">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="form-field my-3">
|
||||
{{ field.label_tag }}
|
||||
{{ field | addclass:"form-control" }}
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text|safe }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<input class="form-control btn btn-outline-secondary" type="submit" value="Submit" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Forgot Password - Reset Successful
|
||||
{% 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">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Password Reset</h1>
|
||||
<hr />
|
||||
{% if user.is_authenticated %}
|
||||
<div class="alert alert-info" role="alert">You are already logged in!</div>
|
||||
{% else %}
|
||||
|
||||
<p>Your password has been successfully reset!</p>
|
||||
|
||||
<p>You may now log in using it <a href="{% url 'login' %}">here.</a></p>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Forgot Password - Reset
|
||||
{% 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">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Reset Password</h1>
|
||||
<hr />
|
||||
{% if not validlink %}
|
||||
<div class="alert alert-danger" role="alert">The password reset link has expired. Please request another to proceed.</div>
|
||||
{% else %}
|
||||
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action=".">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_username">Enter new password:</label>
|
||||
{{ form.new_password1 | addclass:"form-control" }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_username">Confirm password:</label>
|
||||
{{ form.new_password2 | addclass:"form-control" }}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<input class="form-control btn btn-outline-secondary" type="submit" value="Login" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Forgot Password - Reset Link Sent
|
||||
{% 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">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Reset Sent</h1>
|
||||
<hr />
|
||||
{% if user.is_authenticated %}
|
||||
<div class="alert alert-info" role="alert">You are already logged in!</div>
|
||||
{% else %}
|
||||
|
||||
<p>Instructions for resetting your password will be emailed to the
|
||||
address you provided, if that address matches the one we have on file
|
||||
for your account. You should receive them shortly.</p>
|
||||
|
||||
<p>Please allow up to to a few hours for the email to transmit, and be
|
||||
sure to check your spam folder if it doesn't show up in a timely manner.</p>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{% autoescape off %}
|
||||
To initiate the password reset process for your {{ user.get_username }} {{ site_name }} account,
|
||||
click the link below:
|
||||
|
||||
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
|
||||
|
||||
If clicking the link above doesn't work, please copy and paste the URL in a new browser
|
||||
window instead.
|
||||
|
||||
If you did not request a password reset, please disregard this notice. Whoever requested it
|
||||
cannot follow through on resetting your password without access to this message.
|
||||
|
||||
Sincerely,
|
||||
{{ site_name }} Management.
|
||||
{% endautoescape %}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Forgot Password
|
||||
{% 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">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Forgot Password</h1>
|
||||
<hr />
|
||||
{% if user.is_authenticated %}
|
||||
<div class="alert alert-info" role="alert">You are already logged in!</div>
|
||||
{% else %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger" role="alert">The email address provided is incorrect.</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not user.is_authenticated %}
|
||||
<form method="post" action=".">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_username">Email address:</label>
|
||||
{{ form.email | addclass:"form-control" }}
|
||||
<small id="emailHelp" class="form-text text-muted">The email address you provided at registration. If you left it blank, your password cannot be reset through this form.</small>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<input class="form-control btn btn-outline-secondary" type="submit" value="Login" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block titleblock %}
|
||||
Register
|
||||
{% 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">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Register</h1>
|
||||
<hr />
|
||||
{% if user.is_authenticated %}
|
||||
<div class="alert alert-info" role="alert">You are already registered!</div>
|
||||
{% else %}
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger" role="alert">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not user.is_authenticated %}
|
||||
<form method="post" action="?">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="form-field my-3">
|
||||
{{ field.label_tag }}
|
||||
{{ field | addclass:"form-control" }}
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text|safe }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<input class="form-control btn btn-outline-secondary" type="submit" value="Register" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
245
evennia/web/website/tests.py
Normal file
245
evennia/web/website/tests.py
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
from django.test import Client, override_settings
|
||||
from django.urls import reverse
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
||||
class EvenniaWebTest(EvenniaTest):
|
||||
|
||||
# Use the same classes the views are expecting
|
||||
account_typeclass = settings.BASE_ACCOUNT_TYPECLASS
|
||||
object_typeclass = settings.BASE_OBJECT_TYPECLASS
|
||||
character_typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
exit_typeclass = settings.BASE_EXIT_TYPECLASS
|
||||
room_typeclass = settings.BASE_ROOM_TYPECLASS
|
||||
script_typeclass = settings.BASE_SCRIPT_TYPECLASS
|
||||
|
||||
# Default named url
|
||||
url_name = 'index'
|
||||
|
||||
# Response to expect for unauthenticated requests
|
||||
unauthenticated_response = 200
|
||||
|
||||
# Response to expect for authenticated requests
|
||||
authenticated_response = 200
|
||||
|
||||
def setUp(self):
|
||||
super(EvenniaWebTest, self).setUp()
|
||||
|
||||
# Add chars to account rosters
|
||||
self.account.db._playable_characters = [self.char1]
|
||||
self.account2.db._playable_characters = [self.char2]
|
||||
|
||||
for account in (self.account, self.account2):
|
||||
# Demote accounts to Player permissions
|
||||
account.permissions.add('Player')
|
||||
account.permissions.remove('Developer')
|
||||
|
||||
# Grant permissions to chars
|
||||
for char in account.db._playable_characters:
|
||||
char.locks.add('edit:id(%s) or perm(Admin)' % account.pk)
|
||||
char.locks.add('delete:id(%s) or perm(Admin)' % account.pk)
|
||||
char.locks.add('view:all()')
|
||||
|
||||
def test_valid_chars(self):
|
||||
"Make sure account has playable characters"
|
||||
self.assertTrue(self.char1 in self.account.db._playable_characters)
|
||||
self.assertTrue(self.char2 in self.account2.db._playable_characters)
|
||||
|
||||
def get_kwargs(self):
|
||||
return {}
|
||||
|
||||
def test_get(self):
|
||||
# Try accessing page while not logged in
|
||||
response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()))
|
||||
self.assertEqual(response.status_code, self.unauthenticated_response)
|
||||
|
||||
def login(self):
|
||||
return self.client.login(username='TestAccount', password='testpassword')
|
||||
|
||||
def test_get_authenticated(self):
|
||||
logged_in = self.login()
|
||||
self.assertTrue(logged_in, 'Account failed to log in!')
|
||||
|
||||
# Try accessing page while logged in
|
||||
response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True)
|
||||
|
||||
self.assertEqual(response.status_code, self.authenticated_response)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
class AdminTest(EvenniaWebTest):
|
||||
url_name = 'django_admin'
|
||||
unauthenticated_response = 302
|
||||
|
||||
class IndexTest(EvenniaWebTest):
|
||||
url_name = 'index'
|
||||
|
||||
class RegisterTest(EvenniaWebTest):
|
||||
url_name = 'register'
|
||||
unauthenticated_response = 302
|
||||
|
||||
class LoginTest(EvenniaWebTest):
|
||||
url_name = 'login'
|
||||
|
||||
class LogoutTest(EvenniaWebTest):
|
||||
url_name = 'logout'
|
||||
|
||||
class PasswordResetTest(EvenniaWebTest):
|
||||
url_name = 'password_change'
|
||||
unauthenticated_response = 302
|
||||
|
||||
class WebclientTest(EvenniaWebTest):
|
||||
url_name = 'webclient:index'
|
||||
|
||||
class CharacterCreateView(EvenniaWebTest):
|
||||
url_name = 'character-create'
|
||||
unauthenticated_response = 302
|
||||
|
||||
@override_settings(MULTISESSION_MODE=0)
|
||||
def test_valid_access_multisession_0(self):
|
||||
"Account1 with no characters should be able to create a new one"
|
||||
self.account.db._playable_characters = []
|
||||
|
||||
# Login account
|
||||
self.login()
|
||||
|
||||
# Post data for a new character
|
||||
data = {
|
||||
'db_key': 'gannon',
|
||||
'desc': 'Some dude.'
|
||||
}
|
||||
|
||||
response = self.client.post(reverse(self.url_name), data=data, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make sure the character was actually created
|
||||
self.assertTrue(len(self.account.db._playable_characters) == 1, 'Account only has the following characters attributed to it: %s' % self.account.db._playable_characters)
|
||||
|
||||
@override_settings(MULTISESSION_MODE=2)
|
||||
@override_settings(MAX_NR_CHARACTERS=10)
|
||||
def test_valid_access_multisession_2(self):
|
||||
"Account1 should be able to create a new character"
|
||||
# Login account
|
||||
self.login()
|
||||
|
||||
# Post data for a new character
|
||||
data = {
|
||||
'db_key': 'gannon',
|
||||
'desc': 'Some dude.'
|
||||
}
|
||||
|
||||
response = self.client.post(reverse(self.url_name), data=data, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make sure the character was actually created
|
||||
self.assertTrue(len(self.account.db._playable_characters) > 1, 'Account only has the following characters attributed to it: %s' % self.account.db._playable_characters)
|
||||
|
||||
class CharacterPuppetView(EvenniaWebTest):
|
||||
url_name = 'character-puppet'
|
||||
unauthenticated_response = 302
|
||||
|
||||
def get_kwargs(self):
|
||||
return {
|
||||
'pk': self.char1.pk,
|
||||
'slug': slugify(self.char1.name)
|
||||
}
|
||||
|
||||
def test_invalid_access(self):
|
||||
"Account1 should not be able to puppet Account2:Char2"
|
||||
# Login account
|
||||
self.login()
|
||||
|
||||
# Try to access puppet page for char2
|
||||
kwargs = {
|
||||
'pk': self.char2.pk,
|
||||
'slug': slugify(self.char2.name)
|
||||
}
|
||||
response = self.client.get(reverse(self.url_name, kwargs=kwargs), follow=True)
|
||||
self.assertTrue(response.status_code >= 400, "Invalid access should return a 4xx code-- either obj not found or permission denied! (Returned %s)" % response.status_code)
|
||||
|
||||
class CharacterManageView(EvenniaWebTest):
|
||||
url_name = 'character-manage'
|
||||
unauthenticated_response = 302
|
||||
|
||||
class CharacterUpdateView(EvenniaWebTest):
|
||||
url_name = 'character-update'
|
||||
unauthenticated_response = 302
|
||||
|
||||
def get_kwargs(self):
|
||||
return {
|
||||
'pk': self.char1.pk,
|
||||
'slug': slugify(self.char1.name)
|
||||
}
|
||||
|
||||
def test_valid_access(self):
|
||||
"Account1 should be able to update Account1:Char1"
|
||||
# Login account
|
||||
self.login()
|
||||
|
||||
# Try to access update page for char1
|
||||
response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Try to update char1 desc
|
||||
data = {'db_key': self.char1.db_key, 'desc': "Just a regular type of dude."}
|
||||
response = self.client.post(reverse(self.url_name, kwargs=self.get_kwargs()), data=data, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make sure the change was made successfully
|
||||
self.assertEqual(self.char1.db.desc, data['desc'])
|
||||
|
||||
def test_invalid_access(self):
|
||||
"Account1 should not be able to update Account2:Char2"
|
||||
# Login account
|
||||
self.login()
|
||||
|
||||
# Try to access update page for char2
|
||||
kwargs = {
|
||||
'pk': self.char2.pk,
|
||||
'slug': slugify(self.char2.name)
|
||||
}
|
||||
response = self.client.get(reverse(self.url_name, kwargs=kwargs), follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
class CharacterDeleteView(EvenniaWebTest):
|
||||
url_name = 'character-delete'
|
||||
unauthenticated_response = 302
|
||||
|
||||
def get_kwargs(self):
|
||||
return {
|
||||
'pk': self.char1.pk,
|
||||
'slug': slugify(self.char1.name)
|
||||
}
|
||||
|
||||
def test_valid_access(self):
|
||||
"Account1 should be able to delete Account1:Char1"
|
||||
# Login account
|
||||
self.login()
|
||||
|
||||
# Try to access delete page for char1
|
||||
response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Proceed with deleting it
|
||||
data = {'value': 'yes'}
|
||||
response = self.client.post(reverse(self.url_name, kwargs=self.get_kwargs()), data=data, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make sure it deleted
|
||||
self.assertFalse(self.char1 in self.account.db._playable_characters, 'Char1 is still in Account playable characters list.')
|
||||
|
||||
def test_invalid_access(self):
|
||||
"Account1 should not be able to delete Account2:Char2"
|
||||
# Login account
|
||||
self.login()
|
||||
|
||||
# Try to access delete page for char2
|
||||
kwargs = {
|
||||
'pk': self.char2.pk,
|
||||
'slug': slugify(self.char2.name)
|
||||
}
|
||||
response = self.client.get(reverse(self.url_name, kwargs=kwargs), follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
|
@ -9,12 +9,20 @@ from django import views as django_views
|
|||
from evennia.web.website import views as website_views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', website_views.page_index, name="index"),
|
||||
url(r'^$', website_views.EvenniaIndexView.as_view(), name="index"),
|
||||
url(r'^tbi/', website_views.to_be_implemented, name='to_be_implemented'),
|
||||
|
||||
# User Authentication (makes login/logout url names available)
|
||||
url(r'^authenticate/', include('django.contrib.auth.urls')),
|
||||
|
||||
url(r'^auth/', include('django.contrib.auth.urls')),
|
||||
url(r'^auth/register', website_views.AccountCreateView.as_view(), name="register"),
|
||||
|
||||
# Character management
|
||||
url(r'^characters/create/$', website_views.CharacterCreateView.as_view(), name="character-create"),
|
||||
url(r'^characters/manage/$', website_views.CharacterManageView.as_view(), name="character-manage"),
|
||||
url(r'^characters/puppet/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$', website_views.CharacterPuppetView.as_view(), name="character-puppet"),
|
||||
url(r'^characters/update/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$', website_views.CharacterUpdateView.as_view(), name="character-update"),
|
||||
url(r'^characters/delete/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$', website_views.CharacterDeleteView.as_view(), name="character-delete"),
|
||||
|
||||
# Django original admin page. Make this URL is always available, whether
|
||||
# we've chosen to use Evennia's custom admin or not.
|
||||
url(r'django_admin/', website_views.admin_wrapper, name="django_admin"),
|
||||
|
|
|
|||
|
|
@ -7,16 +7,27 @@ 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 HttpResponseBadRequest, HttpResponseRedirect, Http404
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.views.generic import View, TemplateView, ListView, DetailView, FormView
|
||||
from django.views.generic.base import RedirectView
|
||||
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.utils import class_from_module, logger
|
||||
from evennia.web.website.forms import *
|
||||
|
||||
from django.contrib.auth import login
|
||||
from django.utils.text import slugify
|
||||
|
||||
_BASE_CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
|
||||
|
||||
|
|
@ -92,20 +103,6 @@ def _gamestats():
|
|||
return pagevars
|
||||
|
||||
|
||||
def page_index(request):
|
||||
"""
|
||||
Main root page.
|
||||
"""
|
||||
|
||||
# handle webclient-website shared login
|
||||
_shared_login(request)
|
||||
|
||||
# get game db stats
|
||||
pagevars = _gamestats()
|
||||
|
||||
return render(request, 'index.html', pagevars)
|
||||
|
||||
|
||||
def to_be_implemented(request):
|
||||
"""
|
||||
A notice letting the user know that this particular feature hasn't been
|
||||
|
|
@ -134,3 +131,267 @@ def admin_wrapper(request):
|
|||
Wrapper that allows us to properly use the base Django admin site, if needed.
|
||||
"""
|
||||
return staff_member_required(site.index)(request)
|
||||
|
||||
#
|
||||
# Class-based views
|
||||
#
|
||||
|
||||
class EvenniaIndexView(TemplateView):
|
||||
# Display this HTML page
|
||||
template_name = 'website/index.html'
|
||||
|
||||
# Display these variables on it
|
||||
def get_context_data(self, **kwargs):
|
||||
# Call the base implementation first to get a context object
|
||||
context = super(EvenniaIndexView, self).get_context_data(**kwargs)
|
||||
|
||||
# Add game statistics and other pagevars
|
||||
context.update(_gamestats())
|
||||
|
||||
return context
|
||||
|
||||
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 = class_from_module(settings.BASE_OBJECT_TYPECLASS)
|
||||
access_type = 'view'
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
if not queryset:
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# Get the object, ignoring all checks and filters
|
||||
obj = self.model.objects.get(pk=self.kwargs.get('pk'))
|
||||
|
||||
# Check if this object was requested in a valid manner
|
||||
if slugify(obj.name) != self.kwargs.get(self.slug_url_kwarg):
|
||||
raise HttpResponseBadRequest(u"No %(verbose_name)s found matching the query" %
|
||||
{'verbose_name': queryset.model._meta.verbose_name})
|
||||
|
||||
# Check if account has permissions to access object
|
||||
account = self.request.user
|
||||
if not obj.access(account, self.access_type):
|
||||
raise PermissionDenied(u"You are not authorized to %s this object." % self.access_type)
|
||||
|
||||
# Get the object, based on the specified queryset
|
||||
obj = super(ObjectDetailView, self).get_object(queryset)
|
||||
|
||||
return obj
|
||||
|
||||
class ObjectCreateView(LoginRequiredMixin, EvenniaCreateView):
|
||||
|
||||
model = class_from_module(settings.BASE_OBJECT_TYPECLASS)
|
||||
|
||||
class ObjectDeleteView(LoginRequiredMixin, ObjectDetailView, EvenniaDeleteView):
|
||||
|
||||
model = class_from_module(settings.BASE_OBJECT_TYPECLASS)
|
||||
access_type = 'delete'
|
||||
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 = class_from_module(settings.BASE_OBJECT_TYPECLASS)
|
||||
access_type = 'edit'
|
||||
|
||||
def get_success_url(self):
|
||||
if self.success_url: return self.success_url
|
||||
return self.object.web_get_detail_url()
|
||||
|
||||
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.get_success_url())
|
||||
|
||||
#
|
||||
# Account views
|
||||
#
|
||||
|
||||
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):
|
||||
|
||||
username = form.cleaned_data['username']
|
||||
password = form.cleaned_data['password1']
|
||||
email = form.cleaned_data.get('email', '')
|
||||
|
||||
# Create account
|
||||
account, errs = self.model.create(
|
||||
username=username,
|
||||
password=password,
|
||||
email=email,)
|
||||
|
||||
# If unsuccessful, get messages passed to session.msg
|
||||
if not account:
|
||||
[messages.error(self.request, err) for err in errs]
|
||||
return self.form_invalid(form)
|
||||
|
||||
messages.success(self.request, "Your account '%s' was successfully created! You may log in using it now." % account.name)
|
||||
return HttpResponseRedirect(self.success_url)
|
||||
|
||||
#
|
||||
# Character views
|
||||
#
|
||||
|
||||
class CharacterMixin(object):
|
||||
|
||||
model = class_from_module(settings.BASE_CHARACTER_TYPECLASS)
|
||||
form_class = CharacterForm
|
||||
success_url = reverse_lazy('character-manage')
|
||||
|
||||
def get_queryset(self):
|
||||
# Get IDs of characters owned by account
|
||||
ids = [getattr(x, 'id') for x in self.request.user.characters if x]
|
||||
|
||||
# Return a queryset consisting of those characters
|
||||
return self.model.objects.filter(id__in=ids).order_by(Lower('db_key'))
|
||||
|
||||
class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, ObjectDetailView):
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
# Get the requested character, if it belongs to the authenticated user
|
||||
char = self.get_object()
|
||||
next = self.request.GET.get('next', self.success_url)
|
||||
|
||||
if char:
|
||||
self.request.session['puppet'] = int(char.pk)
|
||||
messages.success(self.request, "You become '%s'!" % char)
|
||||
else:
|
||||
self.request.session['puppet'] = None
|
||||
messages.error(self.request, "You cannot become '%s'." % char)
|
||||
|
||||
return next
|
||||
|
||||
class CharacterManageView(LoginRequiredMixin, CharacterMixin, ListView):
|
||||
|
||||
paginate_by = 10
|
||||
template_name = 'website/character_manage_list.html'
|
||||
page_title = 'Manage Characters'
|
||||
|
||||
class CharacterUpdateView(CharacterMixin, ObjectUpdateView):
|
||||
|
||||
form_class = CharacterUpdateForm
|
||||
template_name = 'website/character_form.html'
|
||||
|
||||
class CharacterDeleteView(CharacterMixin, ObjectDeleteView):
|
||||
pass
|
||||
|
||||
class CharacterCreateView(CharacterMixin, ObjectCreateView):
|
||||
|
||||
template_name = 'website/character_form.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
# Get account ref
|
||||
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
|
||||
try:
|
||||
character, errors = self.model.create(charname, account, description=description)
|
||||
|
||||
# Assign attributes from form
|
||||
[setattr(character.db, key, value) for key,value in self.attributes.items()]
|
||||
character.db.creator_id = account.id
|
||||
character.save()
|
||||
account.save()
|
||||
|
||||
except Exception as e:
|
||||
messages.error(self.request, "There was an error creating your character. If this problem persists, contact an admin.")
|
||||
logger.log_trace()
|
||||
return self.form_invalid(form)
|
||||
|
||||
if character:
|
||||
messages.success(self.request, "Your character '%s' was created!" % character.name)
|
||||
return HttpResponseRedirect(self.success_url)
|
||||
else:
|
||||
messages.error(self.request, "Your character could not be created. Please contact an admin.")
|
||||
return self.form_invalid(form)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue