Adding change email functionality

remotes/origin/enhancement/email-actions
Alejandro Alonso 2014-08-04 10:15:16 +02:00
parent 4d67615248
commit 3e9c836c7f
12 changed files with 427 additions and 14 deletions

View File

@ -12,8 +12,6 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django_jinja import library from django_jinja import library
from django_sites import get_by_id as get_site_by_id from django_sites import get_by_id as get_site_by_id
@ -23,6 +21,7 @@ urls = {
"login": "/login", "login": "/login",
"change-password": "/change-password/{0}", "change-password": "/change-password/{0}",
"change-email": "/change-email/{0}",
"invitation": "/invitation/{0}", "invitation": "/invitation/{0}",
"backlog": "/project/{0}/backlog/", "backlog": "/project/{0}/backlog/",

View File

@ -48,7 +48,7 @@ class UserAdmin(DjangoUserAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}), (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}),
(_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags')}), (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}),
(_('Permissions'), {'fields': ('is_active', 'is_superuser',)}), (_('Permissions'), {'fields': ('is_active', 'is_superuser',)}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
) )

View File

@ -19,26 +19,22 @@ import uuid
from django.db.models.loading import get_model from django.db.models.loading import get_model
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.contrib.auth import logout, login, authenticate
from django.contrib.auth.hashers import make_password
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from easy_thumbnails.exceptions import InvalidImageFormatError
from easy_thumbnails.source_generators import pil_image from easy_thumbnails.source_generators import pil_image
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework import status from rest_framework import status
from djmail.template_mail import MagicMailBuilder from djmail.template_mail import MagicMailBuilder
from taiga.base.decorators import list_route, detail_route from taiga.base.decorators import list_route, detail_route
from taiga.base.decorators import action
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet
from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.slug import slugify_uniquely
from taiga.projects.votes import services as votes_service from taiga.projects.votes import services as votes_service
from taiga.projects.serializers import StarredSerializer from taiga.projects.serializers import StarredSerializer
@ -208,6 +204,67 @@ class UsersViewSet(ModelCrudViewSet):
stars_data = StarredSerializer(stars, many=True) stars_data = StarredSerializer(stars, many=True)
return Response(stars_data.data) return Response(stars_data.data)
#TODO: commit_on_success
def partial_update(self, request, *args, **kwargs):
"""
We must detect if the user is trying to change his email so we can
save that value and generate a token that allows him to validate it in
the new email account
"""
user = self.get_object()
self.check_permissions(request, "update", user)
ret = super(UsersViewSet, self).partial_update(request, *args, **kwargs)
new_email = request.DATA.get('email', None)
if new_email is not None:
valid_new_email = True
duplicated_email = models.User.objects.filter(email = new_email).exists()
try:
validate_email(new_email)
except ValidationError:
valid_new_email = False
valid_new_email = valid_new_email and new_email != request.user.email
if duplicated_email:
raise exc.WrongArguments(_("Duplicated email"))
elif not valid_new_email:
raise exc.WrongArguments(_("Not valid email"))
#We need to generate a token for the email
request.user.email_token = str(uuid.uuid1())
request.user.new_email = new_email
request.user.save(update_fields=["email_token", "new_email"])
mbuilder = MagicMailBuilder()
email = mbuilder.change_email(request.user.email, {"user": request.user})
email.send()
return ret
@list_route(methods=["POST"])
def change_email(self, request, pk=None):
"""
Verify the email change to current logged user.
"""
serializer = serializers.ChangeEmailSerializer(data=request.DATA, many=False)
if not serializer.is_valid():
raise exc.WrongArguments(_("Token is invalid"))
try:
user = models.User.objects.get(email_token=serializer.data["email_token"])
except models.User.DoesNotExist:
raise exc.WrongArguments(_("Token is invalid"))
self.check_permissions(request, "change_email", user)
user.email = user.new_email
user.new_email = None
user.email_token = None
user.save(update_fields=["email", "new_email", "email_token"])
return Response(status=status.HTTP_204_NO_CONTENT)
def destroy(self, request, pk=None): def destroy(self, request, pk=None):
user = self.get_object() user = self.get_object()
self.check_permissions(request, "destroy", user) self.check_permissions(request, "destroy", user)

