Skip to content
This repository has been archived by the owner on Jan 19, 2021. It is now read-only.

Commit

Permalink
[Fix bug 1191850] Backend refactoring for edit profile redesign.
Browse files Browse the repository at this point in the history
* Create subforms to facilitate new settings page.
* Replace old user/profile forms
* Change view to handle multiple forms
* Add logic to determine form gets submitted
* Integration work between frontend/backend
* Check for form errors in view instead of template.
* Move registration form to different template.
* Change IDs to avoid linking to `#developer` element.
* Check for non form/field errors in each tab.
  • Loading branch information
johngian committed Sep 18, 2015
1 parent 98d945b commit 91b5b87
Show file tree
Hide file tree
Showing 12 changed files with 553 additions and 418 deletions.
122 changes: 86 additions & 36 deletions mozillians/phonebook/forms.py
Expand Up @@ -156,42 +156,15 @@ def clean_username(self):
return username


class ProfileForm(happyforms.ModelForm):
class BasicInformationForm(happyforms.ModelForm):
photo = forms.ImageField(label=_lazy(u'Profile Photo'), required=False)
photo_delete = forms.BooleanField(label=_lazy(u'Remove Profile Photo'),
required=False)
date_mozillian = forms.DateField(
required=False,
label=_lazy(u'When did you get involved with Mozilla?'),
widget=MonthYearWidget(years=range(1998, datetime.today().year + 1),
required=False))
skills = forms.CharField(
label='',
help_text=_lazy(u'Start typing to add a skill (example: Python, '
u'javascript, Graphic Design, User Research)'),
required=False)
lat = forms.FloatField(widget=forms.HiddenInput)
lng = forms.FloatField(widget=forms.HiddenInput)
savecountry = forms.BooleanField(
label=_lazy(u'Required'),
initial=True, required=False,
widget=forms.CheckboxInput(attrs={'disabled': 'disabled'})
)
saveregion = forms.BooleanField(label=_lazy(u'Save'), required=False, show_hidden_initial=True)
savecity = forms.BooleanField(label=_lazy(u'Save'), required=False, show_hidden_initial=True)

class Meta:
model = UserProfile
fields = ('full_name', 'ircname', 'bio', 'photo',
'allows_community_sites', 'tshirt',
'title', 'allows_mozilla_sites',
'date_mozillian', 'story_link', 'timezone',
'privacy_photo', 'privacy_full_name', 'privacy_ircname',
'privacy_timezone', 'privacy_tshirt',
'privacy_bio', 'privacy_geo_city', 'privacy_geo_region',
'privacy_geo_country', 'privacy_groups',
'privacy_skills', 'privacy_languages',
'privacy_date_mozillian', 'privacy_story_link', 'privacy_title')
fields = ('photo', 'privacy_photo', 'full_name',
'privacy_full_name', 'bio', 'privacy_bio',)
widgets = {'bio': forms.Textarea()}

def clean_photo(self):
Expand All @@ -215,6 +188,18 @@ def clean_photo(self):
photo.size = cleaned_photo.tell()
return photo


class SkillsForm(happyforms.ModelForm):
skills = forms.CharField(
label='',
help_text=_lazy(u'Start typing to add a skill (example: Python, '
u'javascript, Graphic Design, User Research)'),
required=False)

class Meta:
model = UserProfile
fields = ('privacy_skills',)

def clean_skills(self):
if not re.match(r'^[a-zA-Z0-9 +.:,-]*$', self.cleaned_data['skills']):
# Commas cannot be included in skill names because we use them to
Expand All @@ -226,6 +211,32 @@ def clean_skills(self):
map(lambda x: x.strip() or False,
skills.lower().split(',')))

def save(self, *args, **kwargs):
"""Save the data to profile."""
self.instance.set_membership(Skill, self.cleaned_data['skills'])
super(SkillsForm, self).save(*args, **kwargs)


class LanguagesPrivacyForm(happyforms.ModelForm):
class Meta:
model = UserProfile
fields = ('privacy_languages',)


