diff --git a/evennia/web/website/forms.py b/evennia/web/website/forms.py new file mode 100644 index 0000000000..937c6be6e8 --- /dev/null +++ b/evennia/web/website/forms.py @@ -0,0 +1,146 @@ +from django import forms +from django.conf import settings +from django.contrib.auth.forms import UserCreationForm, UsernameField +from evennia.utils import class_from_module +from random import choice, randint + +class AccountForm(UserCreationForm): + + class Meta: + model = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) + fields = ("username", "email") + field_classes = {'username': UsernameField} + + email = forms.EmailField(help_text="A valid email address. Optional; used for password resets.", required=False) + +class CharacterForm(forms.Form): + name = forms.CharField(help_text="The name of your intended character.") + age = forms.IntegerField(min_value=3, max_value=99, help_text="How old your character should be once spawned.") + description = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}), max_length=2048, min_length=160, required=False) + + @classmethod + def assign_attributes(cls, attribute_list, points, min_points, max_points): + """ + Randomly distributes a number of points across the given attributes, + while also ensuring each attribute gets at least a certain amount + and at most a certain amount. + + Args: + attribute_list (iterable): List or tuple of attribute names to assign + points to. + points (int): Starting number of points + min_points (int): Least amount of points each attribute should have + max_points (int): Most amount of points each attribute should have + + Returns: + spread (dict): Dict of attributes and a point assignment. + + """ + num_buckets = len(attribute_list) + point_spread = (x for x in self.random_distribution(points, num_buckets, min_points, max_points)) + + # For each field, get the point calculation for the next attribute value generated + return {attribute: next(point_spread) for k in attribute_list} + + @classmethod + def random_distribution(cls, points, num_buckets, min_points, max_points): + """ + Distributes a set number of points randomly across a number of 'buckets' + while also attempting to ensure each bucket's value finishes within a + certain range. + + If your math doesn't add up (you try to distribute 5 points across 100 + buckets and insist each bucket has at least 20 points), the algorithm + will return the best spread it could achieve but will not raise an error + (so in this case, 5 random buckets would get 1 point each and that's all). + + Args: + points (int): The number of points to distribute. + num_buckets (int): The number of 'buckets' (or stats, skills, etc) + you wish to distribute points to. + min_points (int): The least amount of points each bucket should have. + max_points (int): The most points each bucket should have. + + Returns: + buckets (list): List of random point assignments. + + """ + buckets = [0 for x in range(num_buckets)] + indices = [i for (i, value) in enumerate(buckets)] + + # Do this while we have eligible buckets, points to assign and we haven't + # maxed out all the buckets. + while indices and points and sum(buckets) <= (max_points * num_buckets): + # Pick a random bucket index + index = choice(indices) + + # Add to bucket + buckets[index] = buckets[index] + 1 + points = points - 1 + + # Get the indices of eligible buckets + indices = [i for (i, value) in enumerate(buckets) if (value < min_points) or (value < max_points)] + + return buckets + +class CharacterUpdateForm(CharacterForm): + class Meta: + fields = ('description',) + +class ExtendedCharacterForm(CharacterForm): + + GENDERS = ( + ('male', 'Male'), + ('female', 'Female'), + ('androgynous', 'Androgynous'), + ('special', 'Special') + ) + + RACES = ( + ('human', 'Human'), + ('elf', 'Elf'), + ('orc', 'Orc'), + ) + + CLASSES = ( + ('civilian', 'Civilian'), + ('warrior', 'Warrior'), + ('thief', 'Thief'), + ('cleric', 'Cleric') + ) + + PERKS = ( + ('strong', 'Extra strength'), + ('nimble', 'Quick on their toes'), + ('diplomatic', 'Fast talker') + ) + + name = forms.CharField(help_text="The name of your intended character.") + age = forms.IntegerField(min_value=3, max_value=99, help_text="How old your character should be once spawned.") + gender = forms.ChoiceField(choices=GENDERS, help_text="Which end of the multidimensional spectrum does your character most closely align with, in terms of gender?") + race = forms.ChoiceField(choices=RACES, help_text="What race does your character belong to?") + job = forms.ChoiceField(choices=CLASSES, help_text="What profession or role does your character fulfill or is otherwise destined to?") + + perks = forms.MultipleChoiceField(choices=PERKS, help_text="What extraordinary abilities does your character possess?") + description = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}), max_length=2048, min_length=160, required=False) + + strength = forms.IntegerField(min_value=1, max_value=10) + perception = forms.IntegerField(min_value=1, max_value=10) + intelligence = forms.IntegerField(min_value=1, max_value=10) + dexterity = forms.IntegerField(min_value=1, max_value=10) + charisma = forms.IntegerField(min_value=1, max_value=10) + vitality = forms.IntegerField(min_value=1, max_value=10) + magic = forms.IntegerField(min_value=1, max_value=10) + + def __init__(self, *args, **kwargs): + # Do all the normal initizliation stuff that would otherwise be happening + super(ExtendedCharacterCreationForm, self).__init__(*args, **kwargs) + + # Given a pool of points, let's randomly distribute them across attributes. + # First get a list of attributes + attributes = ('strength', 'perception', 'intelligence', 'dexterity', 'charisma', 'vitality', 'magic') + # Distribute a random number of points across them + attrs = self.assign_attributes(attributes, 50, 1, 10) + # Initialize the form with the results of the point distribution + for field in attrs.keys(): + self.initial[field] = attrs[field] \ No newline at end of file diff --git a/evennia/web/website/urls.py b/evennia/web/website/urls.py index f906b6b142..d6dfc0c748 100644 --- a/evennia/web/website/urls.py +++ b/evennia/web/website/urls.py @@ -13,8 +13,14 @@ urlpatterns = [ url(r'^tbi/', website_views.to_be_implemented, name='to_be_implemented'), # User Authentication (makes login/logout url names available) - url(r'^authenticate/', include('django.contrib.auth.urls')), - + url(r'^auth/', include('django.contrib.auth.urls')), + url(r'^auth/register', website_views.AccountCreationView.as_view(), name="register"), + + # Character management + url(r'^characters/create/$', website_views.CharacterCreateView.as_view(), name="chargen"), + url(r'^characters/manage/$', website_views.CharacterManageView.as_view(), name="manage-characters"), + url(r'^characters/update/(?P[\w\d\-]+)/(?P[0-9]+)/$', website_views.CharacterUpdateView.as_view(), name="update-character"), + # 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"), diff --git a/evennia/web/website/views.py b/evennia/web/website/views.py index fe93b06426..d25c3493ec 100644 --- a/evennia/web/website/views.py +++ b/evennia/web/website/views.py @@ -17,6 +17,7 @@ from evennia.accounts.models import AccountDB from evennia.utils import logger from django.contrib.auth import login +from django.utils.text import slugify _BASE_CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS @@ -134,3 +135,121 @@ def admin_wrapper(request): Wrapper that allows us to properly use the base Django admin site, if needed. """ return staff_member_required(site.index)(request) + +class ObjectDetailView(DetailView): + model = ObjectDB + + def get_object(self, queryset=None): + obj = super(ObjectDetailView, self).get_object(queryset) + if not slugify(obj.name) == self.kwargs.get('slug'): + raise Http404(u"No %(verbose_name)s found matching the query" % + {'verbose_name': queryset.model._meta.verbose_name}) + return obj + +class AccountCreationView(FormView): + form_class = AccountForm + template_name = 'website/registration/register.html' + success_url = reverse_lazy('login') + + def form_valid(self, form): + # Check to make sure basics validated + valid = super(AccountCreationView, self).form_valid(form) + if not valid: return self.form_invalid(form) + + username = form.cleaned_data['username'] + password = form.cleaned_data['password1'] + email = form.cleaned_data.get('email', '') + + # Create a fake session object to intercept calls to the terminal + from mock import Mock + session = self.request + session.address = self.request.META.get('REMOTE_ADDR', '') + session.msg = Mock() + + # Create account + from evennia.commands.default.unloggedin import _create_account + permissions = settings.PERMISSION_ACCOUNT_DEFAULT + account = _create_account(session, username, password, permissions) + + # If unsuccessful, get messages passed to session.msg + if not account: + [messages.error(self.request, call) for call in session.msg.call_args_list] + return self.form_invalid(form) + + # Append email address if given + account.email = email + account.save() + + messages.success(self.request, "Your account '%s' was successfully created! You may log in using it now." % account.name) + return HttpResponseRedirect(self.success_url) + +class CharacterManageView(LoginRequiredMixin, ListView): + model = ObjectDB + + def get_queryset(self): + # Get IDs of characters owned by account + ids = [getattr(x, 'id') for x in self.request.user.db._playable_characters] + + # Return a queryset consisting of those characters + return self.model.filter(id__in=ids) + +class CharacterUpdateView(LoginRequiredMixin, FormView): + form_class = CharacterUpdateForm + template_name = 'website/generic_form.html' + success_url = '/'#reverse_lazy('character-manage') + +class CharacterCreateView(LoginRequiredMixin, FormView): + form_class = CharacterForm + template_name = 'website/chargen_form.html' + success_url = '/'#reverse_lazy('character-manage') + + def form_valid(self, form): + # Get account ref + account = self.request.user + character = None + + # Get attributes from the form + self.attributes = {k: form.cleaned_data[k] for k in form.cleaned_data.keys()} + charname = self.attributes.pop('name') + description = self.attributes.pop('description') + + # Create a character + permissions = settings.PERMISSION_ACCOUNT_DEFAULT + typeclass = settings.BASE_CHARACTER_TYPECLASS + default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) + + from evennia.utils import create + try: + character = create.create_object(typeclass, key=charname, home=default_home, permissions=permissions) + # set playable character list + account.db._playable_characters.append(character) + + # allow only the character itself and the account to puppet this character (and Developers). + character.locks.add("puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)" % + (character.id, account.id)) + + # If no description is set, set a default description + if not description: + character.db.desc = "This is a character." + else: + character.db.desc = description + + # We need to set this to have @ic auto-connect to this character + account.db._last_puppet = character + + # Assign attributes from form + [setattr(character.db, field, self.attributes[field]) for field in self.attributes.keys()] + character.db.creator_id = account.id + character.save() + + except Exception as e: + messages.error(self.request, "There was an error creating your character. If this problem persists, contact an admin.") + logger.log_trace() + return self.form_invalid(form) + + if character: + messages.success(self.request, "Your character '%s' was created!" % character.name) + return HttpResponseRedirect(self.success_url) + else: + messages.error(self.request, "Your character could not be created. Please contact an admin.") + return self.form_invalid(form)