View File

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as 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 'User.email_token'
db.add_column('users_user', 'email_token',
self.gf('django.db.models.fields.CharField')(default=None, blank=True, null=True, max_length=200),
keep_default=False)
# Adding field 'User.new_email'
db.add_column('users_user', 'new_email',
self.gf('django.db.models.fields.EmailField')(blank=True, null=True, max_length=75),
keep_default=False)
def backwards(self, orm):
# Deleting field 'User.email_token'
db.delete_column('users_user', 'email_token')
# Deleting field 'User.new_email'
db.delete_column('users_user', 'new_email')
models = {
'projects.issuestatus': {
'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'IssueStatus', 'unique_together': "(('project', 'name'),)"},
'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'issue_statuses'"})
},
'projects.issuetype': {
'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'IssueType', 'unique_together': "(('project', 'name'),)"},
'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'issue_types'"})
},
'projects.membership': {
'Meta': {'ordering': "['project', 'user__full_name', 'user__username', 'user__email', 'email']", 'object_name': 'Membership', 'unique_together': "(('user', 'project'),)"},
'created_at': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'default': 'datetime.datetime.now', 'auto_now_add': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'blank': 'True', 'null': 'True', 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'invited_by_id': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True'}),
'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'memberships'"}),
'role': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.Role']", 'related_name': "'memberships'"}),
'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'blank': 'True', 'null': 'True', 'max_length': '60'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.User']", 'default': 'None', 'blank': 'True', 'null': 'True', 'related_name': "'memberships'"})
},
'projects.points': {
'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Points', 'unique_together': "(('project', 'name'),)"},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'points'"}),
'value': ('django.db.models.fields.FloatField', [], {'default': 'None', 'blank': 'True', 'null': 'True'})
},
'projects.priority': {
'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Priority', 'unique_together': "(('project', 'name'),)"},
'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'priorities'"})
},
'projects.project': {
'Meta': {'ordering': "['name']", 'object_name': 'Project'},
'anon_permissions': ('djorm_pgarray.fields.TextArrayField', [], {'default': '[]', 'blank': 'True', 'null': 'True', 'dbtype': "'text'"}),
'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}),
'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.ProjectTemplate']", 'default': 'None', 'blank': 'True', 'null': 'True', 'related_name': "'projects'"}),
'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL', 'related_name': "'+'", 'to': "orm['projects.IssueStatus']", 'unique': 'True'}),
'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL', 'related_name': "'+'", 'to': "orm['projects.IssueType']", 'unique': 'True'}),
'default_points': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL', 'related_name': "'+'", 'to': "orm['projects.Points']", 'unique': 'True'}),
'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL', 'related_name': "'+'", 'to': "orm['projects.Priority']", 'unique': 'True'}),
'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL', 'related_name': "'+'", 'to': "orm['projects.Severity']", 'unique': 'True'}),
'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL', 'related_name': "'+'", 'to': "orm['projects.TaskStatus']", 'unique': 'True'}),
'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'null': 'True', 'on_delete': 'models.SET_NULL', 'related_name': "'+'", 'to': "orm['projects.UserStoryStatus']", 'unique': 'True'}),
'description': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_backlog_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_issues_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_kanban_activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_wiki_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['users.User']", 'symmetrical': 'False', 'through': "orm['projects.Membership']", 'related_name': "'projects'"}),
'modified_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '250'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.User']", 'related_name': "'owned_projects'"}),
'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'public_permissions': ('djorm_pgarray.fields.TextArrayField', [], {'default': '[]', 'blank': 'True', 'null': 'True', 'dbtype': "'text'"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'blank': 'True', 'unique': 'True'}),
'tags': ('djorm_pgarray.fields.TextArrayField', [], {'default': 'None', 'blank': 'True', 'null': 'True', 'dbtype': "'text'"}),
'total_milestones': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True', 'null': 'True'}),
'total_story_points': ('django.db.models.fields.FloatField', [], {'default': '0'}),
'videoconferences': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '250'}),
'videoconferences_salt': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '250'})
},
'projects.projecttemplate': {
'Meta': {'ordering': "['name']", 'object_name': 'ProjectTemplate'},
'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}),
'default_options': ('django_pgjson.fields.JsonField', [], {}),
'default_owner_role': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'description': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_backlog_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_issues_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_kanban_activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_wiki_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'issue_statuses': ('django_pgjson.fields.JsonField', [], {}),
'issue_types': ('django_pgjson.fields.JsonField', [], {}),
'modified_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '250'}),
'points': ('django_pgjson.fields.JsonField', [], {}),
'priorities': ('django_pgjson.fields.JsonField', [], {}),
'roles': ('django_pgjson.fields.JsonField', [], {}),
'severities': ('django_pgjson.fields.JsonField', [], {}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'blank': 'True', 'unique': 'True'}),
'task_statuses': ('django_pgjson.fields.JsonField', [], {}),
'us_statuses': ('django_pgjson.fields.JsonField', [], {}),
'videoconferences': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '250'}),
'videoconferences_salt': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '250'})
},
'projects.severity': {
'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Severity', 'unique_together': "(('project', 'name'),)"},
'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'severities'"})
},
'projects.taskstatus': {
'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'TaskStatus', 'unique_together': "(('project', 'name'),)"},
'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'task_statuses'"})
},
'projects.userstorystatus': {
'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'UserStoryStatus', 'unique_together': "(('project', 'name'),)"},
'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'us_statuses'"}),
'wip_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'blank': 'True', 'null': 'True'})
},
'users.role': {
'Meta': {'ordering': "['order', 'slug']", 'object_name': 'Role', 'unique_together': "(('slug', 'project'),)"},
'computable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'permissions': ('djorm_pgarray.fields.TextArrayField', [], {'default': '[]', 'blank': 'True', 'null': 'True', 'dbtype': "'text'"}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'roles'"}),
'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '250'})
},
'users.user': {
'Meta': {'ordering': "['username']", 'object_name': 'User'},
'bio': ('django.db.models.fields.TextField', [], {'blank': 'True', 'default': "''"}),
'color': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "'#d8b489'", 'max_length': '9'}),
'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'default_language': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '20'}),
'default_timezone': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '20'}),
'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}),
'email_token': ('django.db.models.fields.CharField', [], {'default': 'None', 'blank': 'True', 'null': 'True', 'max_length': '200'}),
'full_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '256'}),
'github_id': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'new_email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'null': 'True', 'max_length': '75'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'photo': ('django.db.models.fields.files.FileField', [], {'blank': 'True', 'null': 'True', 'max_length': '500'}),
'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'blank': 'True', 'null': 'True', 'max_length': '200'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
}
}
complete_apps = ['users']

