Skip to content

Commit

Permalink
Merge pull request #1671 from strikaco/modernize
Browse files Browse the repository at this point in the history
Frontend CRUD enhancements (character/account creation and management)
  • Loading branch information
Griatch committed Oct 26, 2018
2 parents 31b017b + 70e9f43 commit ddb3cd4
Show file tree
Hide file tree
Showing 29 changed files with 1,310 additions and 70 deletions.
16 changes: 15 additions & 1 deletion evennia/accounts/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,19 @@ def nicks(self):
@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

Expand Down Expand Up @@ -720,7 +733,8 @@ def create(cls, *args, **kwargs):

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
Expand Down
12 changes: 10 additions & 2 deletions evennia/objects/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -1944,12 +1944,20 @@ def create(cls, key, account, **kwargs):
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:
Expand All @@ -1963,7 +1971,7 @@ def create(cls, key, account, **kwargs):
# 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)
Expand Down
2 changes: 2 additions & 0 deletions evennia/settings_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@
'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.
Expand Down Expand Up @@ -789,6 +790,7 @@
'django.contrib.flatpages',
'django.contrib.sites',
'django.contrib.staticfiles',
'django.contrib.messages',
'sekizai',
'evennia.utils.idmapper',
'evennia.server',
Expand Down
32 changes: 32 additions & 0 deletions evennia/typeclasses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,38 @@ def web_get_detail_url(self):
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):
"""
Expand Down
10 changes: 10 additions & 0 deletions evennia/web/utils/general_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 15 additions & 7 deletions evennia/web/utils/tests.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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'],
Expand Down
56 changes: 56 additions & 0 deletions evennia/web/website/forms.py
Original file line number Diff line number Diff line change
@@ -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
27 changes: 23 additions & 4 deletions evennia/web/website/templates/website/_menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,28 @@
{% 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>
Expand All @@ -51,7 +70,7 @@
<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 %}
Expand Down
14 changes: 11 additions & 3 deletions evennia/web/website/templates/website/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -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" %}">
Expand All @@ -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">
Expand All @@ -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>
Expand All @@ -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 changes: 51 additions & 0 deletions evennia/web/website/templates/website/character_form.html
Original file line number Diff line number Diff line change
@@ -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 comments on commit ddb3cd4

Please sign in to comment.