Fix merge conflicts

This commit is contained in:
Griatch 2019-01-01 15:19:20 +01:00
commit 981119b640
89 changed files with 5435 additions and 818 deletions

View file

@ -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,

View file

@ -0,0 +1,61 @@
from django.contrib.auth import authenticate, login
from evennia.accounts.models import AccountDB
from evennia.utils import logger
class SharedLoginMiddleware(object):
"""
Handle the shared login between website and webclient.
"""
def __init__(self, get_response):
# One-time configuration and initialization.
self.get_response = get_response
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
# Synchronize credentials between webclient and website
# Must be performed *before* rendering the view (issue #1723)
self.make_shared_login(request)
# Process view
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
# Return processed view
return response
@classmethod
def make_shared_login(cls, request):
csession = request.session
account = request.user
website_uid = csession.get("website_authenticated_uid", None)
webclient_uid = csession.get("webclient_authenticated_uid", None)
if not csession.session_key:
# this is necessary to build the sessid key
csession.save()
if account.is_authenticated():
# Logged into website
if not website_uid:
# fresh website login (just from login page)
csession["website_authenticated_uid"] = account.id
if webclient_uid is None:
# auto-login web client
csession["webclient_authenticated_uid"] = account.id
elif webclient_uid:
# Not logged into website, but logged into webclient
if not website_uid:
csession["website_authenticated_uid"] = account.id
account = AccountDB.objects.get(id=webclient_uid)
try:
# calls our custom authenticate, in web/utils/backend.py
authenticate(autologin=account)
login(request, account)
except AttributeError:
logger.log_trace()

View file

@ -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'],

View file

@ -102,7 +102,7 @@ An "emitter" object must have a function
return;
}
this.connection.connect();
log('Evenna reconnecting.')
log('Evennia reconnecting.')
},
// Returns true if the connection is open.

View file