View File

@ -96,6 +96,12 @@ class User(AbstractBaseUser, PermissionsMixin):
verbose_name=_("colorize tags")) verbose_name=_("colorize tags"))
token = models.CharField(max_length=200, null=True, blank=True, default=None, token = models.CharField(max_length=200, null=True, blank=True, default=None,
verbose_name=_("token")) verbose_name=_("token"))
email_token = models.CharField(max_length=200, null=True, blank=True, default=None,
verbose_name=_("email token"))
new_email = models.EmailField(_('new email address'), null=True, blank=True)
github_id = models.IntegerField(null=True, blank=True, verbose_name=_("github ID")) github_id = models.IntegerField(null=True, blank=True, verbose_name=_("github ID"))
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'

View File

@ -38,3 +38,4 @@ class UserPermission(ResourcePermission):
change_avatar_perms = IsAuthenticated() change_avatar_perms = IsAuthenticated()
remove_avatar_perms = IsAuthenticated() remove_avatar_perms = IsAuthenticated()
starred_perms = AllowAny() starred_perms = AllowAny()
change_email_perms = IsTheSameUser()

View File

@ -14,8 +14,6 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from .models import User from .models import User
@ -47,3 +45,6 @@ class UserSerializer(serializers.ModelSerializer):
class RecoverySerializer(serializers.Serializer): class RecoverySerializer(serializers.Serializer):
token = serializers.CharField(max_length=200) token = serializers.CharField(max_length=200)
password = serializers.CharField(min_length=6) password = serializers.CharField(min_length=6)
class ChangeEmailSerializer(serializers.Serializer):
email_token = serializers.CharField(max_length=200)

View File

@ -0,0 +1,27 @@
{% extends "emails/base.jinja" %}
{% set final_url = resolve_front_url("change-email", user.email_token) %}
{% set final_url_name = "Taiga - Change email" %}
{% block body %}
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
<tr>
<td>
<h1>Change your email:</h1>
<p>Hello {{ user.get_full_name() }},</p>
<p>you can recover your password by going to the following url:</p>
<p><a style="color: #669900;" href="{{ final_url }}">{{ final_url }}</a>
<p>You can ignore this message if you did not request.</p>
<p>Regards</p>
<p>--<br />The Taiga Team</p>
</td>
</tr>
</table>
{% endblock %}
{% block footer %}
<p style="padding: 10px; border-top: 1px solid #eee;">
More info at:
<a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
</p>
{% endblock %}

View File

@ -0,0 +1,12 @@
Hello {{ user.get_full_name() }},
you can confirm your change of email by going to the following url:
{{ resolve_front_url('change-email', user.email_token) }}
You can ignore this message if you did not request.
Regards
--
The Taiga Team

View File

@ -0,0 +1 @@
[Taiga] Change email

View File

@ -4,8 +4,6 @@ from django.core.urlresolvers import reverse
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from taiga.users.serializers import UserSerializer from taiga.users.serializers import UserSerializer
from taiga.users.models import User
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from tests import factories as f from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
@ -232,7 +230,7 @@ def test_user_action_change_password_from_recovery(client, data):
def test_user_action_password_recovery(client, data): def test_user_action_password_recovery(client, data):
url = reverse('users-password-recovery') url = reverse('users-password-recovery')
new_user = f.UserFactory.create(username="test") f.UserFactory.create(username="test")
users = [ users = [
None, None,
@ -244,3 +242,20 @@ def test_user_action_password_recovery(client, data):
patch_data = json.dumps({"username": "test"}) patch_data = json.dumps({"username": "test"})
results = helper_test_http_method(client, 'post', url, patch_data, users) results = helper_test_http_method(client, 'post', url, patch_data, users)
assert results == [200, 200, 200, 200] assert results == [200, 200, 200, 200]
def test_user_action_change_email(client, data):
url = reverse('users-change-email')
data.registered_user.email_token = "test-token"
data.registered_user.new_email = "new@email.com"
data.registered_user.save()
users = [
None,
data.registered_user,
data.other_user,
]
patch_data = json.dumps({"email_token": "test-token"})
results = helper_test_http_method(client, 'post', url, patch_data, users)
assert results == [401, 204, 400]

View File

@ -0,0 +1,100 @@
import pytest
import json
from django.core.urlresolvers import reverse
from .. import factories as f
from taiga.users import models
pytestmark = pytest.mark.django_db
def test_api_user_patch_same_email(client):
user = f.UserFactory.create(email="same@email.com")
url = reverse('users-detail', kwargs={"pk": user.pk})
data = {"email": "same@email.com"}
client.login(user)
response = client.patch(url, json.dumps(data), content_type="application/json")
assert response.status_code == 400
assert response.data['_error_message'] == 'Duplicated email'
def test_api_user_patch_duplicated_email(client):
f.UserFactory.create(email="one@email.com")
user = f.UserFactory.create(email="two@email.com")
url = reverse('users-detail', kwargs={"pk": user.pk})
data = {"email": "one@email.com"}
client.login(user)
response = client.patch(url, json.dumps(data), content_type="application/json")
assert response.status_code == 400
assert response.data['_error_message'] == 'Duplicated email'
def test_api_user_patch_invalid_email(client):
user = f.UserFactory.create(email="my@email.com")
url = reverse('users-detail', kwargs={"pk": user.pk})
data = {"email": "my@email"}
client.login(user)
response = client.patch(url, json.dumps(data), content_type="application/json")
assert response.status_code == 400
assert response.data['_error_message'] == 'Not valid email'
def test_api_user_patch_valid_email(client):
user = f.UserFactory.create(email="old@email.com")
url = reverse('users-detail', kwargs={"pk": user.pk})
data = {"email": "new@email.com"}
client.login(user)
response = client.patch(url, json.dumps(data), content_type="application/json")
assert response.status_code == 200
user = models.User.objects.get(pk=user.id)
assert user.email_token != None
assert user.new_email == "new@email.com"
def test_api_user_action_change_email_ok(client):
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
url = reverse('users-change-email')
data = {"email_token": "change_email_token"}
client.login(user)
response = client.post(url, json.dumps(data), content_type="application/json")
assert response.status_code == 204
user = models.User.objects.get(pk=user.id)
assert user.email_token == None
assert user.new_email == None
assert user.email == "new@email.com"
def test_api_user_action_change_email_no_token(client):
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
url = reverse('users-change-email')
data = {}
client.login(user)
response = client.post(url, json.dumps(data), content_type="application/json")
assert response.status_code == 400
assert response.data['_error_message'] == 'Token is invalid'
def test_api_user_action_change_email_invalid_token(client):
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
url = reverse('users-change-email')
data = {"email_token": "invalid_email_token"}
client.login(user)
response = client.post(url, json.dumps(data), content_type="application/json")
assert response.status_code == 400
assert response.data['_error_message'] == 'Token is invalid'