Adding change email functionality
parent
4d67615248
commit
3e9c836c7f
|
@ -12,8 +12,6 @@
|
|||
# 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/>.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from django_jinja import library
|
||||
from django_sites import get_by_id as get_site_by_id
|
||||
|
||||
|
@ -23,6 +21,7 @@ urls = {
|
|||
|
||||
"login": "/login",
|
||||
"change-password": "/change-password/{0}",
|
||||
"change-email": "/change-email/{0}",
|
||||
"invitation": "/invitation/{0}",
|
||||
|
||||
"backlog": "/project/{0}/backlog/",
|
||||
|
|
|
@ -48,7 +48,7 @@ class UserAdmin(DjangoUserAdmin):
|
|||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
(_('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',)}),
|
||||
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
|
|
|
@ -19,26 +19,22 @@ import uuid
|
|||
from django.db.models.loading import get_model
|
||||
from django.db.models import Q
|
||||
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.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 rest_framework.response import Response
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework import status
|
||||
|
||||
from djmail.template_mail import MagicMailBuilder
|
||||
|
||||
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.api import ModelCrudViewSet
|
||||
from taiga.base.api import ModelListViewSet
|
||||
from taiga.base.utils.slug import slugify_uniquely
|
||||
from taiga.projects.votes import services as votes_service
|
||||
from taiga.projects.serializers import StarredSerializer
|
||||
|
@ -208,6 +204,67 @@ class UsersViewSet(ModelCrudViewSet):
|
|||
stars_data = StarredSerializer(stars, many=True)
|
||||
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):
|
||||
user = self.get_object()
|
||||
self.check_permissions(request, "destroy", user)
|
||||
|
|
|
@ -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']
|
|
@ -96,6 +96,12 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||
verbose_name=_("colorize tags"))
|
||||
token = models.CharField(max_length=200, null=True, blank=True, default=None,
|
||||
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"))
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
|
|
|
@ -38,3 +38,4 @@ class UserPermission(ResourcePermission):
|
|||
change_avatar_perms = IsAuthenticated()
|
||||
remove_avatar_perms = IsAuthenticated()
|
||||
starred_perms = AllowAny()
|
||||
change_email_perms = IsTheSameUser()
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
# 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/>.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import User
|
||||
|
@ -47,3 +45,6 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
class RecoverySerializer(serializers.Serializer):
|
||||
token = serializers.CharField(max_length=200)
|
||||
password = serializers.CharField(min_length=6)
|
||||
|
||||
class ChangeEmailSerializer(serializers.Serializer):
|
||||
email_token = serializers.CharField(max_length=200)
|
||||
|
|
|
@ -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 %}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
[Taiga] Change email
|
|
@ -4,8 +4,6 @@ from django.core.urlresolvers import reverse
|
|||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
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.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):
|
||||
url = reverse('users-password-recovery')
|
||||
|
||||
new_user = f.UserFactory.create(username="test")
|
||||
f.UserFactory.create(username="test")
|
||||
|
||||
users = [
|
||||
None,
|
||||
|
@ -244,3 +242,20 @@ def test_user_action_password_recovery(client, data):
|
|||
patch_data = json.dumps({"username": "test"})
|
||||
results = helper_test_http_method(client, 'post', url, patch_data, users)
|
||||
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]
|
||||
|
|
|
@ -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'
|
Loading…
Reference in New Issue