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
|
# 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/",
|
||||||
|
|
|
@ -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')}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"))
|
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'
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 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]
|
||||||
|
|
|
@ -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