diff --git a/cadasta/accounts/forms.py b/cadasta/accounts/forms.py index 74dbfe1f0..7cfad2187 100644 --- a/cadasta/accounts/forms.py +++ b/cadasta/accounts/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.conf import settings from django.utils.translation import ugettext as _ from .models import User @@ -14,11 +15,17 @@ class Meta: fields = ['username', 'email', 'password1', 'password2', 'full_name'] + def clean_username(self): + username = self.data.get('username') + if username.lower() in settings.CADASTA_INVALID_ENTITY_NAMES: + raise forms.ValidationError( + _("Username cannot be “add” or “new”.")) + return username + def clean_password1(self): password = self.data.get('password1') if password != self.data.get('password2'): raise forms.ValidationError(_("Passwords do not match")) - return password def clean_email(self): @@ -26,7 +33,6 @@ def clean_email(self): if User.objects.filter(email=email).exists(): raise forms.ValidationError( _("Another user with this email already exists")) - return email def save(self, *args, **kwargs): @@ -47,7 +53,9 @@ def clean_username(self): User.objects.filter(username=username).exists()): raise forms.ValidationError( _("Another user with this username already exists")) - + if username.lower() in settings.CADASTA_INVALID_ENTITY_NAMES: + raise forms.ValidationError( + _("Username cannot be “add” or “new”.")) return username def clean_email(self): @@ -56,5 +64,4 @@ def clean_email(self): User.objects.filter(email=email).exists()): raise forms.ValidationError( _("Another user with this email already exists")) - return email diff --git a/cadasta/accounts/serializers.py b/cadasta/accounts/serializers.py index 8d11bd328..63cde5a99 100644 --- a/cadasta/accounts/serializers.py +++ b/cadasta/accounts/serializers.py @@ -1,5 +1,5 @@ +from django.conf import settings from django.utils import timezone - from django.utils.translation import ugettext as _ from rest_framework.serializers import EmailField, ValidationError @@ -33,6 +33,12 @@ class Meta: 'email': {'required': True, 'unique': True} } + def validate_username(self, username): + if username.lower() in settings.CADASTA_INVALID_ENTITY_NAMES: + raise ValidationError( + _("Username cannot be “add” or “new”.")) + return username + class UserSerializer(djoser_serializers.UserSerializer): email = EmailField( @@ -64,6 +70,9 @@ def validate_username(self, username): username != instance.username and self.context['request'].user != instance): raise ValidationError('Cannot update username') + if username.lower() in settings.CADASTA_INVALID_ENTITY_NAMES: + raise ValidationError( + _("Username cannot be “add” or “new”.")) return username def validate_last_login(self, last_login): diff --git a/cadasta/accounts/tests/test_forms.py b/cadasta/accounts/tests/test_forms.py index 08548081a..a16f54d9f 100644 --- a/cadasta/accounts/tests/test_forms.py +++ b/cadasta/accounts/tests/test_forms.py @@ -1,3 +1,5 @@ +import random + from django.utils.translation import gettext as _ from ..models import User @@ -55,6 +57,22 @@ def test_signup_with_existing_email(self): in form.errors.get('email')) assert User.objects.count() == 1 + def test_signup_with_restricted_username(self): + invalid_usernames = ('add', 'ADD', 'Add', 'new', 'NEW', 'New') + data = { + 'username': random.choice(invalid_usernames), + 'email': 'john@beatles.uk', + 'password1': 'iloveyoko79', + 'password2': 'iloveyoko68', + 'full_name': 'John Lennon', + } + form = RegisterForm(data) + + assert form.is_valid() is False + assert (_("Username cannot be “add” or “new”.") + in form.errors.get('username')) + assert User.objects.count() == 0 + class ProfileFormTest(UserTestCase): def test_update_user(self): @@ -94,3 +112,15 @@ def test_update_user_with_existing_email(self): } form = ProfileForm(data, instance=user) assert form.is_valid() is False + + def test_update_user_with_restricted_username(self): + user = UserFactory.create(username='imagine71', + email='john@beatles.uk') + invalid_usernames = ('add', 'ADD', 'Add', 'new', 'NEW', 'New') + data = { + 'username': random.choice(invalid_usernames), + 'email': 'john@beatles.uk', + 'full_name': 'John Lennon', + } + form = ProfileForm(data, instance=user) + assert form.is_valid() is False diff --git a/cadasta/accounts/tests/test_serializers.py b/cadasta/accounts/tests/test_serializers.py index 4da9276f4..00d461263 100644 --- a/cadasta/accounts/tests/test_serializers.py +++ b/cadasta/accounts/tests/test_serializers.py @@ -1,3 +1,4 @@ +import random import pytest from datetime import datetime from django.utils.translation import gettext as _ @@ -73,6 +74,21 @@ def test_create_with_existing_email(self): assert (_("Another user is already registered with this email address") in serializer._errors['email']) + def test_create_with_restricted_username(self): + invalid_usernames = ('add', 'ADD', 'Add', 'new', 'NEW', 'New') + data = { + 'username': random.choice(invalid_usernames), + 'email': 'john@beatles.uk', + 'password': 'iloveyoko79', + 'password_repeat': 'iloveyoko79', + 'full_name': 'John Lennon', + } + + serializer = RegistrationSerializer(data=data) + assert not serializer.is_valid() + assert (_("Username cannot be “add” or “new”.") + in serializer._errors['username']) + class UserSerializerTest(UserTestCase): def test_field_serialization(self): @@ -102,8 +118,7 @@ def test_create_with_valid_data(self): def test_update_username_fails(self): serializer = UserSerializer(data=BASIC_TEST_DATA) assert serializer.is_valid() - serializer.save() - user = User.objects.first() + user = serializer.save() other_user = UserFactory.create() update_data = {'username': 'bad-update'} request = APIRequestFactory().patch('/user/imagine71', update_data) @@ -125,6 +140,25 @@ def test_update_last_login_fails(self): assert not serializer2.is_valid() assert serializer2.errors['last_login'] == ['Cannot update last_login'] + def test_update_with_restricted_username(self): + serializer = UserSerializer(data=BASIC_TEST_DATA) + assert serializer.is_valid() + user = serializer.save() + invalid_usernames = ('add', 'ADD', 'Add', 'new', 'NEW', 'New') + data = { + 'username': random.choice(invalid_usernames), + 'email': 'john@beatles.uk', + 'full_name': 'John Lennon', + } + request = APIRequestFactory().patch('/user/imagine71', data) + force_authenticate(request, user=user) + serializer2 = UserSerializer( + user, data=data, context={'request': Request(request)} + ) + assert not serializer2.is_valid() + assert serializer2.errors['username'] == [ + _("Username cannot be “add” or “new”.")] + class AccountLoginSerializerTest(UserTestCase): def test_unverified_account(self): diff --git a/cadasta/config/settings/default.py b/cadasta/config/settings/default.py index c39ad9752..8b16321b4 100644 --- a/cadasta/config/settings/default.py +++ b/cadasta/config/settings/default.py @@ -178,6 +178,9 @@ } } +# Invalid names for Cadasta organizations, projects, and usernames +CADASTA_INVALID_ENTITY_NAMES = ['add', 'new'] + # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ diff --git a/cadasta/organization/forms.py b/cadasta/organization/forms.py index 5dc2e53fa..e552bc69b 100644 --- a/cadasta/organization/forms.py +++ b/cadasta/organization/forms.py @@ -1,6 +1,8 @@ from django import forms +from django.conf import settings from django.contrib.postgres import forms as pg_forms from django.contrib.gis import forms as gisforms +from django.utils.text import slugify from django.utils.translation import ugettext as _ from django.db import transaction @@ -96,13 +98,22 @@ def to_list(self, value): def clean_urls(self): return self.to_list(self.data.get('urls')) + def clean_name(self): + is_create = not self.instance.id + name = self.cleaned_data['name'] + invalid_names = settings.CADASTA_INVALID_ENTITY_NAMES + if is_create and slugify(name) in invalid_names: + raise forms.ValidationError( + _("Organization name cannot be “Add” or “New”.")) + return name + def save(self, *args, **kwargs): instance = super(OrganizationForm, self).save(commit=False) - create = not instance.id + is_create = not instance.id instance.save() - if create: + if is_create: OrganizationRole.objects.create( organization=instance, user=self.user, @@ -208,6 +219,13 @@ def __init__(self, *args, **kwargs): (o.slug, o.name) for o in Organization.objects.order_by('name') ] + def clean_name(self): + name = self.cleaned_data['name'] + if slugify(name) in settings.CADASTA_INVALID_ENTITY_NAMES: + raise forms.ValidationError( + _("Project name cannot be “Add” or “New”.")) + return name + class ProjectEditDetails(forms.ModelForm): urls = pg_forms.SimpleArrayField(forms.URLField(), required=False) diff --git a/cadasta/organization/serializers.py b/cadasta/organization/serializers.py index 46ad05e0c..3ddafb936 100644 --- a/cadasta/organization/serializers.py +++ b/cadasta/organization/serializers.py @@ -2,6 +2,7 @@ from django.db.models import Q from django.core.mail import send_mail from django.core.urlresolvers import reverse +from django.utils.text import slugify from django.utils.translation import ugettext as _ from django.template.loader import get_template from django.template import Context @@ -26,6 +27,14 @@ class Meta: read_only_fields = ('id', 'slug',) detail_only_fields = ('users',) + def validate_name(self, value): + is_create = not self.instance + invalid_names = settings.CADASTA_INVALID_ENTITY_NAMES + if is_create and slugify(value) in invalid_names: + raise serializers.ValidationError( + _("Organization name cannot be “Add” or “New”.")) + return value + def create(self, *args, **kwargs): org = super(OrganizationSerializer, self).create(*args, **kwargs) @@ -43,6 +52,14 @@ class ProjectSerializer(DetailSerializer, serializers.ModelSerializer): organization = OrganizationSerializer(read_only=True) country = CountryField(required=False) + def validate_name(self, value): + is_create = not self.instance + invalid_names = settings.CADASTA_INVALID_ENTITY_NAMES + if is_create and slugify(value) in invalid_names: + raise serializers.ValidationError( + _("Project name cannot be “Add” or “New”.")) + return value + class Meta: model = Project fields = ('id', 'organization', 'country', 'name', 'description', diff --git a/cadasta/organization/tests/test_forms.py b/cadasta/organization/tests/test_forms.py index 5e68cc079..0b184c5d9 100644 --- a/cadasta/organization/tests/test_forms.py +++ b/cadasta/organization/tests/test_forms.py @@ -1,4 +1,5 @@ import os +import random from pytest import raises from django.conf import settings @@ -18,7 +19,7 @@ from tutelary.models import Policy -class OrganzationTest(UserTestCase): +class OrganizationTest(UserTestCase): def setUp(self): super().setUp() self.data = { @@ -77,6 +78,23 @@ def test_add_organization_with_contact(self): 'email': 'ringo@beatles.uk' }] + def test_add_organization_with_restricted_name(self): + invalid_names = ('add', 'ADD', 'Add', 'new', 'NEW', 'New') + data = { + 'name': random.choice(invalid_names), + 'contacts-TOTAL_FORMS': 1, + 'contacts-INITIAL_FORMS': 0, + 'contacts-0-name': '', + 'contacts-0-email': '', + 'contacts-0-tel': '' + } + form = forms.OrganizationForm(data, user=UserFactory.create()) + assert not form.is_valid() + assert form.errors == { + 'name': ["Organization name cannot be “Add” or “New”."] + } + assert not Organization.objects.exists() + def test_update_organization(self): org = OrganizationFactory.create(slug='some-org') self.data['description'] = 'Org description' @@ -216,6 +234,26 @@ def test_edit_project_roles(self): is False) +class ProjectAddDetailsTest(UserTestCase): + def test_add_new_project_with_restricted_name(self): + org = OrganizationFactory.create() + invalid_names = ('add', 'ADD', 'Add', 'new', 'NEW', 'New') + data = { + 'organization': org.slug, + 'name': random.choice(invalid_names), + 'contacts-TOTAL_FORMS': 1, + 'contacts-INITIAL_FORMS': 0, + 'contacts-0-name': '', + 'contacts-0-email': '', + 'contacts-0-tel': '' + } + form = forms.ProjectAddDetails(data=data) + assert not form.is_valid() + assert form.errors == { + 'name': ["Project name cannot be “Add” or “New”."] + } + + class ProjectEditDetailsTest(UserTestCase): def _get_form(self, form_name): path = os.path.dirname(settings.BASE_DIR) diff --git a/cadasta/organization/tests/test_serializers.py b/cadasta/organization/tests/test_serializers.py index 9e29d5c2e..69cdc7cb4 100644 --- a/cadasta/organization/tests/test_serializers.py +++ b/cadasta/organization/tests/test_serializers.py @@ -1,3 +1,4 @@ +import random import pytest from datetime import datetime from django.utils.text import slugify @@ -49,6 +50,23 @@ def test_users_are_serialized_detail_view(self): serializer = serializers.OrganizationSerializer(org, detail=True) assert 'users' in serializer.data + def test_restricted_organization_name(self): + request = APIRequestFactory().post('/') + user = UserFactory.create() + setattr(request, 'user', user) + + invalid_names = ('add', 'ADD', 'Add', 'new', 'NEW', 'New') + data = {'name': random.choice(invalid_names)} + serializer = serializers.OrganizationSerializer( + data=data, + context={'request': request} + ) + with pytest.raises(ValidationError): + serializer.is_valid(raise_exception=True) + assert serializer.errors == { + 'name': ["Organization name cannot be “Add” or “New”."] + } + class ProjectSerializerTest(TestCase): def test_project_is_set(self): @@ -107,6 +125,24 @@ def test_project_private_visibility(self): project_instance = serializer.instance assert project_instance.access == 'private' + def test_restricted_project_name(self): + org = OrganizationFactory.create() + invalid_names = ('add', 'ADD', 'Add', 'new', 'NEW', 'New') + data = { + 'name': random.choice(invalid_names), + 'organization': org, + 'access': 'private' + } + serializer = serializers.ProjectSerializer( + data=data, + context={'organization': org} + ) + with pytest.raises(ValidationError): + serializer.is_valid(raise_exception=True) + assert serializer.errors == { + 'name': ["Project name cannot be “Add” or “New”."] + } + class ProjectGeometrySerializerTest(TestCase): def test_method_fields_work(self):