Permalink
Browse files

wrap into single commit

  • Loading branch information...
Trevor Peacock
Trevor Peacock committed Jan 30, 2019
1 parent 040d10a commit 7da3f3a8158c0b159175e2edecfc703733e3de51
Showing with 187 additions and 57 deletions.
  1. +1 −0 src/wiki/apps.py
  2. +29 −0 src/wiki/checks.py
  3. +20 −56 src/wiki/forms.py
  4. +89 −0 src/wiki/forms_account_handling.py
  5. +41 −1 tests/core/test_checks.py
  6. +7 −0 tests/testdata/models.py
@@ -15,4 +15,5 @@ def ready(self):
register(checks.check_for_required_installed_apps, checks.Tags.required_installed_apps)
register(checks.check_for_obsolete_installed_apps, checks.Tags.obsolete_installed_apps)
register(checks.check_for_context_processors, checks.Tags.context_processors)
register(checks.check_for_fields_in_custom_user_model, checks.Tags.fields_in_custom_user_model)
load_wiki_plugins()
@@ -7,6 +7,7 @@ class Tags:
required_installed_apps = "required_installed_apps"
obsolete_installed_apps = "obsolete_installed_apps"
context_processors = "context_processors"
fields_in_custom_user_model = "fields_in_custom_user_model"


REQUIRED_INSTALLED_APPS = (
@@ -30,6 +31,12 @@ class Tags:
('sekizai.context_processors.sekizai', 'E009'),
)

FIELDS_IN_CUSTOM_USER_MODEL = (
# check function, field fetcher, required field type, error code
('check_user_field', 'USERNAME_FIELD', 'CharField', 'E010'),
('check_email_field', 'get_email_field_name()', 'EmailField', 'E011'),
)


def check_for_required_installed_apps(app_configs, **kwargs):
errors = []
@@ -69,3 +76,25 @@ def check_for_context_processors(app_configs, **kwargs):
)
)
return errors


def check_for_fields_in_custom_user_model(app_configs, **kwargs):
errors = []
from wiki.conf import settings
if not settings.ACCOUNT_HANDLING:
return errors
import wiki.forms_account_handling
from django.contrib.auth import get_user_model
User = get_user_model()
for check_function_name, field_fetcher, required_field_type, error_code in FIELDS_IN_CUSTOM_USER_MODEL:
function = getattr(wiki.forms_account_handling, check_function_name)
if not function(User):
errors.append(
Error(
'%s.%s.%s refers to a field that is not of type %s' % (User.__module__, User.__name__, field_fetcher, required_field_type),
hint='If you have your own login/logout views, turn off settings.WIKI_ACCOUNT_HANDLING',
obj=User,
id='wiki.%s' % error_code,
)
)
return errors
@@ -1,11 +1,26 @@
import random
import string

__all__ = [
'UserCreationForm',
'UserUpdateForm',
'WikiSlugField',
'SpamProtectionMixin',
'CreateRootForm',
'MoveForm',
'EditForm',
'SelectWidgetBootstrap',
'TextInputPrepend',
'CreateForm',
'DeleteForm',
'PermissionsForm',
'DirFilterForm',
'SearchForm',
]

from datetime import timedelta

from django import forms
from django.apps import apps
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
from django.core import validators
from django.core.validators import RegexValidator
from django.forms.widgets import HiddenInput
@@ -21,6 +36,8 @@
from wiki.core.plugins.base import PluginSettingsFormMixin
from wiki.editors import getEditor

from .forms_account_handling import UserCreationForm, UserUpdateForm

validate_slug_numbers = RegexValidator(
r'^[0-9]+$',
_("A 'slug' cannot consist solely of numbers."),
@@ -565,56 +582,3 @@ class SearchForm(forms.Form):
'placeholder': _('Search...'),
'class': 'search-query'}),
required=False)