class LocationForm(happyforms.ModelForm):
lat = forms.FloatField(widget=forms.HiddenInput)
lng = forms.FloatField(widget=forms.HiddenInput)
savecountry = forms.BooleanField(label=_lazy(u'Required'),
initial=True, required=False,
widget=forms.CheckboxInput(attrs={'disabled': 'disabled'}))
saveregion = forms.BooleanField(label=_lazy(u'Save'), required=False, show_hidden_initial=True)
savecity = forms.BooleanField(label=_lazy(u'Save'), required=False, show_hidden_initial=True)

class Meta:
model = UserProfile
fields = ('timezone', 'privacy_timezone', 'privacy_geo_city', 'privacy_geo_region',
'privacy_geo_country',)

def clean(self):
# If lng/lat were provided, make sure they point at a country somewhere...
if self.cleaned_data.get('lat') is not None and self.cleaned_data.get('lng') is not None:
Expand Down Expand Up @@ -256,10 +267,43 @@ def clean(self):

return self.cleaned_data

def save(self, *args, **kwargs):
"""Save the data to profile."""
self.instance.set_membership(Skill, self.cleaned_data['skills'])
super(ProfileForm, self).save(*args, **kwargs)

class ContributionForm(happyforms.ModelForm):
date_mozillian = forms.DateField(
required=False,
label=_lazy(u'When did you get involved with Mozilla?'),
widget=MonthYearWidget(years=range(1998, datetime.today().year + 1),
required=False))

class Meta:
model = UserProfile
fields = ('title', 'privacy_title',
'date_mozillian', 'privacy_date_mozillian',
'story_link', 'privacy_story_link',)


class TshirtForm(happyforms.ModelForm):
class Meta:
model = UserProfile
fields = ('tshirt', 'privacy_tshirt',)


class GroupsPrivacyForm(happyforms.ModelForm):
class Meta:
model = UserProfile
fields = ('privacy_groups',)


class IRCForm(happyforms.ModelForm):
class Meta:
model = UserProfile
fields = ('ircname', 'privacy_ircname',)


class DeveloperForm(happyforms.ModelForm):
class Meta:
model = UserProfile
fields = ('allows_community_sites', 'allows_mozilla_sites',)


class BaseLanguageFormSet(BaseInlineFormSet):
Expand Down Expand Up @@ -296,11 +340,17 @@ def email_changed(self):
return self.cleaned_data['email'] != self.initial['email']


class RegisterForm(ProfileForm):
class RegisterForm(BasicInformationForm, LocationForm):
optin = forms.BooleanField(
widget=forms.CheckboxInput(attrs={'class': 'checkbox'}),
required=True)

class Meta:
model = UserProfile
fields = ('photo', 'full_name', 'timezone', 'privacy_photo', 'privacy_full_name', 'optin',
'privacy_timezone', 'privacy_geo_city', 'privacy_geo_region',
'privacy_geo_country',)


class VouchForm(happyforms.Form):
"""Vouching is captured via a user's id and a description of the reason for vouching."""
Expand Down Expand Up @@ -336,4 +386,4 @@ class APIKeyRequestForm(happyforms.ModelForm):

class Meta:
model = APIv2App
fields = ('name', 'description', 'url')
fields = ('name', 'description', 'url',)
169 changes: 101 additions & 68 deletions mozillians/phonebook/views.py
Expand Up @@ -5,12 +5,13 @@
from django.contrib.auth.models import User
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db import transaction
from django.http import Http404
from django.http import HttpResponseRedirect, Http404
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.views.decorators.cache import cache_page, never_cache
from django.views.decorators.http import require_POST

from funfactory.helpers import urlparams
from funfactory.urlresolvers import reverse
from tower import ugettext as _
from waffle.decorators import waffle_flag
Expand Down Expand Up @@ -157,79 +158,111 @@ def view_profile(request, username):
@never_cache
def edit_profile(request):
"""Edit user profile view."""
# Don't user request.user
# Don't use request.user
user = User.objects.get(pk=request.user.id)
profile = user.userprofile
user_groups = profile.groups.all().order_by('name')
user_skills = stringify_groups(profile.skills.all().order_by('name'))

