Skip to content
Browse files

[fix bug 763509] Add location to profiles.

Users can add country/region, state/province, and city to their profile.
The data isn't displayed on the profile anywhere--let's do that in a
follow-up: this is the important part for right now.

You can't specify a specific region without specifying a more general
one. For example, you can't list a state without a country, or a city
without a state.

Includes tests.
  • Loading branch information...
1 parent 793090e commit 367be2f7cb55c67df4576df0beddc7138ed88ceb James Socol committed Jun 11, 2012
View
26 apps/phonebook/forms.py
@@ -123,20 +123,11 @@ class ProfileForm(UserForm):
required=False,
widget=UsernameWidget)
- #: L10n: Street address; not entire address
- street = forms.CharField(label=_lazy(u'Address'), required=False)
- city = forms.CharField(label=_lazy(u'City'), required=False)
- # TODO: Add validation of states/provinces/etc. for known/large countries.
- province = forms.CharField(label=_lazy(u'Province/State'), required=False)
- # TODO: Add list of countries.
- country = forms.CharField(label=_lazy(u'Country'), required=False)
- postal_code = forms.CharField(label=_lazy(u'Postal/Zip Code'),
- required=False)
-
class Meta:
# Model form stuff
model = UserProfile
- fields = ('ircname', 'website', 'bio', 'photo')
+ fields = ('ircname', 'website', 'bio', 'photo', 'country', 'region',
+ 'city')
widgets = {
'bio': forms.Textarea()
}
@@ -166,6 +157,19 @@ def clean_skills(self):
for s in self.cleaned_data['skills'].lower().split(',')
if s and ',' not in s]
+ def clean(self):
+ """Make sure geographic fields aren't underspecified."""
+ cleaned_data = super(ProfileForm, self).clean()
+ # Rather than raising ValidationErrors for the whole form, we can
+ # add errors to specific fields.
+ if cleaned_data['city'] and not cleaned_data['region']:
+ self._errors['region'] = [_(u'You must specify a region to '
+ 'specify a city.')]
+ if cleaned_data['region'] and not cleaned_data['country']:
+ self._errors['country'] = [_(u'You must specify a country to '
+ 'specify a district.')]
+ return cleaned_data
+
def save(self, request):
"""Save the data to profile."""
self.instance.set_membership(Group, self.cleaned_data['groups'])
View
12 apps/phonebook/templates/phonebook/edit_profile.html
@@ -22,11 +22,23 @@
</ul>
<div class="tab-content">
<div class="tab-pane active" id="1">
+ <h2>{{ _('Personal') }}</h2>
{% include 'phonebook/includes/photo_form.html' %}
{{ bootstrap(form.first_name) }}
{{ bootstrap(form.last_name) }}
{{ bootstrap(form.website) }}
{{ bootstrap(form.bio) }}
+
+ <h2>{{ _('Location') }}</h2>
+ <p>
+ {% trans %}
+ Let us know where in the world you are. Other vouched Mozillians
+ will be able to contact you easier and vice-versa.
+ {% endtrans %}
+ </p>
+ {{ bootstrap(form.country) }}
+ {{ bootstrap(form.region) }}
+ {{ bootstrap(form.city) }}
</div>
<div class="tab-pane" id="skills">
<legend>Groups</legend>
View
38 apps/phonebook/tests/test_edit_profile.py
@@ -0,0 +1,38 @@
+from django.test.utils import override_settings
+
+from funfactory.urlresolvers import reverse
+from nose.tools import eq_
+
+from common import browserid_mock
+from common.tests import TestCase, user
+
+ASSERTION = 'asldkfjasldfka'
+
+@override_settings(AUTHENTICATION_BACKENDS=('common.backends.TestBackend',))
+class EditProfileTests(TestCase):
+
+ def test_geographic_fields_increasing(self):
+ """Geographic fields exist and require increasing specificity."""
+ u = user()
+ self.client.login(email=u.email, password='testpass')
+ # For some reason last_name is a required field.
+ data = {'city': 'New York', 'last_name': 'Foobar'}
+ url = reverse('profile.edit')
+ response = self.client.post(url, data)
+ eq_(400, response.status_code)
+
+ data.update({'region': 'New York'})
+ response = self.client.post(url, data)
+ eq_(400, response.status_code)
+
+ data.update({'country': 'us'})
+ response = self.client.post(url, data, follow=True)
+ eq_(200, response.status_code)
+
+ def test_invalid_country(self):
+ """Not every country is a real country."""
+ u = user()
+ self.client.login(email=u.email, password='testpass')
+ data = {'country': 'xyz', 'last_name': 'Foobar'}
+ response = self.client.post(reverse('profile.edit'), data)
+ eq_(400, response.status_code)
View
11 apps/phonebook/views.py
@@ -12,6 +12,7 @@
import commonware.log
from funfactory.urlresolvers import reverse
+from product_details import product_details
from tower import ugettext as _
from groups.helpers import stringify_groups
@@ -56,6 +57,10 @@ def profile(request, username):
@never_cache
@login_required
def edit_profile(request):
+ COUNTRIES = product_details.get_regions(request.locale).items()
+ COUNTRIES = sorted(COUNTRIES, key=lambda country: country[1])
+ COUNTRIES.insert(0, ('', '----'))
+
profile = request.user.get_profile()
user_groups = stringify_groups(profile.groups.all().order_by('name'))
user_skills = stringify_groups(profile.skills.all().order_by('name'))
@@ -66,6 +71,7 @@ def edit_profile(request):
request.FILES,
instance=profile,
)
+ form.fields['region'].choices = COUNTRIES
if form.is_valid():
old_username = request.user.username
form.save(request)
@@ -94,6 +100,7 @@ def edit_profile(request):
instance=profile,
initial=initial,
)
+ form.fields['country'].choices = COUNTRIES
# When changing this keep in mind that the same view is used for
# user.register.
@@ -102,7 +109,9 @@ def edit_profile(request):
user_groups=user_groups,
my_vouches=UserProfile.objects.filter(vouched_by=profile),
profile=profile)
- return render(request, 'phonebook/edit_profile.html', d)
+ # If there are form errors, don't send a 200 OK.
+ status = 400 if form.errors else 200
+ return render(request, 'phonebook/edit_profile.html', d, status=status)
@never_cache
View
111 ...migrations/0015_auto__add_field_userprofile_country__add_field_userprofile_region__add.py
@@ -0,0 +1,111 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'UserProfile.country'
+ db.add_column('profile', 'country', self.gf('django.db.models.fields.CharField')(default='', max_length=5, blank=True), keep_default=False)
+
+ # Adding field 'UserProfile.region'
+ db.add_column('profile', 'region', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False)
+
+ # Adding field 'UserProfile.city'
+ db.add_column('profile', 'city', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'UserProfile.country'
+ db.delete_column('profile', 'country')
+
+ # Deleting field 'UserProfile.region'
+ db.delete_column('profile', 'region')
+
+ # Deleting field 'UserProfile.city'
+ db.delete_column('profile', 'city')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'groups.group': {
+ 'Meta': {'object_name': 'Group', 'db_table': "'group'"},
+ 'always_auto_complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'auto_complete': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'irc_channel': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '63', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
+ 'steward': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.UserProfile']", 'null': 'True', 'blank': 'True'}),
+ 'system': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'url': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+ 'website': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'blank': 'True'}),
+ 'wiki': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'blank': 'True'})
+ },
+ 'groups.skill': {
+ 'Meta': {'object_name': 'Skill'},
+ 'always_auto_complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'auto_complete': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
+ },
+ 'users.userprofile': {
+ 'Meta': {'object_name': 'UserProfile', 'db_table': "'profile'"},
+ 'bio': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'city': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'country': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}),
+ 'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['groups.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ircname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '63', 'blank': 'True'}),
+ 'is_vouched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}),
+ 'photo': ('sorl.thumbnail.fields.ImageField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+ 'region': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'skills': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['groups.Skill']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}),
+ 'vouched_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['users.UserProfile']", 'null': 'True', 'blank': 'True'}),
+ 'website': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200', 'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['users']
View
12 apps/users/models.py
@@ -12,6 +12,7 @@
from elasticutils.models import SearchMixin
from funfactory.urlresolvers import reverse
from PIL import Image, ImageOps
+from product_details import product_details
from sorl.thumbnail import ImageField
from tower import ugettext as _, ugettext_lazy as _lazy
@@ -23,6 +24,8 @@
fs = FileSystemStorage(location=settings.UPLOAD_ROOT,
base_url='/media/uploads/')
+COUNTRIES = product_details.get_regions('en-US').items()
+
class UserProfile(SearchMixin, models.Model):
# This field is required.
@@ -40,13 +43,22 @@ class UserProfile(SearchMixin, models.Model):
groups = models.ManyToManyField(Group, blank=True)
skills = models.ManyToManyField(Skill, blank=True)
+
+ # Personal info
bio = models.TextField(verbose_name=_lazy(u'Bio'), default='', blank=True)
photo = ImageField(default='', blank=True, storage=fs,
upload_to='userprofile')
display_name = models.CharField(max_length=255, default='', blank=True)
ircname = models.CharField(max_length=63,
verbose_name=_lazy(u'IRC Nickname'),
default='', blank=True)
+ country = models.CharField(max_length=5, default='', blank=True,
+ choices=COUNTRIES,
+ verbose_name=_lazy(u'Country'))
+ region = models.CharField(max_length=255, default='', blank=True,
+ verbose_name=_lazy(u'Region'))
+ city = models.CharField(max_length=255, default='', blank=True,
+ verbose_name=_lazy(u'City'))
@property
def full_name(self):
View
6 media/css/base.css
@@ -451,6 +451,12 @@ legend {
width: 210px;
}
+.tab-content h2 {
+ border-bottom: 1px solid #000;
+ margin-bottom: 6px;
+ padding-bottom: 3px;
+}
+
.tab-content .field_description {
margin-bottom: 20px;
}
View
1 settings/default.py
@@ -159,7 +159,6 @@
'south',
# re-assert dominance of 'django_nose'
'django_nose',
-
]
## Auth

0 comments on commit 367be2f

Please sign in to comment.
Something went wrong with that request. Please try again.