class UserCreationForm(UserCreationForm):
email = forms.EmailField(required=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Add honeypots
self.honeypot_fieldnames = "address", "phone"
self.honeypot_class = ''.join(
random.choice(string.ascii_uppercase + string.digits)
for __ in range(10))
self.honeypot_jsfunction = 'f' + ''.join(
random.choice(string.ascii_uppercase + string.digits)
for __ in range(10))

for fieldname in self.honeypot_fieldnames:
self.fields[fieldname] = forms.CharField(
widget=forms.TextInput(attrs={'class': self.honeypot_class}),
required=False,
)

def clean(self):
cd = super().clean()
for fieldname in self.honeypot_fieldnames:
if cd[fieldname]:
raise forms.ValidationError(
"Thank you, non-human visitor. Please keep trying to fill in the form.")
return cd

class Meta:
model = User
fields = ("username", "email")


class UserUpdateForm(forms.ModelForm):
password1 = forms.CharField(label="New password", widget=forms.PasswordInput(), required=False)
password2 = forms.CharField(label="Confirm password", widget=forms.PasswordInput(), required=False)

def clean(self):
cd = super().clean()
password1 = cd.get('password1')
password2 = cd.get('password2')

if password1 and password1 != password2:
raise forms.ValidationError(_("Passwords don't match"))

return cd

class Meta:
model = User
fields = ['email']
@@ -0,0 +1,89 @@
import random
import string

import django.contrib.auth.models
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields import CharField, EmailField
from django.utils.translation import gettext_lazy as _
from wiki.conf import settings


def _get_field(model, field):
try:
return model._meta.get_field(field)
except FieldDoesNotExist:
return


User = get_user_model()


def check_user_field(user_model):
return isinstance(_get_field(user_model, user_model.USERNAME_FIELD), CharField)


def check_email_field(user_model):
return isinstance(_get_field(user_model, user_model.get_email_field_name()), EmailField)


# django parses the ModelForm (and Meta classes) on class creation, which fails with custom models without expected fields.
# We need to check this here, because if this module can't load then system checks can't run.
CustomUser = User \
if (settings.ACCOUNT_HANDLING and check_user_field(User) and check_email_field(User)) \
else django.contrib.auth.models.User


class UserCreationForm(UserCreationForm):
email = forms.EmailField(required=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Add honeypots
self.honeypot_fieldnames = "address", "phone"
self.honeypot_class = ''.join(
random.choice(string.ascii_uppercase + string.digits)
for __ in range(10))
self.honeypot_jsfunction = 'f' + ''.join(
random.choice(string.ascii_uppercase + string.digits)
for __ in range(10))

for fieldname in self.honeypot_fieldnames:
self.fields[fieldname] = forms.CharField(
widget=forms.TextInput(attrs={'class': self.honeypot_class}),
required=False,
)

def clean(self):
cd = super().clean()
for fieldname in self.honeypot_fieldnames:
if cd[fieldname]:
raise forms.ValidationError(
"Thank you, non-human visitor. Please keep trying to fill in the form.")
return cd

class Meta:
model = CustomUser
fields = (CustomUser.USERNAME_FIELD, CustomUser.get_email_field_name())


class UserUpdateForm(forms.ModelForm):
password1 = forms.CharField(label="New password", widget=forms.PasswordInput(), required=False)
password2 = forms.CharField(label="Confirm password", widget=forms.PasswordInput(), required=False)

def clean(self):
cd = super().clean()
password1 = cd.get('password1')
password2 = cd.get('password2')

if password1 and password1 != password2:
raise forms.ValidationError(_("Passwords don't match"))

return cd

class Meta:
model = CustomUser
fields = [CustomUser.get_email_field_name()]
@@ -3,7 +3,9 @@
from django.conf import settings
from django.core.checks import Error, registry
from django.test import TestCase
from wiki.checks import REQUIRED_CONTEXT_PROCESSORS, REQUIRED_INSTALLED_APPS, Tags
from wiki.checks import FIELDS_IN_CUSTOM_USER_MODEL, REQUIRED_CONTEXT_PROCESSORS, REQUIRED_INSTALLED_APPS, Tags

from ..base import wiki_override_settings


def _remove(settings, arg):
@@ -40,3 +42,41 @@ def test_required_context_processors(self):
)
]
self.assertEqual(errors, expected_errors)

def test_custom_user_model_mitigation_required(self):
"""
Django & six check django.forms.ModelForm.Meta on definition, and raises an error if Meta.fields don't exist in Meta.model.
This causes problems in wiki.forms.UserCreationForm and wiki.forms.UserUpdateForm when a custom user model doesn't have fields django-wiki assumes.
There is some code in wiki.forms that detects this situation.
This check asserts that Django/six are still raising an exception on definition, and asserts the mitigation code in wiki.forms,
and that test_check_for_fields_in_custom_user_model below are required.
"""
from django.core.exceptions import FieldError
from django import forms
from ..testdata.models import VeryCustomUser
with self.assertRaisesRegex(FieldError, 'Unknown field\\(s\\) \\((email|username|, )+\\) specified for VeryCustomUser'):
class UserUpdateForm(forms.ModelForm):
class Meta:
model = VeryCustomUser
fields = ['username', 'email']

def test_check_for_fields_in_custom_user_model(self):
from django.contrib.auth import get_user_model
with wiki_override_settings(WIKI_ACCOUNT_HANDLING=False, AUTH_USER_MODEL='testdata.VeryCustomUser'):
errors = registry.run_checks(tags=[Tags.fields_in_custom_user_model])
self.assertEqual(errors, [])
with wiki_override_settings(WIKI_ACCOUNT_HANDLING=True, AUTH_USER_MODEL='testdata.VeryCustomUser'):
errors = registry.run_checks(tags=[Tags.fields_in_custom_user_model])
expected_errors = [
Error(
'%s.%s.%s refers to a field that is not of type %s' % (
get_user_model().__module__, get_user_model().__name__, field_fetcher, required_field_type),
hint='If you have your own login/logout views, turn off settings.WIKI_ACCOUNT_HANDLING',
obj=get_user_model(),
id='wiki.%s' % error_code,
)
for check_function_name, field_fetcher, required_field_type, error_code in FIELDS_IN_CUSTOM_USER_MODEL]
self.assertEqual(errors, expected_errors)
with wiki_override_settings(WIKI_ACCOUNT_HANDLING=True):
errors = registry.run_checks(tags=[Tags.fields_in_custom_user_model])
self.assertEqual(errors, [])
@@ -1,3 +1,4 @@
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import AbstractUser
from django.db import models

@@ -8,3 +9,9 @@ class CustomUser(AbstractUser):

class CustomGroup(models.Model):
pass


# user with invalid renamed identifier, and no email field
class VeryCustomUser(AbstractBaseUser):
identifier = models.IntegerField()
USERNAME_FIELD = 'identifier'

0 comments on commit 7da3f3a

Please sign in to comment.