user_form = forms.UserForm(request.POST or None, instance=user)
queryset = ExternalAccount.objects.exclude(type=ExternalAccount.TYPE_EMAIL)
accounts_formset = forms.AccountsFormset(request.POST or None,
instance=profile,
queryset=queryset)
new_profile = False
form = forms.ProfileForm
language_formset = forms.LanguagesFormset(request.POST or None,
instance=profile,
locale=request.locale)

if not profile.is_complete:
new_profile = True
form = forms.RegisterForm

profile_form = form(request.POST or None, request.FILES or None,
instance=profile,
initial={'skills': user_skills,
'saveregion': True if profile.geo_region else False,
'savecity': True if profile.geo_city else False,
'lat': profile.lat,
'lng': profile.lng})

all_forms = [user_form, profile_form, accounts_formset, language_formset]

# Using ``list`` to force calling is_valid on all the forms, even if earlier
# ones are not valid, so we detect and display all the errors.
if all(list(f.is_valid() for f in all_forms)):
old_username = request.user.username
user_form.save()
profile_form.save()
accounts_formset.save()
language_formset.save()

if new_profile:
redeem_invite(profile, request.session.get('invite-code'))
messages.info(request, _(u'Your account has been created.'))
elif user.username != old_username:
# Notify the user that their old profile URL won't work.
messages.info(request,
_(u'You changed your username; please note your '
u'profile URL has also changed.'))

return redirect('phonebook:profile_view', user.username)

emails = ExternalAccount.objects.filter(type=ExternalAccount.TYPE_EMAIL)
email_privacy_form = forms.EmailPrivacyForm(request.POST or None, instance=profile)
alternate_email_formset = forms.AlternateEmailFormset(request.POST or None,
instance=profile,
queryset=emails)
accounts_qs = ExternalAccount.objects.exclude(type=ExternalAccount.TYPE_EMAIL)

sections = {
'registration_section': ['user_form', 'registration_form'],
'basic_section': ['user_form', 'basic_information_form'],
'groups_section': ['groups_privacy_form'],
'skills_section': ['skills_form'],
'email_section': ['email_privacy_form', 'alternate_email_formset'],
'languages_section': ['language_privacy_form', 'language_formset'],
'accounts_section': ['accounts_formset'],
'irc_section': ['irc_form'],
'location_section': ['location_form'],
'contribution_section': ['contribution_form'],
'tshirt_section': ['tshirt_form'],
'developer_section': ['developer_form']
}

curr_sect = next((s for s in sections.keys() if s in request.POST), None)

def get_request_data(form):
if curr_sect and form in sections[curr_sect]:
return request.POST
return None

ctx = {}
ctx['user_form'] = forms.UserForm(get_request_data('user_form'), instance=user)
ctx['registration_form'] = forms.RegisterForm(get_request_data('registration_form'),
request.FILES or None,
instance=profile)
basic_information_data = get_request_data('basic_information_form')
ctx['basic_information_form'] = forms.BasicInformationForm(basic_information_data,
request.FILES or None,
instance=profile)
ctx['accounts_formset'] = forms.AccountsFormset(get_request_data('accounts_formset'),
instance=profile,
queryset=accounts_qs)
ctx['language_formset'] = forms.LanguagesFormset(get_request_data('language_formset'),
instance=profile,
locale=request.locale)
language_privacy_data = get_request_data('language_privacy_form')
ctx['language_privacy_form'] = forms.LanguagesPrivacyForm(language_privacy_data,
instance=profile)
ctx['skills_form'] = forms.SkillsForm(get_request_data('skills_form'), instance=profile,
initial={'skills': user_skills})
location_initial = {
'saveregion': True if profile.geo_region else False,
'savecity': True if profile.geo_city else False,
'lat': profile.lat,
'lng': profile.lng
}
ctx['location_form'] = forms.LocationForm(get_request_data('location_form'), instance=profile,
initial=location_initial)
ctx['contribution_form'] = forms.ContributionForm(get_request_data('contribution_form'),
instance=profile)
ctx['tshirt_form'] = forms.TshirtForm(get_request_data('tshirt_form'), instance=profile)
ctx['groups_privacy_form'] = forms.GroupsPrivacyForm(get_request_data('groups_privacy_form'),
instance=profile)
ctx['irc_form'] = forms.IRCForm(get_request_data('irc_form'), instance=profile)
ctx['developer_form'] = forms.DeveloperForm(get_request_data('developer_form'),
instance=profile)
ctx['email_privacy_form'] = forms.EmailPrivacyForm(get_request_data('email_privacy_form'),
instance=profile)
alternate_email_formset_data = get_request_data('alternate_email_formset')
ctx['alternate_email_formset'] = forms.AlternateEmailFormset(alternate_email_formset_data,
instance=profile,
queryset=emails)
forms_valid = True
if request.POST:
if not curr_sect:
raise Http404
curr_forms = map(lambda x: ctx[x], sections[curr_sect])
forms_valid = all(map(lambda x: x.is_valid(), curr_forms))
if forms_valid:
old_username = request.user.username
for f in curr_forms:
f.save()

