Move and split views
This commit is contained in:
parent
dac2be3074
commit
273cc31146
25 changed files with 1299 additions and 1196 deletions
|
|
@ -99,6 +99,11 @@ class FileHelpEntry:
|
||||||
"text": self.entrytext,
|
"text": self.entrytext,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.key
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<FileHelpEntry {self.key}>"
|
||||||
|
|
||||||
|
|
||||||
class FileHelpStorageHandler:
|
class FileHelpStorageHandler:
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,8 @@ class HelpEntry(SharedMemoryModel):
|
||||||
# HelpEntry main class methods
|
# HelpEntry main class methods
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.key)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<HelpEntry {self.key}>"
|
return f"<HelpEntry {self.key}>"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"""
|
"""
|
||||||
This sub-package holds the web presence of Evennia, using normal
|
This sub-package holds the web presence of Evennia, using normal Django to
|
||||||
Django to relate the database contents to web pages. Also the basic
|
relate the database contents to web pages.
|
||||||
webclient and the website are defined in here (the webserver itself is
|
|
||||||
found under the `server` package).
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@ folder and edit it to add/remove links to the menu.
|
||||||
</li>
|
</li>
|
||||||
<!-- evennia documentation -->
|
<!-- evennia documentation -->
|
||||||
<li>
|
<li>
|
||||||
<a class="nav-link" href="https://github.com/evennia/evennia/wiki/Evennia-Introduction/">About</a>
|
<a class="nav-link" href="https://www.evennia.com/docs/latest/Evennia-Introduction.html">About</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a class="nav-link" href="https://github.com/evennia/evennia/wiki">Documentation</a></li>
|
<li><a class="nav-link" href="https://www.evennia.com/docs/latest">Documentation</a></li>
|
||||||
<!-- end evennia documentation -->
|
<!-- end evennia documentation -->
|
||||||
|
|
||||||
<!-- game views -->
|
<!-- game views -->
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span class="text-white">Powered by <a class="text-white font-weight-bold" href="http://evennia.com">Evennia.</a></span>
|
<span class="text-white">Powered by <a class="text-white font-weight-bold" href="https://evennia.com">Evennia.</a></span>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<h1 class="card-title">{{ view.page_title }} ({{ object|title }})</h1>
|
<h1 class="card-title">{{ view.page_title }} ({{ object|title }})</h1>
|
||||||
<hr />
|
<hr />
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'help' %}">Compendium</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'help' %}">Help Index</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"><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>
|
<li class="breadcrumb-item active" aria-current="page">{{ object.db_key|title }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<h1 class="card-title">{{ view.page_title }}</h1>
|
<h1 class="card-title">{{ view.page_title }}</h1>
|
||||||
<hr />
|
<hr />
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'help' %}">Compendium</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'help' %}">Help Index</a></li>
|
||||||
</ol>
|
</ol>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
Welcome to your new installation of <a href="http://evennia.com">Evennia</a>, your friendly
|
Welcome to your new installation of <a href="https://evennia.com">Evennia</a>, your friendly
|
||||||
neighborhood next-generation MUD development system and server.
|
neighborhood next-generation MUD development system and server.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -32,12 +32,15 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
For more info, take your time to
|
For more info, take your time to
|
||||||
peruse our extensive online <a href="https://github.com/evennia/evennia/wiki">documentation</a>.
|
peruse our extensive online <a href="https://evennia.com/docs/latest">documentation</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Should you have any questions, concerns, bug reports, or
|
Should you have any questions, concerns, bug reports, or
|
||||||
if you want to help out, don't hesitate to join the Evennia community to make your voice heard! Drop a mail to the
|
if you want to help out, don't hesitate to join the Evennia community
|
||||||
<a href="https://groups.google.com/forum/#!forum/evennia">mailing list</a> or to come say hi in the <a href="http://webchat.freenode.net/?channels=evennia">developer chatroom</a>.
|
to make your voice heard! Drop a mail to the <a
|
||||||
|
href="https://github.com/evennia/evennia/discussions">mailing
|
||||||
|
list</a> or to come say hi in the <a
|
||||||
|
href="http://webchat.freenode.net/?channels=evennia">developer chatroom</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you find bugs, please report them to our <a href="https://github.com/evennia/evennia/issues">Issue tracker</a>.
|
If you find bugs, please report them to our <a href="https://github.com/evennia/evennia/issues">Issue tracker</a>.
|
||||||
|
|
@ -100,7 +103,7 @@
|
||||||
<h4 class="card-header text-center">Evennia</h4>
|
<h4 class="card-header text-center">Evennia</h4>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p><a href="http://evennia.com">Evennia</a> is an open-source MUD server built in
|
<p><a href="https://evennia.com">Evennia</a> is an open-source MUD server built in
|
||||||
<a href="http://python.org">Python</a>, on top of the
|
<a href="http://python.org">Python</a>, on top of the
|
||||||
<a href="http://twistedmatrix.com">Twisted</a> and
|
<a href="http://twistedmatrix.com">Twisted</a> and
|
||||||
<a href="http://djangoproject.com">Django</a> frameworks. This
|
<a href="http://djangoproject.com">Django</a> frameworks. This
|
||||||
|
|
|
||||||
10
evennia/web/templatetags/README.md
Normal file
10
evennia/web/templatetags/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Template tags
|
||||||
|
|
||||||
|
Template-tags are python code safely callable from inside the html template.
|
||||||
|
|
||||||
|
In a template they are used as
|
||||||
|
|
||||||
|
{% mytagname args %s}
|
||||||
|
|
||||||
|
See the Django documentation on template tags and a lot of tags already coming
|
||||||
|
with Django. Placing code here allows for custom tags.
|
||||||
|
|
@ -28,7 +28,7 @@ class EvenniaForm(forms.Form):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Call parent function
|
# Call parent function
|
||||||
cleaned = super(EvenniaForm, self).clean()
|
cleaned = super().clean()
|
||||||
|
|
||||||
# Escape all values provided by user
|
# Escape all values provided by user
|
||||||
cleaned = {k: escape(v) for k, v in cleaned.items()}
|
cleaned = {k: escape(v) for k, v in cleaned.items()}
|
||||||
|
|
|
||||||
|
|
@ -6,69 +6,57 @@ from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django import views as django_views
|
from django import views as django_views
|
||||||
from . import views
|
from .views import (index, errors, accounts, help as helpviews, channels,
|
||||||
|
characters, admin as adminviews)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^$", views.EvenniaIndexView.as_view(), name="index"),
|
# website front page
|
||||||
url(r"^tbi/", views.to_be_implemented, name="to_be_implemented"),
|
url(r"^$", index.EvenniaIndexView.as_view(), name="index"),
|
||||||
|
|
||||||
|
# errors
|
||||||
|
url(r"^tbi/", errors.to_be_implemented, name="to_be_implemented"),
|
||||||
|
|
||||||
# User Authentication (makes login/logout url names available)
|
# User Authentication (makes login/logout url names available)
|
||||||
url(r"^auth/register", views.AccountCreateView.as_view(), name="register"),
|
url(r"^auth/register", accounts.AccountCreateView.as_view(), name="register"),
|
||||||
url(r"^auth/", include("django.contrib.auth.urls")),
|
url(r"^auth/", include("django.contrib.auth.urls")),
|
||||||
|
|
||||||
# Help Topics
|
# Help Topics
|
||||||
url(r"^help/$", views.HelpListView.as_view(), name="help"),
|
url(r"^help/$", helpviews.HelpListView.as_view(), name="help"),
|
||||||
url(
|
url(r"^help/(?P<category>[\w\d\-]+)/(?P<topic>[\w\d\-]+)/$",
|
||||||
r"^help/(?P<category>[\w\d\-]+)/(?P<topic>[\w\d\-]+)/$",
|
helpviews.HelpDetailView.as_view(),
|
||||||
views.HelpDetailView.as_view(),
|
name="help-entry-detail"),
|
||||||
name="help-entry-detail",
|
|
||||||
),
|
|
||||||
|
|
||||||
# Channels
|
# Channels
|
||||||
url(r"^channels/$", views.ChannelListView.as_view(), name="channels"),
|
url(r"^channels/$", channels.ChannelListView.as_view(), name="channels"),
|
||||||
url(
|
url(r"^channels/(?P<slug>[\w\d\-]+)/$",
|
||||||
r"^channels/(?P<slug>[\w\d\-]+)/$",
|
channels.ChannelDetailView.as_view(),
|
||||||
views.ChannelDetailView.as_view(),
|
name="channel-detail"),
|
||||||
name="channel-detail",
|
|
||||||
),
|
|
||||||
|
|
||||||
# Character management
|
# Character management
|
||||||
url(r"^characters/$", views.CharacterListView.as_view(), name="characters"),
|
url(r"^characters/$", characters.CharacterListView.as_view(), name="characters"),
|
||||||
url(
|
url(r"^characters/create/$",
|
||||||
r"^characters/create/$",
|
characters.CharacterCreateView.as_view(),
|
||||||
views.CharacterCreateView.as_view(),
|
name="character-create"),
|
||||||
name="character-create",
|
url(r"^characters/manage/$",
|
||||||
),
|
characters.CharacterManageView.as_view(),
|
||||||
url(
|
name="character-manage"),
|
||||||
r"^characters/manage/$",
|
url(r"^characters/detail/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$",
|
||||||
views.CharacterManageView.as_view(),
|
characters.CharacterDetailView.as_view(),
|
||||||
name="character-manage",
|
name="character-detail"),
|
||||||
),
|
url(r"^characters/puppet/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$",
|
||||||
url(
|
characters.CharacterPuppetView.as_view(),
|
||||||
r"^characters/detail/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$",
|
name="character-puppet"),
|
||||||
views.CharacterDetailView.as_view(),
|
url(r"^characters/update/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$",
|
||||||
name="character-detail",
|
characters.CharacterUpdateView.as_view(),
|
||||||
),
|
name="character-update"),
|
||||||
url(
|
url(r"^characters/delete/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$",
|
||||||
r"^characters/puppet/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$",
|
characters.CharacterDeleteView.as_view(),
|
||||||
views.CharacterPuppetView.as_view(),
|
name="character-delete"),
|
||||||
name="character-puppet",
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r"^characters/update/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$",
|
|
||||||
views.CharacterUpdateView.as_view(),
|
|
||||||
name="character-update",
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r"^characters/delete/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$",
|
|
||||||
views.CharacterDeleteView.as_view(),
|
|
||||||
name="character-delete",
|
|
||||||
),
|
|
||||||
|
|
||||||
# Django original admin page. Make this URL is always available, whether
|
# Django original admin page. Make this URL is always available, whether
|
||||||
# we've chosen to use Evennia's custom admin or not.
|
# we've chosen to use Evennia's custom admin or not.
|
||||||
|
|
||||||
url(r"django_admin/", views.admin_wrapper, name="django_admin"),
|
url(r"django_admin/", adminviews.admin_wrapper, name="django_admin"),
|
||||||
|
|
||||||
# Admin docs
|
# Admin docs
|
||||||
url(r"^admin/doc/", include("django.contrib.admindocs.urls")),
|
url(r"^admin/doc/", include("django.contrib.admindocs.urls")),
|
||||||
|
|
@ -77,7 +65,7 @@ urlpatterns = [
|
||||||
if settings.EVENNIA_ADMIN:
|
if settings.EVENNIA_ADMIN:
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
# Our override for the admin.
|
# Our override for the admin.
|
||||||
url("^admin/$", views.evennia_admin, name="evennia_admin"),
|
url("^admin/$", adminviews.evennia_admin, name="evennia_admin"),
|
||||||
# Makes sure that other admin pages get loaded.
|
# Makes sure that other admin pages get loaded.
|
||||||
url(r"^admin/", admin.site.urls),
|
url(r"^admin/", admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
4
evennia/web/website/views/__init__.py
Normal file
4
evennia/web/website/views/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""
|
||||||
|
Website views.
|
||||||
|
|
||||||
|
"""
|
||||||
76
evennia/web/website/views/accounts.py
Normal file
76
evennia/web/website/views/accounts.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"""
|
||||||
|
Views for managing accounts.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from evennia.utils import class_from_module
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
|
from .mixins import EvenniaCreateView, TypeclassMixin
|
||||||
|
from evennia.web.website import forms
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMixin(TypeclassMixin):
|
||||||
|
"""
|
||||||
|
This is used to grant abilities to classes it is added to.
|
||||||
|
|
||||||
|
Any view class with this in its inheritance list will be modified to work
|
||||||
|
with Account objects instead of generic Objects or otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
model = class_from_module(settings.BASE_ACCOUNT_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_ACCOUNT_TYPECLASS)
|
||||||
|
form_class = forms.AccountForm
|
||||||
|
|
||||||
|
|
||||||
|
class AccountCreateView(AccountMixin, EvenniaCreateView):
|
||||||
|
"""
|
||||||
|
Account creation view.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
template_name = "website/registration/register.html"
|
||||||
|
success_url = reverse_lazy("login")
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""
|
||||||
|
Django hook, modified for Evennia.
|
||||||
|
|
||||||
|
This hook is called after a valid form is submitted.
|
||||||
|
|
||||||
|
When an account creation form is submitted and the data is deemed valid,
|
||||||
|
proceeds with creating the Account object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get values provided
|
||||||
|
username = form.cleaned_data["username"]
|
||||||
|
password = form.cleaned_data["password1"]
|
||||||
|
email = form.cleaned_data.get("email", "")
|
||||||
|
|
||||||
|
# Create account
|
||||||
|
account, errs = self.typeclass.create(username=username, password=password, email=email)
|
||||||
|
|
||||||
|
# If unsuccessful, display error messages to user
|
||||||
|
if not account:
|
||||||
|
[messages.error(self.request, err) for err in errs]
|
||||||
|
|
||||||
|
# Call the Django "form failure" hook
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
# Inform user of success
|
||||||
|
messages.success(
|
||||||
|
self.request,
|
||||||
|
"Your account '%s' was successfully created! "
|
||||||
|
"You may log in using it now." % account.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Redirect the user to the login page
|
||||||
|
return HttpResponseRedirect(self.success_url)
|
||||||
|
|
||||||
24
evennia/web/website/views/admin.py
Normal file
24
evennia/web/website/views/admin.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""
|
||||||
|
Admin views.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.admin.sites import site
|
||||||
|
from evennia.accounts.models import AccountDB
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def evennia_admin(request):
|
||||||
|
"""
|
||||||
|
Helpful Evennia-specific admin page.
|
||||||
|
"""
|
||||||
|
return render(request, "evennia_admin.html", {"accountdb": AccountDB})
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
175
evennia/web/website/views/channels.py
Normal file
175
evennia/web/website/views/channels.py
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
"""
|
||||||
|
Views for managing channels.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.views.generic import ListView
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from django.db.models.functions import Lower
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
|
|
||||||
|
from evennia.utils.logger import tail_log_file
|
||||||
|
from evennia.utils import class_from_module
|
||||||
|
from .mixins import TypeclassMixin
|
||||||
|
from .objects import ObjectDetailView
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelMixin(TypeclassMixin):
|
||||||
|
"""
|
||||||
|
This is a "mixin", a modifier of sorts.
|
||||||
|
|
||||||
|
Any view class with this in its inheritance list will be modified to work
|
||||||
|
with HelpEntry objects instead of generic Objects or otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
model = class_from_module(settings.BASE_CHANNEL_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_CHANNEL_TYPECLASS)
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
page_title = "Channels"
|
||||||
|
|
||||||
|
# What lock type to check for the requesting user, authenticated or not.
|
||||||
|
# https://github.com/evennia/evennia/wiki/Locks#valid-access_types
|
||||||
|
access_type = "listen"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
Django hook; here we want to return a list of only those Channels
|
||||||
|
and other documentation that the current user is allowed to see.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
queryset (QuerySet): List of Channels available to the user.
|
||||||
|
|
||||||
|
"""
|
||||||
|
account = self.request.user
|
||||||
|
|
||||||
|
# Get list of all Channels
|
||||||
|
channels = self.typeclass.objects.all().iterator()
|
||||||
|
|
||||||
|
# Now figure out which ones the current user is allowed to see
|
||||||
|
bucket = [channel.id for channel in channels if channel.access(account, "listen")]
|
||||||
|
|
||||||
|
# Re-query and set a sorted list
|
||||||
|
filtered = self.typeclass.objects.filter(id__in=bucket).order_by(Lower("db_key"))
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelListView(ChannelMixin, ListView):
|
||||||
|
"""
|
||||||
|
Returns a list of channels that can be viewed by a user, authenticated
|
||||||
|
or not.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
paginate_by = 100
|
||||||
|
template_name = "website/channel_list.html"
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
page_title = "Channel Index"
|
||||||
|
|
||||||
|
max_popular = 10
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Django hook; we override it to calculate the most popular channels.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
context (dict): Django context object
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# Calculate which channels are most popular
|
||||||
|
context["most_popular"] = sorted(
|
||||||
|
list(self.get_queryset()),
|
||||||
|
key=lambda channel: len(channel.subscriptions.all()),
|
||||||
|
reverse=True,
|
||||||
|
)[: self.max_popular]
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelDetailView(ChannelMixin, ObjectDetailView):
|
||||||
|
"""
|
||||||
|
Returns the log entries for a given channel.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
template_name = "website/channel_detail.html"
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
# What attributes of the object you wish to display on the page. Model-level
|
||||||
|
# attributes will take precedence over identically-named db.attributes!
|
||||||
|
# The order you specify here will be followed.
|
||||||
|
attributes = ["name"]
|
||||||
|
|
||||||
|
# How many log entries to read and display.
|
||||||
|
max_num_lines = 10000
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Django hook; before we can display the channel logs, we need to recall
|
||||||
|
the logfile and read its lines.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
context (dict): Django context object
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get the parent context object, necessary first step
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
channel = self.object
|
||||||
|
|
||||||
|
# Get the filename this Channel is recording to
|
||||||
|
filename = channel.get_log_filename()
|
||||||
|
|
||||||
|
# Split log entries so we can filter by time
|
||||||
|
bucket = []
|
||||||
|
for log in (x.strip() for x in tail_log_file(filename, 0, self.max_num_lines)):
|
||||||
|
if not log:
|
||||||
|
continue
|
||||||
|
time, msg = log.split(" [-] ")
|
||||||
|
time_key = time.split(":")[0]
|
||||||
|
|
||||||
|
bucket.append({"key": time_key, "timestamp": time, "message": msg})
|
||||||
|
|
||||||
|
# Add the processed entries to the context
|
||||||
|
context["object_list"] = bucket
|
||||||
|
|
||||||
|
# Get a list of unique timestamps by hour and sort them
|
||||||
|
context["object_filters"] = sorted(set([x["key"] for x in bucket]))
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
"""
|
||||||
|
Override of Django hook that retrieves an object by slugified channel
|
||||||
|
name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
channel (Channel): Channel requested in the URL.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get the queryset for the help entries the user can access
|
||||||
|
if not queryset:
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
|
# Find the object in the queryset
|
||||||
|
channel = slugify(self.kwargs.get("slug", ""))
|
||||||
|
obj = next((x for x in queryset if slugify(x.db_key) == channel), None)
|
||||||
|
|
||||||
|
# Check if this object was requested in a valid manner
|
||||||
|
if not obj:
|
||||||
|
raise HttpResponseBadRequest(
|
||||||
|
"No %(verbose_name)s found matching the query"
|
||||||
|
% {"verbose_name": queryset.model._meta.verbose_name}
|
||||||
|
)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
254
evennia/web/website/views/characters.py
Normal file
254
evennia/web/website/views/characters.py
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
"""
|
||||||
|
Views for manipulating Characters (children of Objects often used for
|
||||||
|
puppeting).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.db.models.functions import Lower
|
||||||
|
from django.views.generic.base import RedirectView
|
||||||
|
from django.views.generic import ListView
|
||||||
|
from evennia.utils import class_from_module
|
||||||
|
from .mixins import TypeclassMixin
|
||||||
|
from .objects import ObjectDetailView, ObjectDeleteView, ObjectUpdateView, ObjectCreateView
|
||||||
|
from evennia.web.website import forms
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterMixin(TypeclassMixin):
|
||||||
|
"""
|
||||||
|
This is a "mixin", a modifier of sorts.
|
||||||
|
|
||||||
|
Any view class with this in its inheritance list will be modified to work
|
||||||
|
with Character objects instead of generic Objects or otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
model = class_from_module(settings.BASE_CHARACTER_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_CHARACTER_TYPECLASS)
|
||||||
|
form_class = forms.CharacterForm
|
||||||
|
success_url = reverse_lazy("character-manage")
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
This method will override the Django get_queryset method to only
|
||||||
|
return a list of characters associated with the current authenticated
|
||||||
|
user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
queryset (QuerySet): Django queryset for use in the given view.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get IDs of characters owned by account
|
||||||
|
account = self.request.user
|
||||||
|
ids = [getattr(x, "id") for x in account.characters if x]
|
||||||
|
|
||||||
|
# Return a queryset consisting of those characters
|
||||||
|
return self.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterListView(LoginRequiredMixin, CharacterMixin, ListView):
|
||||||
|
"""
|
||||||
|
This view provides a mechanism by which a logged-in player can view a list
|
||||||
|
of all other characters.
|
||||||
|
|
||||||
|
This view requires authentication by default as a nominal effort to prevent
|
||||||
|
human stalkers and automated bots/scrapers from harvesting data on your users.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
template_name = "website/character_list.html"
|
||||||
|
paginate_by = 100
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
page_title = "Character List"
|
||||||
|
access_type = "view"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
This method will override the Django get_queryset method to return a
|
||||||
|
list of all characters (filtered/sorted) instead of just those limited
|
||||||
|
to the account.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
queryset (QuerySet): Django queryset for use in the given view.
|
||||||
|
|
||||||
|
"""
|
||||||
|
account = self.request.user
|
||||||
|
|
||||||
|
# Return a queryset consisting of characters the user is allowed to
|
||||||
|
# see.
|
||||||
|
ids = [
|
||||||
|
obj.id for obj in self.typeclass.objects.all() if obj.access(account, self.access_type)
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, ObjectDetailView):
|
||||||
|
"""
|
||||||
|
This view provides a mechanism by which a logged-in player can "puppet" one
|
||||||
|
of their characters within the context of the website.
|
||||||
|
|
||||||
|
It also ensures that any user attempting to puppet something is logged in,
|
||||||
|
and that their intended puppet is one that they own.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Django hook.
|
||||||
|
|
||||||
|
This view returns the URL to which the user should be redirected after
|
||||||
|
a passed or failed puppet attempt.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
url (str): Path to post-puppet destination.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get the requested character, if it belongs to the authenticated user
|
||||||
|
char = self.get_object()
|
||||||
|
|
||||||
|
# Get the page the user came from
|
||||||
|
next_page = self.request.GET.get("next", self.success_url)
|
||||||
|
|
||||||
|
if char:
|
||||||
|
# If the account owns the char, store the ID of the char in the
|
||||||
|
# Django request's session (different from Evennia session!).
|
||||||
|
# We do this because characters don't serialize well.
|
||||||
|
self.request.session["puppet"] = int(char.pk)
|
||||||
|
messages.success(self.request, "You become '%s'!" % char)
|
||||||
|
else:
|
||||||
|
# If the puppeting failed, clear out the cached puppet value
|
||||||
|
self.request.session["puppet"] = None
|
||||||
|
messages.error(self.request, "You cannot become '%s'." % char)
|
||||||
|
|
||||||
|
return next_page
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterManageView(LoginRequiredMixin, CharacterMixin, ListView):
|
||||||
|
"""
|
||||||
|
This view provides a mechanism by which a logged-in player can browse,
|
||||||
|
edit, or delete their own characters.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
paginate_by = 10
|
||||||
|
template_name = "website/character_manage_list.html"
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
page_title = "Manage Characters"
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterUpdateView(CharacterMixin, ObjectUpdateView):
|
||||||
|
"""
|
||||||
|
This view provides a mechanism by which a logged-in player (enforced by
|
||||||
|
ObjectUpdateView) can edit the attributes of a character they own.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
form_class = forms.CharacterUpdateForm
|
||||||
|
template_name = "website/character_form.html"
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterDetailView(CharacterMixin, ObjectDetailView):
|
||||||
|
"""
|
||||||
|
This view provides a mechanism by which a user can view the attributes of
|
||||||
|
a character, owned by them or not.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
template_name = "website/object_detail.html"
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
# What attributes to display for this object
|
||||||
|
attributes = ["name", "desc"]
|
||||||
|
access_type = "view"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
This method will override the Django get_queryset method to return a
|
||||||
|
list of all characters the user may access.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
queryset (QuerySet): Django queryset for use in the given view.
|
||||||
|
|
||||||
|
"""
|
||||||
|
account = self.request.user
|
||||||
|
|
||||||
|
# Return a queryset consisting of characters the user is allowed to
|
||||||
|
# see.
|
||||||
|
ids = [
|
||||||
|
obj.id for obj in self.typeclass.objects.all() if obj.access(account, self.access_type)
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterDeleteView(CharacterMixin, ObjectDeleteView):
|
||||||
|
"""
|
||||||
|
This view provides a mechanism by which a logged-in player (enforced by
|
||||||
|
ObjectDeleteView) can delete a character they own.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterCreateView(CharacterMixin, ObjectCreateView):
|
||||||
|
"""
|
||||||
|
This view provides a mechanism by which a logged-in player (enforced by
|
||||||
|
ObjectCreateView) can create a new character.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
template_name = "website/character_form.html"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""
|
||||||
|
Django hook, modified for Evennia.
|
||||||
|
|
||||||
|
This hook is called after a valid form is submitted.
|
||||||
|
|
||||||
|
When an character creation form is submitted and the data is deemed valid,
|
||||||
|
proceeds with creating the Character object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get account object creating the character
|
||||||
|
account = self.request.user
|
||||||
|
character = None
|
||||||
|
|
||||||
|
# Get attributes from the form
|
||||||
|
self.attributes = {k: form.cleaned_data[k] for k in form.cleaned_data.keys()}
|
||||||
|
charname = self.attributes.pop("db_key")
|
||||||
|
description = self.attributes.pop("desc")
|
||||||
|
# Create a character
|
||||||
|
character, errors = self.typeclass.create(charname, account, description=description)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
# Echo error messages to the user
|
||||||
|
[messages.error(self.request, x) for x in errors]
|
||||||
|
|
||||||
|
if character:
|
||||||
|
# Assign attributes from form
|
||||||
|
for key, value in self.attributes.items():
|
||||||
|
setattr(character.db, key, value)
|
||||||
|
|
||||||
|
# Return the user to the character management page, unless overridden
|
||||||
|
messages.success(self.request, "Your character '%s' was created!" % character.name)
|
||||||
|
return HttpResponseRedirect(self.success_url)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Call the Django "form failed" hook
|
||||||
|
messages.error(self.request, "Your character could not be created.")
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
16
evennia/web/website/views/errors.py
Normal file
16
evennia/web/website/views/errors.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
Error views.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
def to_be_implemented(request):
|
||||||
|
"""
|
||||||
|
A notice letting the user know that this particular feature hasn't been
|
||||||
|
implemented yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pagevars = {"page_title": "To Be Implemented..."}
|
||||||
|
|
||||||
|
return render(request, "tbi.html", pagevars)
|
||||||
162
evennia/web/website/views/help.py
Normal file
162
evennia/web/website/views/help.py
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
"""
|
||||||
|
Views to manipulate help entries.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from django.views.generic import ListView
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
|
from django.db.models.functions import Lower
|
||||||
|
from evennia.help.models import HelpEntry
|
||||||
|
from .mixins import TypeclassMixin, EvenniaDetailView
|
||||||
|
|
||||||
|
|
||||||
|
class HelpMixin(TypeclassMixin):
|
||||||
|
"""
|
||||||
|
This is a "mixin", a modifier of sorts.
|
||||||
|
|
||||||
|
Any view class with this in its inheritance list will be modified to work
|
||||||
|
with HelpEntry objects instead of generic Objects or otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
model = HelpEntry
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
page_title = "Help"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
Django hook; here we want to return a list of only those HelpEntries
|
||||||
|
and other documentation that the current user is allowed to see.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
queryset (QuerySet): List of Help entries available to the user.
|
||||||
|
|
||||||
|
"""
|
||||||
|
account = self.request.user
|
||||||
|
|
||||||
|
# Get list of all HelpEntries
|
||||||
|
entries = self.typeclass.objects.all().iterator()
|
||||||
|
|
||||||
|
# Now figure out which ones the current user is allowed to see
|
||||||
|
bucket = [entry.id for entry in entries if entry.access(account, "view")]
|
||||||
|
|
||||||
|
# Re-query and set a sorted list
|
||||||
|
filtered = (
|
||||||
|
self.typeclass.objects.filter(id__in=bucket)
|
||||||
|
.order_by(Lower("db_key"))
|
||||||
|
.order_by(Lower("db_help_category"))
|
||||||
|
)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
class HelpListView(HelpMixin, ListView):
|
||||||
|
"""
|
||||||
|
Returns a list of help entries that can be viewed by a user, authenticated
|
||||||
|
or not.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
paginate_by = 500
|
||||||
|
template_name = "website/help_list.html"
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
page_title = "Help Index"
|
||||||
|
|
||||||
|
|
||||||
|
class HelpDetailView(HelpMixin, EvenniaDetailView):
|
||||||
|
"""
|
||||||
|
Returns the detail page for a given help entry.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
template_name = "website/help_detail.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Adds navigational data to the template to let browsers go to the next
|
||||||
|
or previous entry in the help list.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
context (dict): Django context object
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# Get the object in question
|
||||||
|
obj = self.get_object()
|
||||||
|
|
||||||
|
# Get queryset and filter out non-related categories
|
||||||
|
queryset = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(db_help_category=obj.db_help_category)
|
||||||
|
.order_by(Lower("db_key"))
|
||||||
|
)
|
||||||
|
context["topic_list"] = queryset
|
||||||
|
|
||||||
|
# Find the index position of the given obj in the queryset
|
||||||
|
objs = list(queryset)
|
||||||
|
for i, x in enumerate(objs):
|
||||||
|
if obj is x:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Find the previous and next topics, if either exist
|
||||||
|
try:
|
||||||
|
assert i + 1 <= len(objs) and objs[i + 1] is not obj
|
||||||
|
context["topic_next"] = objs[i + 1]
|
||||||
|
except:
|
||||||
|
context["topic_next"] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert i - 1 >= 0 and objs[i - 1] is not obj
|
||||||
|
context["topic_previous"] = objs[i - 1]
|
||||||
|
except:
|
||||||
|
context["topic_previous"] = None
|
||||||
|
|
||||||
|
# Format the help entry using HTML instead of newlines
|
||||||
|
text = obj.db_entrytext
|
||||||
|
text = text.replace("\r\n\r\n", "\n\n")
|
||||||
|
text = text.replace("\r\n", "\n")
|
||||||
|
text = text.replace("\n", "<br />")
|
||||||
|
context["entry_text"] = text
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
"""
|
||||||
|
Override of Django hook that retrieves an object by category and topic
|
||||||
|
instead of pk and slug.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
entry (HelpEntry): HelpEntry requested in the URL.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get the queryset for the help entries the user can access
|
||||||
|
if not queryset:
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
|
# Find the object in the queryset
|
||||||
|
category = slugify(self.kwargs.get("category", ""))
|
||||||
|
topic = slugify(self.kwargs.get("topic", ""))
|
||||||
|
obj = next(
|
||||||
|
(
|
||||||
|
x
|
||||||
|
for x in queryset
|
||||||
|
if slugify(x.db_help_category) == category and slugify(x.db_key) == topic
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if this object was requested in a valid manner
|
||||||
|
if not obj:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"No %(verbose_name)s found matching the query"
|
||||||
|
% {"verbose_name": queryset.model._meta.verbose_name}
|
||||||
|
)
|
||||||
|
|
||||||
|
return obj
|
||||||
116
evennia/web/website/views/index.py
Normal file
116
evennia/web/website/views/index.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""
|
||||||
|
The main index page, including the game stats
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from evennia import SESSION_HANDLER
|
||||||
|
from evennia.objects.models import ObjectDB
|
||||||
|
from evennia.accounts.models import AccountDB
|
||||||
|
from evennia.utils import class_from_module
|
||||||
|
|
||||||
|
|
||||||
|
def _gamestats():
|
||||||
|
"""
|
||||||
|
Generate a the gamestat context for the main index page
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Some misc. configurable stuff.
|
||||||
|
# TODO: Move this to either SQL or settings.py based configuration.
|
||||||
|
fpage_account_limit = 4
|
||||||
|
|
||||||
|
# A QuerySet of the most recently connected accounts.
|
||||||
|
recent_users = AccountDB.objects.get_recently_connected_accounts()[:fpage_account_limit]
|
||||||
|
nplyrs_conn_recent = len(recent_users) or "none"
|
||||||
|
nplyrs = AccountDB.objects.num_total_accounts() or "none"
|
||||||
|
nplyrs_reg_recent = len(AccountDB.objects.get_recently_created_accounts()) or "none"
|
||||||
|
nsess = SESSION_HANDLER.account_count()
|
||||||
|
# nsess = len(AccountDB.objects.get_connected_accounts()) or "no one"
|
||||||
|
|
||||||
|
nobjs = ObjectDB.objects.count()
|
||||||
|
nobjs = nobjs or 1 # fix zero-div error with empty database
|
||||||
|
Character = class_from_module(settings.BASE_CHARACTER_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_CHARACTER_TYPECLASS)
|
||||||
|
nchars = Character.objects.all_family().count()
|
||||||
|
Room = class_from_module(settings.BASE_ROOM_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_ROOM_TYPECLASS)
|
||||||
|
nrooms = Room.objects.all_family().count()
|
||||||
|
Exit = class_from_module(settings.BASE_EXIT_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_EXIT_TYPECLASS)
|
||||||
|
nexits = Exit.objects.all_family().count()
|
||||||
|
nothers = nobjs - nchars - nrooms - nexits
|
||||||
|
|
||||||
|
pagevars = {
|
||||||
|
"page_title": "Front Page",
|
||||||
|
"accounts_connected_recent": recent_users,
|
||||||
|
"num_accounts_connected": nsess or "no one",
|
||||||
|
"num_accounts_registered": nplyrs or "no",
|
||||||
|
"num_accounts_connected_recent": nplyrs_conn_recent or "no",
|
||||||
|
"num_accounts_registered_recent": nplyrs_reg_recent or "no one",
|
||||||
|
"num_rooms": nrooms or "none",
|
||||||
|
"num_exits": nexits or "no",
|
||||||
|
"num_objects": nobjs or "none",
|
||||||
|
"num_characters": nchars or "no",
|
||||||
|
"num_others": nothers or "no",
|
||||||
|
}
|
||||||
|
return pagevars
|
||||||
|
|
||||||
|
|
||||||
|
class EvenniaIndexView(TemplateView):
|
||||||
|
"""
|
||||||
|
This is a basic example of a Django class-based view, which are functionally
|
||||||
|
very similar to Evennia Commands but differ in structure. Commands are used
|
||||||
|
to interface with users using a terminal client. Views are used to interface
|
||||||
|
with users using a web browser.
|
||||||
|
|
||||||
|
To use a class-based view, you need to have written a template in HTML, and
|
||||||
|
then you write a view like this to tell Django what values to display on it.
|
||||||
|
|
||||||
|
While there are simpler ways of writing views using plain functions (and
|
||||||
|
Evennia currently contains a few examples of them), just like Commands,
|
||||||
|
writing views as classes provides you with more flexibility-- you can extend
|
||||||
|
classes and change things to suit your needs rather than having to copy and
|
||||||
|
paste entire code blocks over and over. Django also comes with many default
|
||||||
|
views for displaying things, all of them implemented as classes.
|
||||||
|
|
||||||
|
This particular example displays the index page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Tell the view what HTML template to use for the page
|
||||||
|
template_name = "website/index.html"
|
||||||
|
|
||||||
|
# This method tells the view what data should be displayed on the template.
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
This is a common Django method. Think of this as the website
|
||||||
|
equivalent of the Evennia Command.func() method.
|
||||||
|
|
||||||
|
If you just want to display a static page with no customization, you
|
||||||
|
don't need to define this method-- just create a view, define
|
||||||
|
template_name and you're done.
|
||||||
|
|
||||||
|
The only catch here is that if you extend or overwrite this method,
|
||||||
|
you'll always want to make sure you call the parent method to get a
|
||||||
|
context object. It's just a dict, but it comes prepopulated with all
|
||||||
|
sorts of background data intended for display on the page.
|
||||||
|
|
||||||
|
You can do whatever you want to it, but it must be returned at the end
|
||||||
|
of this method.
|
||||||
|
|
||||||
|
Keyword Args:
|
||||||
|
any (any): Passed through.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
context (dict): Dictionary of data you want to display on the page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Always call the base implementation first to get a context object
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# Add game statistics and other pagevars
|
||||||
|
context.update(_gamestats())
|
||||||
|
|
||||||
|
return context
|
||||||
91
evennia/web/website/views/mixins.py
Normal file
91
evennia/web/website/views/mixins.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""
|
||||||
|
These are mixins for class-based views, granting functionality.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.views.generic import DetailView
|
||||||
|
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
||||||
|
|
||||||
|
|
||||||
|
class TypeclassMixin:
|
||||||
|
"""
|
||||||
|
This is a "mixin", a modifier of sorts.
|
||||||
|
|
||||||
|
Django views typically work with classes called "models." Evennia objects
|
||||||
|
are an enhancement upon these Django models and are called "typeclasses."
|
||||||
|
But Django itself has no idea what a "typeclass" is.
|
||||||
|
|
||||||
|
For the sake of mitigating confusion, any view class with this in its
|
||||||
|
inheritance list will be modified to work with Evennia Typeclass objects or
|
||||||
|
Django models interchangeably.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def typeclass(self):
|
||||||
|
return self.model
|
||||||
|
|
||||||
|
@typeclass.setter
|
||||||
|
def typeclass(self, value):
|
||||||
|
self.model = value
|
||||||
|
|
||||||
|
|
||||||
|
class EvenniaCreateView(CreateView, TypeclassMixin):
|
||||||
|
"""
|
||||||
|
This view extends Django's default CreateView.
|
||||||
|
|
||||||
|
CreateView is used for creating new objects, be they Accounts, Characters or
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page_title(self):
|
||||||
|
# Makes sure the page has a sensible title.
|
||||||
|
return "Create %s" % self.typeclass._meta.verbose_name.title()
|
||||||
|
|
||||||
|
|
||||||
|
class EvenniaDetailView(DetailView, TypeclassMixin):
|
||||||
|
"""
|
||||||
|
This view extends Django's default DetailView.
|
||||||
|
|
||||||
|
DetailView is used for displaying objects, be they Accounts, Characters or
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page_title(self):
|
||||||
|
# Makes sure the page has a sensible title.
|
||||||
|
return "%s Detail" % self.typeclass._meta.verbose_name.title()
|
||||||
|
|
||||||
|
|
||||||
|
class EvenniaUpdateView(UpdateView, TypeclassMixin):
|
||||||
|
"""
|
||||||
|
This view extends Django's default UpdateView.
|
||||||
|
|
||||||
|
UpdateView is used for updating objects, be they Accounts, Characters or
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page_title(self):
|
||||||
|
# Makes sure the page has a sensible title.
|
||||||
|
return "Update %s" % self.typeclass._meta.verbose_name.title()
|
||||||
|
|
||||||
|
|
||||||
|
class EvenniaDeleteView(DeleteView, TypeclassMixin):
|
||||||
|
"""
|
||||||
|
This view extends Django's default DeleteView.
|
||||||
|
|
||||||
|
DeleteView is used for deleting objects, be they Accounts, Characters or
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page_title(self):
|
||||||
|
# Makes sure the page has a sensible title.
|
||||||
|
return "Delete %s" % self.typeclass._meta.verbose_name.title()
|
||||||
|
|
||||||
|
|
||||||
268
evennia/web/website/views/objects.py
Normal file
268
evennia/web/website/views/objects.py
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
"""
|
||||||
|
Views for managing a specific object)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.contrib import messages
|
||||||
|
from evennia.utils import class_from_module
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from .mixins import (
|
||||||
|
EvenniaCreateView, EvenniaDeleteView, EvenniaUpdateView, EvenniaDetailView)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectDetailView(EvenniaDetailView):
|
||||||
|
"""
|
||||||
|
This is an important view.
|
||||||
|
|
||||||
|
Any view you write that deals with displaying, updating or deleting a
|
||||||
|
specific object will want to inherit from this. It provides the mechanisms
|
||||||
|
by which to retrieve the object and make sure the user requesting it has
|
||||||
|
permissions to actually *do* things to it.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
#
|
||||||
|
# Choose what class of object this view will display. Note that this should
|
||||||
|
# be an actual Python class (i.e. do `from typeclasses.characters import
|
||||||
|
# Character`, then put `Character`), not an Evennia typeclass path
|
||||||
|
# (i.e. `typeclasses.characters.Character`).
|
||||||
|
#
|
||||||
|
# So when you extend it, this line should look simple, like:
|
||||||
|
# model = Object
|
||||||
|
model = class_from_module(settings.BASE_OBJECT_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_OBJECT_TYPECLASS)
|
||||||
|
|
||||||
|
# What HTML template you wish to use to display this page.
|
||||||
|
template_name = "website/object_detail.html"
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
#
|
||||||
|
# What lock type to check for the requesting user, authenticated or not.
|
||||||
|
# https://github.com/evennia/evennia/wiki/Locks#valid-access_types
|
||||||
|
access_type = "view"
|
||||||
|
|
||||||
|
# What attributes of the object you wish to display on the page. Model-level
|
||||||
|
# attributes will take precedence over identically-named db.attributes!
|
||||||
|
# The order you specify here will be followed.
|
||||||
|
attributes = ["name", "desc"]
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Adds an 'attributes' list to the request context consisting of the
|
||||||
|
attributes specified at the class level, and in the order provided.
|
||||||
|
|
||||||
|
Django views do not provide a way to reference dynamic attributes, so
|
||||||
|
we have to grab them all before we render the template.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
context (dict): Django context object
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get the base Django context object
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# Get the object in question
|
||||||
|
obj = self.get_object()
|
||||||
|
|
||||||
|
# Create an ordered dictionary to contain the attribute map
|
||||||
|
attribute_list = OrderedDict()
|
||||||
|
|
||||||
|
for attribute in self.attributes:
|
||||||
|
# Check if the attribute is a core fieldname (name, desc)
|
||||||
|
if attribute in self.typeclass._meta._property_names:
|
||||||
|
attribute_list[attribute.title()] = getattr(obj, attribute, "")
|
||||||
|
|
||||||
|
# Check if the attribute is a db attribute (char1.db.favorite_color)
|
||||||
|
else:
|
||||||
|
attribute_list[attribute.title()] = getattr(obj.db, attribute, "")
|
||||||
|
|
||||||
|
# Add our attribute map to the Django request context, so it gets
|
||||||
|
# displayed on the template
|
||||||
|
context["attribute_list"] = attribute_list
|
||||||
|
|
||||||
|
# Return the comprehensive context object
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
"""
|
||||||
|
Override of Django hook that provides some important Evennia-specific
|
||||||
|
functionality.
|
||||||
|
|
||||||
|
Evennia does not natively store slugs, so where a slug is provided,
|
||||||
|
calculate the same for the object and make sure it matches.
|
||||||
|
|
||||||
|
This also checks to make sure the user has access to view/edit/delete
|
||||||
|
this object!
|
||||||
|
|
||||||
|
"""
|
||||||
|
# A queryset can be provided to pre-emptively limit what objects can
|
||||||
|
# possibly be returned. For example, you can supply a queryset that
|
||||||
|
# only returns objects whose name begins with "a".
|
||||||
|
if not queryset:
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
|
# Get the object, ignoring all checks and filters for now
|
||||||
|
obj = self.typeclass.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(
|
||||||
|
"No %(verbose_name)s found matching the query"
|
||||||
|
% {"verbose_name": queryset.model._meta.verbose_name}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the requestor account has permissions to access object
|
||||||
|
account = self.request.user
|
||||||
|
if not obj.access(account, self.access_type):
|
||||||
|
raise PermissionDenied("You are not authorized to %s this object." % self.access_type)
|
||||||
|
|
||||||
|
# Get the object, if it is in the specified queryset
|
||||||
|
obj = super(ObjectDetailView, self).get_object(queryset)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectCreateView(LoginRequiredMixin, EvenniaCreateView):
|
||||||
|
"""
|
||||||
|
This is an important view.
|
||||||
|
|
||||||
|
Any view you write that deals with creating a specific object will want to
|
||||||
|
inherit from this. It provides the mechanisms by which to make sure the user
|
||||||
|
requesting creation of an object is authenticated, and provides a sane
|
||||||
|
default title for the page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = class_from_module(settings.BASE_OBJECT_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_OBJECT_TYPECLASS)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectDeleteView(LoginRequiredMixin, ObjectDetailView, EvenniaDeleteView):
|
||||||
|
"""
|
||||||
|
This is an important view for obvious reasons!
|
||||||
|
|
||||||
|
Any view you write that deals with deleting a specific object will want to
|
||||||
|
inherit from this. It provides the mechanisms by which to make sure the user
|
||||||
|
requesting deletion of an object is authenticated, and that they have
|
||||||
|
permissions to delete the requested object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
model = class_from_module(settings.BASE_OBJECT_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_OBJECT_TYPECLASS)
|
||||||
|
template_name = "website/object_confirm_delete.html"
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
access_type = "delete"
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get the object in question. ObjectDetailView.get_object() will also
|
||||||
|
# check to make sure the current user (authenticated or not) has
|
||||||
|
# permission to delete it!
|
||||||
|
obj = str(self.get_object())
|
||||||
|
|
||||||
|
# Perform the actual deletion (the parent class handles this, which will
|
||||||
|
# in turn call the delete() method on the object)
|
||||||
|
response = super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Notify the user of the deletion
|
||||||
|
messages.success(request, "Successfully deleted '%s'." % obj)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectUpdateView(LoginRequiredMixin, ObjectDetailView, EvenniaUpdateView):
|
||||||
|
"""
|
||||||
|
This is an important view.
|
||||||
|
|
||||||
|
Any view you write that deals with updating a specific object will want to
|
||||||
|
inherit from this. It provides the mechanisms by which to make sure the user
|
||||||
|
requesting editing of an object is authenticated, and that they have
|
||||||
|
permissions to edit the requested object.
|
||||||
|
|
||||||
|
This functions slightly different from default Django UpdateViews in that
|
||||||
|
it does not update core model fields, *only* object attributes!
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Django constructs --
|
||||||
|
model = class_from_module(settings.BASE_OBJECT_TYPECLASS,
|
||||||
|
fallback=settings.FALLBACK_OBJECT_TYPECLASS)
|
||||||
|
|
||||||
|
# -- Evennia constructs --
|
||||||
|
access_type = "edit"
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
"""
|
||||||
|
Django hook.
|
||||||
|
|
||||||
|
Can be overridden to return any URL you want to redirect the user to
|
||||||
|
after the object is successfully updated, but by default it goes to the
|
||||||
|
object detail page so the user can see their changes reflected.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.success_url:
|
||||||
|
return self.success_url
|
||||||
|
return self.object.web_get_detail_url()
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
"""
|
||||||
|
Django hook, modified for Evennia.
|
||||||
|
|
||||||
|
Prepopulates the update form field values based on object db attributes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
data (dict): Dictionary of key:value pairs containing initial form
|
||||||
|
data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# 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 is run when the form is submitted and the data on it is deemed
|
||||||
|
valid-- all values are within expected ranges, all strings contain
|
||||||
|
valid characters and lengths, etc.
|
||||||
|
|
||||||
|
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 attributes 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():
|
||||||
|
self.object.attributes.add(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())
|
||||||
42
evennia/web/website/views/views.py
Normal file
42
evennia/web/website/views/views.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""
|
||||||
|
This file contains the generic, assorted views that don't fall under one of the other applications.
|
||||||
|
Views are django's way of processing e.g. html templates on the fly.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.contrib.admin.sites import site
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
|
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
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views.generic import TemplateView, ListView, DetailView
|
||||||
|
from django.views.generic.base import RedirectView
|
||||||
|
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
||||||
|
|
||||||
|
from evennia import SESSION_HANDLER
|
||||||
|
from evennia.help.models import HelpEntry
|
||||||
|
from evennia.objects.models import ObjectDB
|
||||||
|
from evennia.accounts.models import AccountDB
|
||||||
|
from evennia.utils import class_from_module
|
||||||
|
from evennia.utils.logger import tail_log_file
|
||||||
|
from . import forms
|
||||||
|
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
#
|
||||||
|
# Channel views
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Help views
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue