# -*- coding: utf-8 -*-
"""Forms for core app."""

from __future__ import (
    absolute_import, division, print_function, unicode_literals)

import logging
from builtins import object

from django import forms
from django.contrib.auth.models import User
from django.forms.fields import CharField
from django.utils.translation import ugettext_lazy as _

from .models import UserProfile

log = logging.getLogger(__name__)


class UserProfileForm(forms.ModelForm):
    first_name = CharField(label=_('First name'), required=False)
    last_name = CharField(label=_('Last name'), required=False)

    class Meta(object):
        model = UserProfile
        # Don't allow users edit someone else's user page
        fields = ['first_name', 'last_name', 'homepage']

    def __init__(self, *args, **kwargs):
        super(UserProfileForm, self).__init__(*args, **kwargs)
        try:
            self.fields['first_name'].initial = self.instance.user.first_name
            self.fields['last_name'].initial = self.instance.user.last_name
        except AttributeError:
            pass

    def save(self, commit=True):
        first_name = self.cleaned_data.pop('first_name', None)
        last_name = self.cleaned_data.pop('last_name', None)
        profile = super(UserProfileForm, self).save(commit=commit)
        if commit:
            user = profile.user
            user.first_name = first_name
            user.last_name = last_name
            user.save()
        return profile


class UserDeleteForm(forms.ModelForm):
    username = CharField(
        label=_('Username'),
        help_text=_('Please type your username to confirm.'),
    )

    class Meta(object):
        model = User
        fields = ['username']

    def clean_username(self):
        data = self.cleaned_data['username']

        if self.instance.username != data:
            raise forms.ValidationError(_('Username does not match!'))

        return data


class UserAdvertisingForm(forms.ModelForm):
    class Meta(object):
        model = UserProfile
        fields = ['allow_ads']


class FacetField(forms.MultipleChoiceField):

    """
    For filtering searches on a facet.

    Has validation for the format of facet values.
    """

    def valid_value(self, value):
        """
        Although this is a choice field, no choices need to be supplied.

        Instead, we just validate that the value is in the correct format for
        facet filtering (facet_name:value)
        """
        if ':' not in value:
            return False
        return True

readthedocs/core/forms.py
# -*- coding: utf-8 -*-
"""Forms for core app."""

from __future__ import (
    absolute_import, division, print_function, unicode_literals)

import logging
from builtins import object

from django import forms
from django.contrib.auth.models import User
from django.forms.fields import CharField
from django.utils.translation import ugettext_lazy as _

from .models import UserProfile

log = logging.getLogger(__name__)


class UserProfileForm(forms.ModelForm):
    first_name = CharField(label=_('First name'), required=False, max_length=30)
    last_name = CharField(label=_('Last name'), required=False, max_length=30)

    class Meta(object):
        model = UserProfile
        # Don't allow users edit someone else's user page
        fields = ['first_name', 'last_name', 'homepage']

    def __init__(self, *args, **kwargs):
        super(UserProfileForm, self).__init__(*args, **kwargs)
        try:
            self.fields['first_name'].initial = self.instance.user.first_name
            self.fields['last_name'].initial = self.instance.user.last_name
        except AttributeError:
            pass

    def save(self, commit=True):
        first_name = self.cleaned_data.pop('first_name', None)
        last_name = self.cleaned_data.pop('last_name', None)
        profile = super(UserProfileForm, self).save(commit=commit)
        if commit:
            user = profile.user
            user.first_name = first_name
            user.last_name = last_name
            user.save()
        return profile


class UserDeleteForm(forms.ModelForm):
    username = CharField(
        label=_('Username'),
        help_text=_('Please type your username to confirm.'),
    )

    class Meta(object):
        model = User
        fields = ['username']

    def clean_username(self):
        data = self.cleaned_data['username']

        if self.instance.username != data:
            raise forms.ValidationError(_('Username does not match!'))

        return data


class UserAdvertisingForm(forms.ModelForm):
    class Meta(object):
        model = UserProfile
        fields = ['allow_ads']


class FacetField(forms.MultipleChoiceField):

    """
    For filtering searches on a facet.

    Has validation for the format of facet values.
    """

    def valid_value(self, value):
        """
        Although this is a choice field, no choices need to be supplied.

        Instead, we just validate that the value is in the correct format for
        facet filtering (facet_name:value)
        """
        if ':' not in value:
            return False
        return True

readthedocs/core/forms.py
diff --git a/readthedocs/core/forms.py b/readthedocs/core/forms.py
index 6899afa1295..bc062286547 100644
--- a/readthedocs/core/forms.py
+++ b/readthedocs/core/forms.py
@@ -18,8 +18,8 @@
 
 
 class UserProfileForm(forms.ModelForm):
-    first_name = CharField(label=_('First name'), required=False)
-    last_name = CharField(label=_('Last name'), required=False)
+    first_name = CharField(label=_('First name'), required=False, max_length=30)
+    last_name = CharField(label=_('Last name'), required=False, max_length=30)
 
     class Meta(object):
         model = UserProfile
diff --git a/readthedocs/rtd_tests/tests/test_profile_views.py b/readthedocs/rtd_tests/tests/test_profile_views.py
index cdef5d20906..ba7a2989b24 100644
--- a/readthedocs/rtd_tests/tests/test_profile_views.py
+++ b/readthedocs/rtd_tests/tests/test_profile_views.py
@@ -35,6 +35,27 @@ def test_edit_profile(self):
         self.assertEqual(self.user.last_name, 'Docs')
         self.assertEqual(self.user.profile.homepage, 'readthedocs.org')
 
+    def test_edit_profile_with_invalid_values(self):
+        resp = self.client.get(
+            reverse('profiles_profile_edit'),
+        )
+        self.assertTrue(resp.status_code, 200)
+
+        resp = self.client.post(
+            reverse('profiles_profile_edit'),
+            data={
+                'first_name': 'a' * 31,
+                'last_name': 'b' * 31,
+                'homepage': 'c' * 101,
+            }
+        )
+
+        FORM_ERROR_FORMAT = 'Ensure this value has at most {} characters (it has {}).'
+
+        self.assertFormError(resp, form='form', field='first_name', errors=FORM_ERROR_FORMAT.format(30, 31))
+        self.assertFormError(resp, form='form', field='last_name', errors=FORM_ERROR_FORMAT.format(30, 31))
+        self.assertFormError(resp, form='form', field='homepage', errors=FORM_ERROR_FORMAT.format(100, 101))
+
     def test_delete_account(self):
         resp = self.client.get(
             reverse('delete_account')

Validate profile fields on form
Related code

https://github.com/rtfd/readthedocs.org/blob/164800694a25d769234c6e7019c483f347fe9226/readthedocs/core/forms.py#L20-L46

This will raise an exception if the length is greater than the model

Sentry issue https://sentry.io/read-the-docs/readthedocs-org/issues/666774301/