if curr_sect == 'registration_section':
redeem_invite(profile, request.session.get('invite-code'))
elif user.username != old_username:
msg = (u'You changed your username; '
u'please note your profile URL has also changed.')
messages.info(request, _(msg))

next_section = request.GET.get('next')
next_url = urlparams(reverse('phonebook:profile_edit'), next_section)
return HttpResponseRedirect(next_url)

ctx.update({
'user_groups': user_groups,
'profile': request.user.userprofile,
'vouch_threshold': settings.CAN_VOUCH_THRESHOLD,
'mapbox_id': settings.MAPBOX_PROFILE_ID,
'apps': user.apiapp_set.filter(is_active=True),
'appsv2': profile.apps.filter(enabled=True),
'forms_valid': forms_valid
})

data = dict(profile_form=profile_form,
user_form=user_form,
accounts_formset=accounts_formset,
email_privacy_form=email_privacy_form,
alternate_email_formset=alternate_email_formset,
user_groups=user_groups,
profile=request.user.userprofile,
language_formset=language_formset,
vouch_threshold=settings.CAN_VOUCH_THRESHOLD,
mapbox_id=settings.MAPBOX_PROFILE_ID,
apps=user.apiapp_set.filter(is_active=True),
appsv2=profile.apps.filter(enabled=True))

# If there are form errors, don't send a 200 OK.
status = 400 if any(f.errors for f in all_forms) else 200
return render(request, 'phonebook/edit_profile.html', data, status=status)
return render(request, 'phonebook/edit_profile.html', ctx)


@allow_unvouched
Expand Down
19 changes: 11 additions & 8 deletions mozillians/templates/includes/field.html
Expand Up @@ -2,15 +2,18 @@
<div class="privacy-controls">
<span class="info glyphicon glyphicon-eye-open" aria-hidden="true" title="{{ _('Choose visibility group for this field') }}"></span>
<div class="btn-group btn-group-xs" role="group">
{% for value, text in field.field.choices %}
{% if value == 1 %}
<button type="button" class="btn btn-default active">{{ text }}</button>
{% else %}
<button type="button" class="privacy-toggle btn btn-default {% if value == field.value() %}active{% endif %}" data-target="{{ field.name }}" data-value="{{ value }}">{{ text }}</button>
{% endif %}
{% endfor %}
<input type="hidden" value="{{ field.value() }}" name="{{ field.name }}" id="{{ field.name }}">
{% for value, text in field.field.choices %}
{% if value == 1 %}
<button type="button" class="btn btn-default active">{{ text }}</button>
{% else %}
<button type="button" class="privacy-toggle btn btn-default {% if value == field.value()|int %}active{% endif %}" data-target="{{ field.name }}" data-value="{{ value }}">{{ text }}</button>
{% endif %}
{% endfor %}
<input type="hidden" value="{{ field.value() }}" name="{{ field.html_name }}" id="{{ field.auto_id }}">
</div>
{% if field.errors %}
<div class="error-message">{{ field.errors }}</div>
{% endif %}
</div>
{% else %}

Expand Down

0 comments on commit 91b5b87

Please sign in to comment.