@ -69,8 +69,12 @@ let history_plugin = (function () {
}
if (history_entry !== null) {
// Doing a history navigation; replace the text in the input.
inputfield.val(history_entry);
// Performing a history navigation
// replace the text in the input and move the cursor to the end of the new value
inputfield.val('');
inputfield.blur().focus().val(history_entry);
event.preventDefault();
return true;
}
return false;

View file

@ -30,32 +30,31 @@ plugin_handler.add('hotbuttons', (function () {
// Add Buttons
var addButtonsUI = function () {
var buttons = $( [
'<div id="buttons" class="split split-vertical">',
' <div id="buttonsform" class="wrapper">',
' <div id="buttonscontrol" class="input-group">',
' <button class="btn" id="assign_button0" type="button" value="button0">unassigned</button>',
' <button class="btn" id="assign_button1" type="button" value="button1">unassigned</button>',
' <button class="btn" id="assign_button2" type="button" value="button2">unassigned</button>',
' <button class="btn" id="assign_button3" type="button" value="button3">unassigned</button>',
' <button class="btn" id="assign_button4" type="button" value="button4">unassigned</button>',
' <button class="btn" id="assign_button5" type="button" value="button5">unassigned</button>',
' <button class="btn" id="assign_button6" type="button" value="button6">unassigned</button>',
' <button class="btn" id="assign_button7" type="button" value="button7">unassigned</button>',
' <button class="btn" id="assign_button8" type="button" value="button8">unassigned</button>',
' </div>',
' </div>',
'</div>',
].join("\n") );
'<div id="buttons" class="split split-vertical">',
' <div id="buttonsform">',
' <div id="buttonscontrol" class="input-group">',
' <button class="btn" id="assign_button0" type="button" value="button0">unassigned</button>',
' <button class="btn" id="assign_button1" type="button" value="button1">unassigned</button>',
' <button class="btn" id="assign_button2" type="button" value="button2">unassigned</button>',
' <button class="btn" id="assign_button3" type="button" value="button3">unassigned</button>',
' <button class="btn" id="assign_button4" type="button" value="button4">unassigned</button>',
' <button class="btn" id="assign_button5" type="button" value="button5">unassigned</button>',
' <button class="btn" id="assign_button6" type="button" value="button6">unassigned</button>',
' <button class="btn" id="assign_button7" type="button" value="button7">unassigned</button>',
' <button class="btn" id="assign_button8" type="button" value="button8">unassigned</button>',
' </div>',
' </div>',
'</div>',
].join("\n") );
// Add buttons in front of the existing #inputform
buttons.insertBefore('#inputform');
$('#inputform').addClass('split split-vertical');
$('#input').prev().replaceWith(buttons);
Split(['#buttons','#inputform'], {
Split(['#main','#buttons','#input'], {
sizes: [85,5,10],
direction: 'vertical',
sizes: [50,50],
gutterSize: 4,
minSize: 150,
minSize: [150,20,50],
});
}

View file

@ -77,10 +77,12 @@ let options_plugin = (function () {
if (code === 27) { // Escape key
if ($('#helpdialog').is(':visible')) {
plugins['popups'].closePopup("#helpdialog");
} else {
plugins['popups'].closePopup("#optionsdialog");
return true;
}
if ($('#optionsdialog').is(':visible')) {
plugins['popups'].closePopup("#optionsdialog");
return true;
}
return true;
}
return false;
}
@ -129,6 +131,21 @@ let options_plugin = (function () {
plugins['popups'].closePopup("#helpdialog");
}
//
// Make sure to close any dialogs on connection lost
var onText = function (args, kwargs) {
// is helppopup set? and if so, does this Text have type 'help'?
if ('helppopup' in options && options['helppopup'] ) {
if (kwargs && ('type' in kwargs) && (kwargs['type'] == 'help') ) {
$('#helpdialogcontent').append('<div>'+ args + '</div>');
plugins['popups'].togglePopup("#helpdialog");
return true;
}
}
return false;
}
//
// Register and init plugin
var init = function () {
@ -155,6 +172,7 @@ let options_plugin = (function () {
onGotOptions: onGotOptions,
onPrompt: onPrompt,
onConnectionClose: onConnectionClose,
onText: onText,
}
})()
plugin_handler.add('options', options_plugin);

View file

@ -183,12 +183,12 @@ let splithandler_plugin = (function () {
var dialog = $("#splitdialogcontent");
dialog.empty();
var selection = '<select name="pane">';
var selection = '<select name="pane">';
for ( var pane in split_panes ) {
selection = selection + '<option value="' + pane + '">' + pane + '</option>';
selection = selection + '<option value="' + pane + '">' + pane + '</option>';
}
selection = "Pane to split: " + selection + "</select> ";
dialog.append(selection);
selection = "Pane to split: " + selection + "</select> ";
dialog.append(selection);
dialog.append('<input type="radio" name="direction" value="vertical" checked>top/bottom </>');
dialog.append('<input type="radio" name="direction" value="horizontal">side-by-side <hr />');
@ -203,7 +203,7 @@ let splithandler_plugin = (function () {
dialog.append('<input type="radio" name="flow2" value="replace">replace </>');
dialog.append('<input type="radio" name="flow2" value="append">append <hr />');
dialog.append('<div id="splitclose" class="btn btn-large btn-outline-primary float-right">Split</div>');
dialog.append('<div id="splitclose" class="btn btn-large btn-outline-primary float-right">Split</div>');
$("#splitclose").bind("click", onSplitDialogClose);
@ -251,21 +251,21 @@ let splithandler_plugin = (function () {
var dialog = $("#panedialogcontent");
dialog.empty();
var selection = '<select name="assign-pane">';
var selection = '<select name="assign-pane">';
for ( var pane in split_panes ) {
selection = selection + '<option value="' + pane + '">' + pane + '</option>';
selection = selection + '<option value="' + pane + '">' + pane + '</option>';
}
selection = "Assign to pane: " + selection + "</select> <hr />";
dialog.append(selection);
selection = "Assign to pane: " + selection + "</select> <hr />";
dialog.append(selection);
var multiple = '<select multiple name="assign-type">';
var multiple = '<select multiple name="assign-type">';
for ( var type in known_types ) {
multiple = multiple + '<option value="' + known_types[type] + '">' + known_types[type] + '</option>';
multiple = multiple + '<option value="' + known_types[type] + '">' + known_types[type] + '</option>';
}
multiple = "Content types: " + multiple + "</select> <hr />";
dialog.append(multiple);
multiple = "Content types: " + multiple + "</select> <hr />";
dialog.append(multiple);
dialog.append('<div id="paneclose" class="btn btn-large btn-outline-primary float-right">Assign</div>');
dialog.append('<div id="paneclose" class="btn btn-large btn-outline-primary float-right">Assign</div>');
$("#paneclose").bind("click", onPaneControlDialogClose);
@ -276,9 +276,9 @@ let splithandler_plugin = (function () {
// Close "Pane Controls" dialog
var onPaneControlDialogClose = function () {
var pane = $("select[name=assign-pane]").val();
var types = $("select[name=assign-type]").val();
var types = $("select[name=assign-type]").val();
// var types = new Array;
// var types = new Array;
// $('#splitdialogcontent input[type=checkbox]:checked').each(function() {
// types.push( $(this).attr('value') );
// });
@ -287,24 +287,24 @@ let splithandler_plugin = (function () {
plugins['popups'].closePopup("#panedialog");
}
//
// helper function sending text to a pane
var txtToPane = function (panekey, txt) {
var pane = split_panes[panekey];
var text_div = $('#' + panekey + '-sub');
var pane = split_panes[panekey];
var text_div = $('#' + panekey + '-sub');
if ( pane['update_method'] == 'replace' ) {
text_div.html(txt)
} else if ( pane['update_method'] == 'append' ) {
text_div.append(txt);
var scrollHeight = text_div.parent().prop("scrollHeight");
text_div.parent().animate({ scrollTop: scrollHeight }, 0);
} else { // line feed
text_div.append("<div class='out'>" + txt + "</div>");
var scrollHeight = text_div.parent().prop("scrollHeight");
text_div.parent().animate({ scrollTop: scrollHeight }, 0);
}
if ( pane['update_method'] == 'replace' ) {
text_div.html(txt)
} else if ( pane['update_method'] == 'append' ) {
text_div.append(txt);
var scrollHeight = text_div.parent().prop("scrollHeight");
text_div.parent().animate({ scrollTop: scrollHeight }, 0);
} else { // line feed
text_div.append("<div class='out'>" + txt + "</div>");
var scrollHeight = text_div.parent().prop("scrollHeight");
text_div.parent().animate({ scrollTop: scrollHeight }, 0);
}
}
@ -316,53 +316,76 @@ let splithandler_plugin = (function () {
//
// Accept plugin onText events
var onText = function (args, kwargs) {
// If the message is not itself tagged, we'll assume it
// should go into any panes with 'all' or 'rest' set
// If the message is not itself tagged, we'll assume it
// should go into any panes with 'all' or 'rest' set
var msgtype = "rest";
if ( kwargs && 'type' in kwargs ) {
msgtype = kwargs['type'];
msgtype = kwargs['type'];
if ( ! known_types.includes(msgtype) ) {
// this is a new output type that can be mapped to panes
console.log('detected new output type: ' + msgtype)
known_types.push(msgtype);
}
}
var target_panes = [];
var rest_panes = [];
for (var key in split_panes) {
var pane = split_panes[key];
// is this message type mapped to this pane (or does the pane has an 'all' type)?
if (pane['types'].length > 0) {
if (pane['types'].includes(msgtype) || pane['types'].includes('all')) {
target_panes.push(key);
} else if (pane['types'].includes('rest')) {
// store rest-panes in case we have no explicit to send to
rest_panes.push(key);
}
} else {
// unassigned panes are assumed to be rest-panes too
rest_panes.push(key);
}
}
var ntargets = target_panes.length;
var nrests = rest_panes.length;
if (ntargets > 0) {
// we have explicit target panes to send to
for (var i=0; i<ntargets; i++) {
txtToPane(target_panes[i], args[0]);
}
return true;
} else if (nrests > 0) {
// no targets, send remainder to rest-panes/unassigned
for (var i=0; i<nrests; i++) {
txtToPane(rest_panes[i], args[0]);
}
return true;
}
// unhandled message
}
var target_panes = [];
var rest_panes = [];
for (var key in split_panes) {
var pane = split_panes[key];
// is this message type mapped to this pane (or does the pane has an 'all' type)?
if (pane['types'].length > 0) {
if (pane['types'].includes(msgtype) || pane['types'].includes('all')) {
target_panes.push(key);
} else if (pane['types'].includes('rest')) {
// store rest-panes in case we have no explicit to send to
rest_panes.push(key);
}
} else {
// unassigned panes are assumed to be rest-panes too
rest_panes.push(key);
}
}
var ntargets = target_panes.length;
var nrests = rest_panes.length;
if (ntargets > 0) {
// we have explicit target panes to send to
for (var i=0; i<ntargets; i++) {
txtToPane(target_panes[i], args[0]);
}
return true;
} else if (nrests > 0) {
// no targets, send remainder to rest-panes/unassigned
for (var i=0; i<nrests; i++) {
txtToPane(rest_panes[i], args[0]);
}
return true;
}
// unhandled message
return false;
}
//
// onKeydown check for 'ESC' key.
var onKeydown = function (event) {
var code = event.which;
if (code === 27) { // Escape key
if ($('#splitdialog').is(':visible')) {
plugins['popups'].closePopup("#splitdialog");
return true;
}
if ($('#panedialog').is(':visible')) {
plugins['popups'].closePopup("#panedialog");
return true;
}
}
// capture all keys while one of our "modal" dialogs is open
if ($('#splitdialogcontent').is(':visible') || $('#panedialogcontent').is(':visible')) {
return true;
}
return false;
}
@ -409,6 +432,7 @@ let splithandler_plugin = (function () {
dynamic_split: dynamic_split,
undo_split: undo_split,
set_pane_types: set_pane_types,
onKeydown: onKeydown,
}
})()
plugin_handler.add('splithandler', splithandler_plugin);

View file

@ -64,7 +64,7 @@ JQuery available.
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script>
<!-- set up splits before loading the GUI -->
<script src="https://unpkg.com/split.js/split.min.js"></script>
<script src="https://unpkg.com/split.js@1.5.9/dist/split.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
<!-- Load gui library -->
@ -73,10 +73,10 @@ JQuery available.
<script src={% static "webclient/js/plugins/popups.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/options.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/history.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/splithandler.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/default_in.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/oob.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/notifications.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/splithandler.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/default_out.js" %} language="javascript" type="text/javascript"></script>
{% endblock %}

View file

@ -5,6 +5,8 @@ page and serve it eventual static content.
"""
from django.conf import settings
from django.http import Http404
from django.shortcuts import render
from django.contrib.auth import login, authenticate
@ -12,51 +14,16 @@ from evennia.accounts.models import AccountDB
from evennia.utils import logger
def _shared_login(request):
"""
Handle the shared login between website and webclient.
"""
csession = request.session
account = request.user
# these can have 3 values:
# None - previously unused (auto-login)
# False - actively logged out (don't auto-login)
# <uid> - logged in User/Account id
website_uid = csession.get("website_authenticated_uid", None)
webclient_uid = csession.get("webclient_authenticated_uid", None)
# check if user has authenticated to website
if not csession.session_key:
# this is necessary to build the sessid key
csession.save()
if webclient_uid:
# The webclient has previously registered a login to this browser_session
if not account.is_authenticated() and not website_uid:
try:
account = AccountDB.objects.get(id=webclient_uid)
except AccountDB.DoesNotExist:
# this can happen e.g. for guest accounts or deletions
csession["website_authenticated_uid"] = False
csession["webclient_authenticated_uid"] = False
return
try:
# calls our custom authenticate in web/utils/backends.py
account = authenticate(autologin=account)
login(request, account)
csession["website_authenticated_uid"] = webclient_uid
except AttributeError:
logger.log_trace()
def webclient(request):
"""
Webclient page template loading.
"""
# handle webclient-website shared login
_shared_login(request)
# auto-login is now handled by evennia.web.utils.middleware
# check if webclient should be enabled
if not settings.WEBCLIENT_ENABLED:
raise Http404
# make sure to store the browser session's hash so the webclient can get to it!
pagevars = {'browser_sessid': request.session.session_key}

View file

@ -0,0 +1,157 @@
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):
"""
This is a stock Django form, but modified so that all values provided
through it are escaped (sanitized). Validation is performed by the fields
you define in the form.
This has little to do with Evennia itself and is more general web security-
related.
https://www.owasp.org/index.php/Input_Validation_Cheat_Sheet#Goals_of_Input_Validation
"""
def clean(self):
"""
Django hook. Performed on form submission.
Returns:
cleaned (dict): Dictionary of key:value pairs submitted on the form.
"""
# Call parent function
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(UserCreationForm):
"""
This is a generic Django form tailored to the Account model.
In this incarnation it does not allow getting/setting of attributes, only
core User model fields (username, email, password).
"""
class Meta:
"""
This is a Django construct that provides additional configuration to
the form.
"""
# The model/typeclass this form creates
model = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
# The fields to display on the form, in the given order
fields = ("username", "email")
# Any overrides of field classes
field_classes = {'username': UsernameField}
# Username is collected as part of the core UserCreationForm, so we just need
# to add a field to (optionally) capture email.
email = forms.EmailField(help_text="A valid email address. Optional; used for password resets.", required=False)
class ObjectForm(EvenniaForm, ModelForm):
"""
This is a Django form for generic Evennia Objects that allows modification
of attributes when called from a descendent of ObjectUpdate or ObjectCreate
views.
It defines no fields by default; you have to do that by extending this class
and defining what fields you want to be recorded. See the CharacterForm for
a simple example of how to do this.
"""
class Meta:
"""
This is a Django construct that provides additional configuration to
the form.
"""
# The model/typeclass this form creates
model = class_from_module(settings.BASE_OBJECT_TYPECLASS)
# The fields to display on the form, in the given order
fields = ("db_key",)
# This lets us rename ugly db-specific keys to something more human
labels = {
'db_key': 'Name',
}
class CharacterForm(ObjectForm):
"""
This is a Django form for Evennia Character objects.
Since Evennia characters only have one attribute by default, this form only
defines a field for that single attribute. The names of fields you define should
correspond to their names as stored in the dbhandler; you can display
'prettier' versions of the fieldname on the form using the 'label' kwarg.
The basic field types are CharFields and IntegerFields, which let you enter
text and numbers respectively. IntegerFields have some neat validation tricks
they can do, like mandating values fall within a certain range.
For example, a complete "age" field (which stores its value to
`character.db.age` might look like:
age = forms.IntegerField(
label="Your Age",
min_value=18, max_value=9000,
help_text="Years since your birth.")
Default input fields are generic single-line text boxes. You can control what
sort of input field users will see by specifying a "widget." An example of
this is used for the 'desc' field to show a Textarea box instead of a Textbox.
For help in building out your form, please see:
https://docs.djangoproject.com/en/1.11/topics/forms/#building-a-form-in-django
For more information on fields and their capabilities, see:
https://docs.djangoproject.com/en/1.11/ref/forms/fields/
For more on widgets, see:
https://docs.djangoproject.com/en/1.11/ref/forms/widgets/
"""
class Meta:
"""
This is a Django construct that provides additional configuration to
the form.
"""
# 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 configurable attributes on the Character object.
desc = forms.CharField(label='Description', max_length=2048, required=False,
widget=forms.Textarea(attrs={'rows': 3}),
help_text="A brief description of your character.")
class CharacterUpdateForm(CharacterForm):
"""
This is a Django form for updating Evennia Character objects.
By default it is the same as the CharacterForm, but if there are circumstances
in which you don't want to let players edit all the same attributes they had
access to during creation, you can redefine this form with those fields you do
wish to allow.
"""
pass

View file

@ -23,35 +23,67 @@ folder and edit it to add/remove links to the menu.
<ul class="navbar-nav">
{% block nabvar_left %}
<li>
<a class="nav-link" href="/">Home</a>
<a class="nav-link" href="{% url 'index' %}">Home</a>
</li>
<!-- evennia documentation -->
<li>
<a class="nav-link" href="https://github.com/evennia/evennia/wiki/Evennia-Introduction/">About</a>
</li>
<li><a class="nav-link" href="https://github.com/evennia/evennia/wiki">Documentation</a></li>
<li><a class="nav-link" href="{% url 'admin:index' %}">Admin Interface</a></li>
<!-- end evennia documentation -->
<!-- game views -->
<li><a class="nav-link" href="{% url 'characters' %}">Characters</a></li>
<li><a class="nav-link" href="{% url 'channels' %}">Channels</a></li>
<li><a class="nav-link" href="{% url 'help' %}">Help</a></li>
<!-- end game views -->
{% if webclient_enabled %}
<li><a class="nav-link" href="{% url 'webclient:index' %}">Play Online</a></li>
{% endif %}
{% if user.is_staff %}
<li><a class="nav-link" href="{% url 'admin:index' %}">Admin</a></li>
{% endif %}
{% endblock %}
</ul>
<ul class="nav navbar-nav ml-auto w-120 justify-content-end">
{% 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>
</li>
{% else %}
<li>
<a class="nav-link" href="{% url 'login' %}">Log In</a>
<a class="nav-link" href="{% url 'login' %}?next={{ request.path }}">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 %}

View file

@ -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>

View file

@ -0,0 +1,94 @@
{% extends "base.html" %}
{% block titleblock %}
{{ view.page_title }} ({{ object }})
{% 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 }} ({{ object }})</h1>
<hr />
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'channels' %}">Channels</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ object.db_key|title }}</li>
</ol>
<hr />
<div class="row">
<!-- left column -->
<div class="col-lg-9 col-sm-12">
<!-- heading -->
<div class="card border-light">
<div class="card-body">
{% if object.db.desc and object.db.desc != None %}{{ object.db.desc }}{% else %}No description provided.{% endif %}
</div>
</div>
<hr />
<!-- end heading -->
{% if object_list %}
<pre>{% for log in object_list %}
<a id="{{ log.key }}"></a>{{ log.timestamp }}: {{ log.message }}{% endfor %}</pre>
{% else %}
<pre>No recent log entries have been recorded for this channel.</pre>
{% endif %}
</div>
<!-- end left column -->
<!-- right column -->
<div class="col-lg-3 col-sm-12">
{% if request.user.is_staff %}
<!-- admin button -->
<a class="btn btn-info btn-block mb-3" href="{{ object.web_get_admin_url }}">Admin</a>
<!-- end admin button -->
<hr />
{% endif %}
<div class="card mb-3">
<div class="card-body">
<dl>
{% for attribute, value in attribute_list.items %}
<dt>{{ attribute }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
<dl>
<dt>Subscriptions</dt>
<dd>{{ object.subscriptions.all|length }}</dd>
</dl>
</div>
</div>
{% if object_filters %}
<div class="card mb-3">
<div class="card-header">Date Index</div>
<ul class="list-group list-group-flush">
{% for filter in object_filters %}
<a href="#{{ filter }}" class="list-group-item">{{ filter }}</a>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<!-- end right column -->
</div>
<hr />
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,86 @@
{% 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 />
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'channels' %}">Channels</a></li>
</ol>
<hr />
<div class="row">
<!-- left column -->
<div class="col-lg-9 col-sm-12">
<div class="table-responsive">
<table class="table table-sm">
<thead class="thead-light">
<tr>
<th scope="col">Channel</th>
<th scope="col">Description</th>
<th scope="col">Subscriptions</th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr>
<td><a href="{{ object.web_get_detail_url }}">{{ object.name }}</a></td>
<td>{{ object.db.desc }}</td>
<td>{{ object.subscriptions.all|length }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3">No channels were found!</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- end left column -->
<!-- right column -->
<div class="col-lg-3 col-sm-12">
{% if request.user.is_staff %}
<!-- admin button -->
<a class="btn btn-info btn-block mb-3" href="/admin/comms/channeldb/">Admin</a>
<!-- end admin button -->
<hr />
{% endif %}
{% if most_popular %}
<div class="card mb-3">
<div class="card-header">Most Popular</div>
<ul class="list-group list-group-flush">
{% for top in most_popular %}
<a href="{{ top.web_get_detail_url }}" class="list-group-item">{{ top }} <span class="badge badge-light float-right">{{ top.subscriptions.all|length }}</span></a>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<!-- end right column -->
</div>
<hr />
</div>
</div>
</div>
</div>
{% endblock %}

View 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 %}

View file

@ -0,0 +1,27 @@
{% 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 />
<ul>
{% for object in object_list %}
<li><a href="{{ object.web_get_detail_url }}">{{ object }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -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 %}

View file

@ -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.

View 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 %}

View file

@ -0,0 +1,86 @@
{% extends "base.html" %}
{% block titleblock %}
{{ view.page_title }} ({{ object|title }})
{% endblock %}
{% block content %}
{% load addclass %}
<div class="row">
<div class="col">
<!-- main content -->
<div class="card mb-3">
<div class="card-body">
<h1 class="card-title">{{ view.page_title }} ({{ object|title }})</h1>
<hr />
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'help' %}">Compendium</a></li>
<li class="breadcrumb-item"><a href="{% url 'help' %}#{{ object.db_help_category }}">{{ object.db_help_category|title }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ object.db_key|title }}</li>
</ol>
<hr />
<div class="row">
<!-- left column -->
<div class="col-lg-9 col-sm-12">
<p>{{ entry_text }}</p>
{% if topic_previous or topic_next %}
<hr />
<!-- navigation -->
<nav aria-label="Topic Navigation">
<ul class="pagination justify-content-center">
{% if topic_previous %}
<li class="page-item">
<a class="page-link" href="{{ topic_previous.web_get_detail_url }}">Previous ({{ topic_previous|title }})</a>
</li>
{% endif %}
{% if topic_next %}
<li class="page-item">
<a class="page-link" href="{{ topic_next.web_get_detail_url }}">Next ({{ topic_next|title }})</a>
</li>
{% endif %}
</ul>
</nav>
<!-- end navigation -->
{% endif %}
</div>
<!-- end left column -->
<!-- right column (sidebar) -->
<div class="col-lg-3 col-sm-12">
{% if request.user.is_staff %}
<!-- admin button -->
<a class="btn btn-info btn-block mb-3" href="{{ object.web_get_admin_url }}">Admin</a>
<!-- end admin button -->
<hr />
{% endif %}
<div class="card mb-3">
<div class="card-header">{{ object.db_help_category|title }}</div>
<ul class="list-group list-group-flush">
{% for topic in topic_list %}
<a href="{{ topic.web_get_detail_url }}" class="list-group-item {% if topic == object %}active disabled{% endif %}">{{ topic|title }}</a>
{% endfor %}
</ul>
</div>
</div>
<!-- end right column -->
</div>
<hr />
</div>
</div>
<!-- end main content -->
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,109 @@
{% 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 />
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'help' %}">Compendium</a></li>
</ol>
<hr />
<div class="row">
{% regroup object_list by help_category as category_list %}
{% if category_list %}
<!-- left column -->
<div class="col-lg-9 col-sm-12">
<!-- intro -->
<div class="card border-light">
<div class="card-body">
<p>This section of the site is a guide to understanding the mechanics behind {{ game_name }}.</p>
<p>It is organized first by category, then by topic. The box to the right will let you skip to particular categories.</p>
</div>
</div>
<hr />
<!-- end intro -->
<!-- index list -->
<div class="mx-3">
{% for help_category in category_list %}
<h5><a id="{{ help_category.grouper }}"></a>{{ help_category.grouper|title }}</h5>
<ul>
{% for object in help_category.list %}
<li><a href="{{ object.web_get_detail_url }}">{{ object|title }}</a></li>
{% endfor %}
</ul>
{% endfor %}
<!-- end index list -->
</div>
</div>
<!-- end left column -->
<!-- right column (index) -->
<div class="col-lg-3 col-sm-12">
{% if user.is_staff %}
<!-- admin button -->
<a class="btn btn-info btn-block mb-3" href="/admin/help/helpentry/">Admin</a>
<!-- end admin button -->
<hr />
{% endif %}
<div class="card mb-3">
<div class="card-header">Category Index</div>
<ul class="list-group list-group-flush">
{% for category in category_list %}
<a href="#{{ category.grouper }}" class="list-group-item">{{ category.grouper|title }}</a>
{% endfor %}
</ul>
</div>
</div>
<!-- end right column -->
{% else %}
{% if user.is_staff %}
<div class="col-lg-12 col-sm-12">
<div class="alert alert-warning" role="alert">
<h4 class="alert-heading">Hey, staff member {{ user.get_username }}!</h4>
<hr />
<p><strong>Your Help section is currently blank!</strong></p>
<p>You're missing out on an opportunity to attract visitors (and potentially new players) to {{ game_name }}!</p>
<p>Use Evennia's <a href="https://github.com/evennia/evennia/wiki/Help-System#database-help-entries" class="alert-link" target="_blank">Help System</a> to tell the world about the universe you've created, its lore and legends, its people and creatures, and their customs and conflicts!</p>
<p>You don't even need coding skills-- writing Help Entries is no more complicated than writing an email or blog post. Once you publish your first entry, these ugly boxes go away and this page will turn into an index of everything you've written about {{ game_name }}.</p>
<p>The documentation you write is eventually picked up by search engines, so the more you write about how {{ game_name }} works, the larger your web presence will be-- and the more traffic you'll attract.
<p>Everything you write can be viewed either on this site or within the game itself, using the in-game help commands.</p>
<hr>
<p class="mb-0"><a href="/admin/help/helpentry/add/" class="alert-link">Click here</a> to start writing about {{ game_name }}!</p>
</div>
</div>
{% endif %}
<div class="col-lg-12 col-sm-12">
<div class="alert alert-secondary" role="alert">
<h4 class="alert-heading">Under Construction!</h4>
<p>Thanks for your interest, but we're still working on developing and documenting the {{ game_name }} universe!</p>
<p>Check back later for more information as we publish it.</p>
<hr>
<p class="mb-0"><a href="{% url 'index' %}" class="alert-link">Click here</a> to go back to the main page.</p>
</div>
</div>
{% endif %}
</div>
<hr />
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -52,7 +52,7 @@
<div class="row">
<div class="col-12 col-md-4 mb-3">
<div class="card">
<h4 class="card-header">Accounts</h4>
<h4 class="card-header text-center">Accounts</h4>
<div class="card-body">
<p>

View file

@ -0,0 +1,9 @@
{% if messages %}
<!-- messages -->
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
<!-- end messages -->
{% endif %}

View file

@ -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 %}

View file

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block titleblock %}
{{ view.page_title }} ({{ object }})
{% 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 />
<div class="row">
<!-- left/avatar column -->
<div class="col-lg-3 col-sm-12">
<img class="d-flex mr-3" src="http://placehold.jp/250x250.png" alt="Image of {{ object }}">
</div>
<!-- end left/avatar column -->
<!-- right/content column -->
<div class="col-lg-9 col-sm-12">
<dl>
{% for attribute, value in attribute_list.items %}
<dt>{{ attribute }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
</div>
<!-- end right/content column -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,27 @@
{% 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 />
<ul>
{% for object in object_list %}
<li><a href="{{ object.web_get_detail_url }}">{{ object }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View 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">&laquo;</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">&raquo;</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
{% endif %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -0,0 +1,38 @@
{% 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>
<hr />
<p><a href="{% url 'index' %}">Click here</a> to return to the main page.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -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 %}

View file

@ -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="Submit" />
<input type="hidden" name="next" value="{{ next }}" />
</div>
</form>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -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 %}

View file

@ -0,0 +1,281 @@
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 import class_from_module
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
channel_typeclass = settings.BASE_CHANNEL_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'
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'
@override_settings(WEBCLIENT_ENABLED=True)
def test_get(self):
self.authenticated_response = 200
self.unauthenticated_response = 200
super(WebclientTest, self).test_get()
@override_settings(WEBCLIENT_ENABLED=False)
def test_get_disabled(self):
self.authenticated_response = 404
self.unauthenticated_response = 404
super(WebclientTest, self).test_get()
class ChannelListTest(EvenniaWebTest):
url_name = 'channels'
class ChannelDetailTest(EvenniaWebTest):
url_name = 'channel-detail'
def setUp(self):
super(ChannelDetailTest, self).setUp()
klass = class_from_module(self.channel_typeclass)
# Create a channel
klass.create('demo')
def get_kwargs(self):
return {
'slug': slugify('demo')
}
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 CharacterListView(EvenniaWebTest):
url_name = 'characters'
unauthenticated_response = 302
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)

View file

@ -9,12 +9,30 @@ 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/register', website_views.AccountCreateView.as_view(), name="register"),
url(r'^auth/', include('django.contrib.auth.urls')),
# Help Topics
url(r'^help/$', website_views.HelpListView.as_view(), name="help"),
url(r'^help/(?P<category>[\w\d\-]+)/(?P<topic>[\w\d\-]+)/$', website_views.HelpDetailView.as_view(), name="help-entry-detail"),
# Channels
url(r'^channels/$', website_views.ChannelListView.as_view(), name="channels"),
url(r'^channels/(?P<slug>[\w\d\-]+)/$', website_views.ChannelDetailView.as_view(), name="channel-detail"),
# Character management
url(r'^characters/$', website_views.CharacterListView.as_view(), name="characters"),
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/detail/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$', website_views.CharacterDetailView.as_view(), name="character-detail"),
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"),

File diff suppressed because it is too large Load diff