Merge branch 'master' into stable
Conflicts: tests/integration/test_userstories.pyremotes/origin/enhancement/email-actions 1.1.1
commit
bd519b3d64
|
@ -1,5 +1,14 @@
|
||||||
# Changelog #
|
# Changelog #
|
||||||
|
|
||||||
|
## 1.2.0 (Unreleased)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Send an email to the user on signup.
|
||||||
|
- Emit django signal on user signout.
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
- Lots of small and not so small bugfixes
|
||||||
|
|
||||||
## 1.1.0 (2014-10-13)
|
## 1.1.0 (2014-10-13)
|
||||||
|
|
||||||
### Misc
|
### Misc
|
||||||
|
|
|
@ -2,7 +2,7 @@ djangorestframework==2.3.13
|
||||||
Django==1.7
|
Django==1.7
|
||||||
django-picklefield==0.3.1
|
django-picklefield==0.3.1
|
||||||
django-sampledatahelper==0.2.2
|
django-sampledatahelper==0.2.2
|
||||||
gunicorn==18.0
|
gunicorn==19.1.1
|
||||||
psycopg2==2.5.4
|
psycopg2==2.5.4
|
||||||
pillow==2.5.3
|
pillow==2.5.3
|
||||||
pytz==2014.4
|
pytz==2014.4
|
||||||
|
|
|
@ -271,6 +271,9 @@ AUTHENTICATION_BACKENDS = (
|
||||||
"django.contrib.auth.backends.ModelBackend", # default
|
"django.contrib.auth.backends.ModelBackend", # default
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MAX_AGE_AUTH_TOKEN = None
|
||||||
|
MAX_AGE_CANCEL_ACCOUNT = 30 * 24 * 60 * 60 # 30 days in seconds
|
||||||
|
|
||||||
ANONYMOUS_USER_ID = -1
|
ANONYMOUS_USER_ID = -1
|
||||||
|
|
||||||
MAX_SEARCH_RESULTS = 100
|
MAX_SEARCH_RESULTS = 100
|
||||||
|
|
|
@ -35,11 +35,10 @@ fraudulent modifications.
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.core import signing
|
from django.conf import settings
|
||||||
from django.apps import apps
|
|
||||||
from rest_framework.authentication import BaseAuthentication
|
from rest_framework.authentication import BaseAuthentication
|
||||||
from taiga.base import exceptions as exc
|
|
||||||
|
|
||||||
|
from .tokens import get_user_for_token
|
||||||
|
|
||||||
class Session(BaseAuthentication):
|
class Session(BaseAuthentication):
|
||||||
"""
|
"""
|
||||||
|
@ -62,39 +61,6 @@ class Session(BaseAuthentication):
|
||||||
return (user, None)
|
return (user, None)
|
||||||
|
|
||||||
|
|
||||||
def get_token_for_user(user):
|
|
||||||
"""
|
|
||||||
Generate a new signed token containing
|
|
||||||
a specified user.
|
|
||||||
"""
|
|
||||||
data = {"user_id": user.id}
|
|
||||||
return signing.dumps(data)
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_for_token(token):
|
|
||||||
"""
|
|
||||||
Given a selfcontained token, try parse and
|
|
||||||
unsign it.
|
|
||||||
|
|
||||||
If token passes a validation, returns
|
|
||||||
a user instance corresponding with user_id stored
|
|
||||||
in the incoming token.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
data = signing.loads(token)
|
|
||||||
except signing.BadSignature:
|
|
||||||
raise exc.NotAuthenticated("Invalid token")
|
|
||||||
|
|
||||||
model_cls = apps.get_model("users", "User")
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = model_cls.objects.get(pk=data["user_id"])
|
|
||||||
except model_cls.DoesNotExist:
|
|
||||||
raise exc.NotAuthenticated("Invalid token")
|
|
||||||
else:
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
class Token(BaseAuthentication):
|
class Token(BaseAuthentication):
|
||||||
"""
|
"""
|
||||||
Self-contained stateles authentication implementatrion
|
Self-contained stateles authentication implementatrion
|
||||||
|
@ -114,7 +80,10 @@ class Token(BaseAuthentication):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
token = token_rx_match.group(1)
|
token = token_rx_match.group(1)
|
||||||
user = get_user_for_token(token)
|
max_age_auth_token = getattr(settings, "MAX_AGE_AUTH_TOKEN", None)
|
||||||
|
user = get_user_for_token(token, "authentication",
|
||||||
|
max_age=max_age_auth_token)
|
||||||
|
|
||||||
return (user, token)
|
return (user, token)
|
||||||
|
|
||||||
def authenticate_header(self, request):
|
def authenticate_header(self, request):
|
||||||
|
|
|
@ -35,31 +35,18 @@ from taiga.base import exceptions as exc
|
||||||
from taiga.users.serializers import UserSerializer
|
from taiga.users.serializers import UserSerializer
|
||||||
from taiga.users.services import get_and_validate_user
|
from taiga.users.services import get_and_validate_user
|
||||||
|
|
||||||
from .backends import get_token_for_user
|
from .tokens import get_token_for_user
|
||||||
from .signals import user_registered as user_registered_signal
|
from .signals import user_registered as user_registered_signal
|
||||||
|
|
||||||
def send_public_register_email(user) -> bool:
|
def send_register_email(user) -> bool:
|
||||||
"""
|
"""
|
||||||
Given a user, send public register welcome email
|
Given a user, send register welcome email
|
||||||
message to specified user.
|
message to specified user.
|
||||||
"""
|
"""
|
||||||
|
cancel_token = get_token_for_user(user, "cancel_account")
|
||||||
context = {"user": user}
|
context = {"user": user, "cancel_token": cancel_token}
|
||||||
mbuilder = MagicMailBuilder()
|
mbuilder = MagicMailBuilder()
|
||||||
email = mbuilder.public_register_user(user.email, context)
|
email = mbuilder.registered_user(user.email, context)
|
||||||
return bool(email.send())
|
|
||||||
|
|
||||||
|
|
||||||
def send_private_register_email(user, **kwargs) -> bool:
|
|
||||||
"""
|
|
||||||
Given a user, send private register welcome
|
|
||||||
email message to specified user.
|
|
||||||
"""
|
|
||||||
context = {"user": user}
|
|
||||||
context.update(kwargs)
|
|
||||||
|
|
||||||
mbuilder = MagicMailBuilder()
|
|
||||||
email = mbuilder.private_register_user(user.email, context)
|
|
||||||
return bool(email.send())
|
return bool(email.send())
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,7 +112,7 @@ def public_register(username:str, password:str, email:str, full_name:str):
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise exc.WrongArguments("User is already register.")
|
raise exc.WrongArguments("User is already register.")
|
||||||
|
|
||||||
# send_public_register_email(user)
|
send_register_email(user)
|
||||||
user_registered_signal.send(sender=user.__class__, user=user)
|
user_registered_signal.send(sender=user.__class__, user=user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
@ -149,7 +136,7 @@ def private_register_for_existing_user(token:str, username:str, password:str):
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise exc.IntegrityError("Membership with user is already exists.")
|
raise exc.IntegrityError("Membership with user is already exists.")
|
||||||
|
|
||||||
# send_private_register_email(user)
|
send_register_email(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@ -178,6 +165,7 @@ def private_register_for_new_user(token:str, username:str, email:str,
|
||||||
membership = get_membership_by_token(token)
|
membership = get_membership_by_token(token)
|
||||||
membership.user = user
|
membership.user = user
|
||||||
membership.save(update_fields=["user"])
|
membership.save(update_fields=["user"])
|
||||||
|
send_register_email(user)
|
||||||
user_registered_signal.send(sender=user.__class__, user=user)
|
user_registered_signal.send(sender=user.__class__, user=user)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
@ -205,6 +193,7 @@ def github_register(username:str, email:str, full_name:str, github_id:int, bio:s
|
||||||
membership.save(update_fields=["user"])
|
membership.save(update_fields=["user"])
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
|
send_register_email(user)
|
||||||
user_registered_signal.send(sender=user.__class__, user=user)
|
user_registered_signal.send(sender=user.__class__, user=user)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
@ -218,5 +207,5 @@ def make_auth_response_data(user) -> dict:
|
||||||
"""
|
"""
|
||||||
serializer = UserSerializer(user)
|
serializer = UserSerializer(user)
|
||||||
data = dict(serializer.data)
|
data = dict(serializer.data)
|
||||||
data["auth_token"] = get_token_for_user(user)
|
data["auth_token"] = get_token_for_user(user, "authentication")
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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 taiga.base import exceptions as exc
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.core import signing
|
||||||
|
|
||||||
|
def get_token_for_user(user, scope):
|
||||||
|
"""
|
||||||
|
Generate a new signed token containing
|
||||||
|
a specified user limited for a scope (identified as a string).
|
||||||
|
"""
|
||||||
|
data = {"user_%s_id"%(scope): user.id}
|
||||||
|
return signing.dumps(data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_for_token(token, scope, max_age=None):
|
||||||
|
"""
|
||||||
|
Given a selfcontained token and a scope try to parse and
|
||||||
|
unsign it.
|
||||||
|
|
||||||
|
If max_age is specified it checks token expiration.
|
||||||
|
|
||||||
|
If token passes a validation, returns
|
||||||
|
a user instance corresponding with user_id stored
|
||||||
|
in the incoming token.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = signing.loads(token, max_age=max_age)
|
||||||
|
except signing.BadSignature:
|
||||||
|
raise exc.NotAuthenticated("Invalid token")
|
||||||
|
|
||||||
|
model_cls = apps.get_model("users", "User")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = model_cls.objects.get(pk=data["user_%s_id"%(scope)])
|
||||||
|
except (model_cls.DoesNotExist, KeyError):
|
||||||
|
raise exc.NotAuthenticated("Invalid token")
|
||||||
|
else:
|
||||||
|
return user
|
|
@ -246,8 +246,11 @@ class QFilter(FilterBackend):
|
||||||
def filter_queryset(self, request, queryset, view):
|
def filter_queryset(self, request, queryset, view):
|
||||||
q = request.QUERY_PARAMS.get('q', None)
|
q = request.QUERY_PARAMS.get('q', None)
|
||||||
if q:
|
if q:
|
||||||
qs_args = [Q(subject__icontains=x) for x in q.split()]
|
if q.isdigit():
|
||||||
qs_args += [Q(ref=x) for x in q.split() if x.isdigit()]
|
qs_args = [Q(ref=q)]
|
||||||
queryset = queryset.filter(reduce(operator.or_, qs_args))
|
else:
|
||||||
|
qs_args = [Q(subject__icontains=x) for x in q.split()]
|
||||||
|
|
||||||
|
queryset = queryset.filter(reduce(operator.and_, qs_args))
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
|
@ -87,7 +87,7 @@ def get_neighbors(obj, results_set=None):
|
||||||
|
|
||||||
:return: Tuple `<left neighbor>, <right neighbor>`. Left and right neighbors can be `None`.
|
:return: Tuple `<left neighbor>, <right neighbor>`. Left and right neighbors can be `None`.
|
||||||
"""
|
"""
|
||||||
if results_set is None:
|
if results_set is None or results_set.count() == 0:
|
||||||
results_set = type(obj).objects.get_queryset()
|
results_set = type(obj).objects.get_queryset()
|
||||||
try:
|
try:
|
||||||
left = _left_candidates(obj, results_set).reverse()[0]
|
left = _left_candidates(obj, results_set).reverse()[0]
|
||||||
|
|
|
@ -296,6 +296,23 @@ class MilestoneExportSerializer(serializers.ModelSerializer):
|
||||||
watchers = UserRelatedField(many=True, required=False)
|
watchers = UserRelatedField(many=True, required=False)
|
||||||
modified_date = serializers.DateTimeField(required=False)
|
modified_date = serializers.DateTimeField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
project = kwargs.pop('project', None)
|
||||||
|
super(MilestoneExportSerializer, self).__init__(*args, **kwargs)
|
||||||
|
if project:
|
||||||
|
self.project = project
|
||||||
|
|
||||||
|
def validate_name(self, attrs, source):
|
||||||
|
"""
|
||||||
|
Check the milestone name is not duplicated in the project
|
||||||
|
"""
|
||||||
|
name = attrs[source]
|
||||||
|
qs = self.project.milestones.filter(name=name)
|
||||||
|
if qs.exists():
|
||||||
|
raise serializers.ValidationError("Name duplicated for the project")
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = milestones_models.Milestone
|
model = milestones_models.Milestone
|
||||||
exclude = ('id', 'project')
|
exclude = ('id', 'project')
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
import os.path as path
|
import os.path as path
|
||||||
|
from unidecode import unidecode
|
||||||
|
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from taiga.projects.history.services import make_key_from_model_object
|
from taiga.projects.history.services import make_key_from_model_object
|
||||||
|
@ -183,7 +185,7 @@ def store_task(project, task):
|
||||||
|
|
||||||
|
|
||||||
def store_milestone(project, milestone):
|
def store_milestone(project, milestone):
|
||||||
serialized = serializers.MilestoneExportSerializer(data=milestone)
|
serialized = serializers.MilestoneExportSerializer(data=milestone, project=project)
|
||||||
if serialized.is_valid():
|
if serialized.is_valid():
|
||||||
serialized.object.project = project
|
serialized.object.project = project
|
||||||
serialized.object._importing = True
|
serialized.object._importing = True
|
||||||
|
@ -229,6 +231,7 @@ def store_history(project, obj, history):
|
||||||
|
|
||||||
|
|
||||||
def store_wiki_page(project, wiki_page):
|
def store_wiki_page(project, wiki_page):
|
||||||
|
wiki_page['slug'] = slugify(unidecode(wiki_page.get('slug', '')))
|
||||||
serialized = serializers.WikiPageExportSerializer(data=wiki_page)
|
serialized = serializers.WikiPageExportSerializer(data=wiki_page)
|
||||||
if serialized.is_valid():
|
if serialized.is_valid():
|
||||||
serialized.object.project = project
|
serialized.object.project = project
|
||||||
|
|
|
@ -22,6 +22,7 @@ urls = {
|
||||||
"login": "/login",
|
"login": "/login",
|
||||||
"change-password": "/change-password/{0}",
|
"change-password": "/change-password/{0}",
|
||||||
"change-email": "/change-email/{0}",
|
"change-email": "/change-email/{0}",
|
||||||
|
"cancel-account": "/cancel-account/{0}",
|
||||||
"invitation": "/invitation/{0}",
|
"invitation": "/invitation/{0}",
|
||||||
|
|
||||||
"project": "/project/{0}",
|
"project": "/project/{0}",
|
||||||
|
|
|
@ -52,13 +52,16 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
super().pre_conditions_on_save(obj)
|
super().pre_conditions_on_save(obj)
|
||||||
|
|
||||||
if obj.milestone and obj.milestone.project != obj.project:
|
if obj.milestone and obj.milestone.project != obj.project:
|
||||||
raise exc.PermissionDenied(_("You don't have permissions for add/modify this task."))
|
raise exc.WrongArguments(_("You don't have permissions for add/modify this task."))
|
||||||
|
|
||||||
if obj.user_story and obj.user_story.project != obj.project:
|
if obj.user_story and obj.user_story.project != obj.project:
|
||||||
raise exc.PermissionDenied(_("You don't have permissions for add/modify this task."))
|
raise exc.WrongArguments(_("You don't have permissions for add/modify this task."))
|
||||||
|
|
||||||
if obj.status and obj.status.project != obj.project:
|
if obj.status and obj.status.project != obj.project:
|
||||||
raise exc.PermissionDenied(_("You don't have permissions for add/modify this task."))
|
raise exc.WrongArguments(_("You don't have permissions for add/modify this task."))
|
||||||
|
|
||||||
|
if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone:
|
||||||
|
raise exc.WrongArguments(_("You don't have permissions for add/modify this task."))
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_create(self, request, **kwargs):
|
def bulk_create(self, request, **kwargs):
|
||||||
|
|
|
@ -22,6 +22,7 @@ from django.shortcuts import get_object_or_404
|
||||||
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.validators import validate_email
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from easy_thumbnails.source_generators import pil_image
|
from easy_thumbnails.source_generators import pil_image
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ from rest_framework import status
|
||||||
|
|
||||||
from djmail.template_mail import MagicMailBuilder
|
from djmail.template_mail import MagicMailBuilder
|
||||||
|
|
||||||
|
from taiga.auth.tokens import get_user_for_token
|
||||||
from taiga.base.decorators import list_route, detail_route
|
from taiga.base.decorators import list_route, detail_route
|
||||||
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
|
||||||
|
@ -42,6 +44,7 @@ from taiga.projects.serializers import StarredSerializer
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
from .signals import user_cancel_account as user_cancel_account_signal
|
||||||
|
|
||||||
|
|
||||||
class MembersFilterBackend(BaseFilterBackend):
|
class MembersFilterBackend(BaseFilterBackend):
|
||||||
|
@ -258,20 +261,34 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@list_route(methods=["POST"])
|
||||||
|
def cancel(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
Cancel an account via token
|
||||||
|
"""
|
||||||
|
serializer = serializers.CancelAccountSerializer(data=request.DATA, many=False)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None)
|
||||||
|
user = get_user_for_token(serializer.data["cancel_token"], "cancel_account",
|
||||||
|
max_age=max_age_cancel_account)
|
||||||
|
|
||||||
|
except exc.NotAuthenticated:
|
||||||
|
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
|
||||||
|
|
||||||
|
if not user.is_active:
|
||||||
|
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
|
||||||
|
|
||||||
|
user.cancel()
|
||||||
|
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)
|
||||||
user.username = slugify_uniquely("deleted-user", models.User, slugfield="username")
|
stream = request.stream
|
||||||
user.email = "{}@taiga.io".format(user.username)
|
request_data = stream is not None and stream.GET or None
|
||||||
user.is_active = False
|
user_cancel_account_signal.send(sender=user.__class__, user=user, request_data=request_data)
|
||||||
user.full_name = "Deleted user"
|
user.cancel()
|
||||||
user.color = ""
|
|
||||||
user.bio = ""
|
|
||||||
user.default_language = ""
|
|
||||||
user.default_timezone = ""
|
|
||||||
user.colorize_tags = True
|
|
||||||
user.token = None
|
|
||||||
user.github_id = None
|
|
||||||
user.set_unusable_password()
|
|
||||||
user.save()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
|
@ -19,6 +19,7 @@ import os
|
||||||
import os.path as path
|
import os.path as path
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
from unidecode import unidecode
|
from unidecode import unidecode
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ from django.template.defaultfilters import slugify
|
||||||
|
|
||||||
from djorm_pgarray.fields import TextArrayField
|
from djorm_pgarray.fields import TextArrayField
|
||||||
|
|
||||||
|
from taiga.auth.tokens import get_token_for_user
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
from taiga.base.utils.iterators import split_by_n
|
from taiga.base.utils.iterators import split_by_n
|
||||||
from taiga.permissions.permissions import MEMBERS_PERMISSIONS
|
from taiga.permissions.permissions import MEMBERS_PERMISSIONS
|
||||||
|
@ -152,6 +154,24 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||||
def get_full_name(self):
|
def get_full_name(self):
|
||||||
return self.full_name or self.username or self.email
|
return self.full_name or self.username or self.email
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
get_token_for_user(self, "cancel_account")
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self.username = slugify_uniquely("deleted-user", User, slugfield="username")
|
||||||
|
self.email = "{}@taiga.io".format(self.username)
|
||||||
|
self.is_active = False
|
||||||
|
self.full_name = "Deleted user"
|
||||||
|
self.color = ""
|
||||||
|
self.bio = ""
|
||||||
|
self.default_language = ""
|
||||||
|
self.default_timezone = ""
|
||||||
|
self.colorize_tags = True
|
||||||
|
self.token = None
|
||||||
|
self.github_id = None
|
||||||
|
self.set_unusable_password()
|
||||||
|
self.save()
|
||||||
|
|
||||||
class Role(models.Model):
|
class Role(models.Model):
|
||||||
name = models.CharField(max_length=200, null=False, blank=False,
|
name = models.CharField(max_length=200, null=False, blank=False,
|
||||||
|
|
|
@ -69,3 +69,7 @@ class RecoverySerializer(serializers.Serializer):
|
||||||
|
|
||||||
class ChangeEmailSerializer(serializers.Serializer):
|
class ChangeEmailSerializer(serializers.Serializer):
|
||||||
email_token = serializers.CharField(max_length=200)
|
email_token = serializers.CharField(max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
class CancelAccountSerializer(serializers.Serializer):
|
||||||
|
cancel_token = serializers.CharField(max_length=200)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import django.dispatch
|
||||||
|
|
||||||
|
|
||||||
|
user_cancel_account = django.dispatch.Signal(providing_args=["user", "request_data"])
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "emails/base.jinja" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Welcome to Taiga, an Open Source, Agile Project Management Tool</p>
|
||||||
|
|
||||||
|
<p>You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here:</p>
|
||||||
|
{{ resolve_front_url('cancel-account', cancel_token) }}
|
||||||
|
|
||||||
|
<p>We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done.</p>
|
||||||
|
|
||||||
|
<p>We hope you enjoy it.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
<p style="padding: 10px; border-top: 1px solid #eee;">
|
||||||
|
The Taiga development team.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,12 @@
|
||||||
|
Welcome to Taiga, an Open Source, Agile Project Management Tool
|
||||||
|
|
||||||
|
You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here:
|
||||||
|
|
||||||
|
{{ resolve_front_url('cancel-account', cancel_token) }}
|
||||||
|
|
||||||
|
We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done.
|
||||||
|
|
||||||
|
We hope you enjoy it.
|
||||||
|
|
||||||
|
--
|
||||||
|
The Taiga development team.
|
|
@ -0,0 +1 @@
|
||||||
|
You've been Taigatized!
|
|
@ -20,10 +20,14 @@ from unittest.mock import patch, Mock
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.core import mail
|
||||||
|
|
||||||
from .. import factories
|
from .. import factories
|
||||||
|
|
||||||
from taiga.base.connectors import github
|
from taiga.base.connectors import github
|
||||||
|
from taiga.front import resolve as resolve_front_url
|
||||||
|
from taiga.users import models
|
||||||
|
from taiga.auth.tokens import get_token_for_user
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
@ -68,6 +72,28 @@ def test_respond_201_with_invitation_without_public_registration(client, registe
|
||||||
assert response.status_code == 201, response.data
|
assert response.status_code == 201, response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_response_200_in_public_registration(client, settings):
|
||||||
|
settings.PUBLIC_REGISTER_ENABLED = True
|
||||||
|
form = {
|
||||||
|
"type": "public",
|
||||||
|
"username": "mmcfly",
|
||||||
|
"full_name": "martin seamus mcfly",
|
||||||
|
"email": "mmcfly@bttf.com",
|
||||||
|
"password": "password",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(reverse("auth-register"), form)
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.data["username"] == "mmcfly"
|
||||||
|
assert response.data["email"] == "mmcfly@bttf.com"
|
||||||
|
assert response.data["full_name"] == "martin seamus mcfly"
|
||||||
|
assert len(mail.outbox) == 1
|
||||||
|
assert mail.outbox[0].subject == "You've been Taigatized!"
|
||||||
|
user = models.User.objects.get(username="mmcfly")
|
||||||
|
cancel_token = get_token_for_user(user, "cancel_account")
|
||||||
|
cancel_url = resolve_front_url("cancel-account", cancel_token)
|
||||||
|
assert mail.outbox[0].body.index(cancel_url) > 0
|
||||||
|
|
||||||
def test_response_200_in_registration_with_github_account(client, settings):
|
def test_response_200_in_registration_with_github_account(client, settings):
|
||||||
settings.PUBLIC_REGISTER_ENABLED = False
|
settings.PUBLIC_REGISTER_ENABLED = False
|
||||||
form = {"type": "github",
|
form = {"type": "github",
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
|
||||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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.core.urlresolvers import reverse
|
|
||||||
|
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from .. import factories as f
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
DUMMY_BMP_DATA = b'BM:\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\x04\x00\x00\x00\x13\x0b\x00\x00\x13\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
|
||||||
|
|
||||||
|
|
||||||
def test_change_avatar(client):
|
|
||||||
url = reverse('users-change-avatar')
|
|
||||||
|
|
||||||
user = f.UserFactory()
|
|
||||||
client.login(user)
|
|
||||||
|
|
||||||
with NamedTemporaryFile() as avatar:
|
|
||||||
# Test no avatar send
|
|
||||||
post_data = {}
|
|
||||||
response = client.post(url, post_data)
|
|
||||||
assert response.status_code == 400
|
|
||||||
|
|
||||||
# Test invalid file send
|
|
||||||
post_data = {
|
|
||||||
'avatar': avatar
|
|
||||||
}
|
|
||||||
response = client.post(url, post_data)
|
|
||||||
assert response.status_code == 400
|
|
||||||
|
|
||||||
# Test empty valid avatar send
|
|
||||||
avatar.write(DUMMY_BMP_DATA)
|
|
||||||
avatar.seek(0)
|
|
||||||
response = client.post(url, post_data)
|
|
||||||
assert response.status_code == 200
|
|
|
@ -637,3 +637,21 @@ def test_valid_milestone_import(client):
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
response_data = json.loads(response.content.decode("utf-8"))
|
response_data = json.loads(response.content.decode("utf-8"))
|
||||||
|
|
||||||
|
def test_milestone_import_duplicated_milestone(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user)
|
||||||
|
client.login(user)
|
||||||
|
|
||||||
|
url = reverse("importer-milestone", args=[project.pk])
|
||||||
|
data = {
|
||||||
|
"name": "Imported milestone",
|
||||||
|
"estimated_start": "2014-10-10",
|
||||||
|
"estimated_finish": "2014-10-20",
|
||||||
|
}
|
||||||
|
# We create twice the same milestone
|
||||||
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
assert response.status_code == 400
|
||||||
|
response_data = json.loads(response.content.decode("utf-8"))
|
||||||
|
assert response_data["milestones"][0]["name"][0] == "Name duplicated for the project"
|
||||||
|
|
|
@ -69,3 +69,81 @@ def test_api_filter_by_subject(client):
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert number_of_issues == 1, number_of_issues
|
assert number_of_issues == 1, number_of_issues
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_filter_by_text_1(client):
|
||||||
|
f.create_issue()
|
||||||
|
issue = f.create_issue(subject="this is the issue one")
|
||||||
|
f.create_issue(subject="this is the issue two", owner=issue.owner)
|
||||||
|
url = reverse("issues-list") + "?q=one"
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
response = client.get(url)
|
||||||
|
number_of_issues = len(response.data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert number_of_issues == 1
|
||||||
|
|
||||||
|
def test_api_filter_by_text_2(client):
|
||||||
|
f.create_issue()
|
||||||
|
issue = f.create_issue(subject="this is the issue one")
|
||||||
|
f.create_issue(subject="this is the issue two", owner=issue.owner)
|
||||||
|
url = reverse("issues-list") + "?q=this is the issue one"
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
response = client.get(url)
|
||||||
|
number_of_issues = len(response.data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert number_of_issues == 1
|
||||||
|
|
||||||
|
def test_api_filter_by_text_3(client):
|
||||||
|
f.create_issue()
|
||||||
|
issue = f.create_issue(subject="this is the issue one")
|
||||||
|
f.create_issue(subject="this is the issue two", owner=issue.owner)
|
||||||
|
url = reverse("issues-list") + "?q=this is the issue"
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
response = client.get(url)
|
||||||
|
number_of_issues = len(response.data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert number_of_issues == 2
|
||||||
|
|
||||||
|
def test_api_filter_by_text_4(client):
|
||||||
|
f.create_issue()
|
||||||
|
issue = f.create_issue(subject="this is the issue one")
|
||||||
|
f.create_issue(subject="this is the issue two", owner=issue.owner)
|
||||||
|
url = reverse("issues-list") + "?q=one two"
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
response = client.get(url)
|
||||||
|
number_of_issues = len(response.data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert number_of_issues == 0
|
||||||
|
|
||||||
|
def test_api_filter_by_text_5(client):
|
||||||
|
f.create_issue()
|
||||||
|
issue = f.create_issue(subject="python 3")
|
||||||
|
url = reverse("issues-list") + "?q=python 3"
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
response = client.get(url)
|
||||||
|
number_of_issues = len(response.data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert number_of_issues == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_filter_by_text_6(client):
|
||||||
|
f.create_issue()
|
||||||
|
issue = f.create_issue(subject="test")
|
||||||
|
url = reverse("issues-list") + "?q=%s"%(issue.ref)
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
response = client.get(url)
|
||||||
|
number_of_issues = len(response.data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert number_of_issues == 1
|
||||||
|
|
|
@ -29,8 +29,7 @@ from .. import factories as f
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
def test_update_milestone_with_userstories_list(client):
|
||||||
def test_api_update_milestone(client):
|
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
project = f.ProjectFactory.create(owner=user)
|
project = f.ProjectFactory.create(owner=user)
|
||||||
role = f.RoleFactory.create(project=project)
|
role = f.RoleFactory.create(project=project)
|
||||||
|
@ -39,7 +38,6 @@ def test_api_update_milestone(client):
|
||||||
|
|
||||||
points = f.PointsFactory.create(project=project, value=None)
|
points = f.PointsFactory.create(project=project, value=None)
|
||||||
us = f.UserStoryFactory.create(project=project, owner=user)
|
us = f.UserStoryFactory.create(project=project, owner=user)
|
||||||
# role_points = f.RolePointsFactory.create(points=points, user_story=us, role=role)
|
|
||||||
|
|
||||||
url = reverse("milestones-detail", args=[sprint.pk])
|
url = reverse("milestones-detail", args=[sprint.pk])
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,18 @@ class TestIssues:
|
||||||
assert neighbors.left == issue3
|
assert neighbors.left == issue3
|
||||||
assert neighbors.right == issue1
|
assert neighbors.right == issue1
|
||||||
|
|
||||||
|
def test_empty_related_queryset(self):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
|
||||||
|
issue1 = f.IssueFactory.create(project=project)
|
||||||
|
issue2 = f.IssueFactory.create(project=project)
|
||||||
|
issue3 = f.IssueFactory.create(project=project)
|
||||||
|
|
||||||
|
neighbors = n.get_neighbors(issue2, Issue.objects.none())
|
||||||
|
|
||||||
|
assert neighbors.left == issue3
|
||||||
|
assert neighbors.right == issue1
|
||||||
|
|
||||||
def test_ordering_by_severity(self):
|
def test_ordering_by_severity(self):
|
||||||
project = f.ProjectFactory.create()
|
project = f.ProjectFactory.create()
|
||||||
severity1 = f.SeverityFactory.create(project=project, order=1)
|
severity1 = f.SeverityFactory.create(project=project, order=1)
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import json
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from taiga.base.utils import json
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.wiki.models import WikiPage
|
from taiga.projects.wiki.models import WikiPage
|
||||||
from taiga.projects.userstories.models import UserStory
|
from taiga.projects.userstories.models import UserStory
|
||||||
|
@ -58,7 +58,7 @@ def test_valid_concurrent_save_for_issue(client):
|
||||||
url = reverse("issues-detail", args=(issue.id,))
|
url = reverse("issues-detail", args=(issue.id,))
|
||||||
data = {"version": 10}
|
data = {"version": 10}
|
||||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
response = client.patch(url, json.dumps(data), content_type="application/json")
|
||||||
assert json.loads(response.content.decode('utf-8'))['version'] == 11
|
assert json.loads(response.content)['version'] == 11
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
issue = Issue.objects.get(id=issue.id)
|
issue = Issue.objects.get(id=issue.id)
|
||||||
assert issue.version == 11
|
assert issue.version == 11
|
||||||
|
@ -85,7 +85,7 @@ def test_valid_concurrent_save_for_wiki_page(client):
|
||||||
url = reverse("wiki-detail", args=(wiki_page.id,))
|
url = reverse("wiki-detail", args=(wiki_page.id,))
|
||||||
data = {"version": 10}
|
data = {"version": 10}
|
||||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
response = client.patch(url, json.dumps(data), content_type="application/json")
|
||||||
assert json.loads(response.content.decode('utf-8'))['version'] == 11
|
assert json.loads(response.content)['version'] == 11
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
wiki_page = WikiPage.objects.get(id=wiki_page.id)
|
wiki_page = WikiPage.objects.get(id=wiki_page.id)
|
||||||
assert wiki_page.version == 11
|
assert wiki_page.version == 11
|
||||||
|
@ -128,7 +128,7 @@ def test_valid_concurrent_save_for_us(client):
|
||||||
url = reverse("userstories-detail", args=(userstory.id,))
|
url = reverse("userstories-detail", args=(userstory.id,))
|
||||||
data = {"version": 10}
|
data = {"version": 10}
|
||||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
response = client.patch(url, json.dumps(data), content_type="application/json")
|
||||||
assert json.loads(response.content.decode('utf-8'))['version'] == 11
|
assert json.loads(response.content)['version'] == 11
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
userstory = UserStory.objects.get(id=userstory.id)
|
userstory = UserStory.objects.get(id=userstory.id)
|
||||||
assert userstory.version == 11
|
assert userstory.version == 11
|
||||||
|
@ -159,7 +159,7 @@ def test_valid_concurrent_save_for_task(client):
|
||||||
url = reverse("tasks-detail", args=(task.id,))
|
url = reverse("tasks-detail", args=(task.id,))
|
||||||
data = {"version": 10}
|
data = {"version": 10}
|
||||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
response = client.patch(url, json.dumps(data), content_type="application/json")
|
||||||
assert json.loads(response.content.decode('utf-8'))['version'] == 11
|
assert json.loads(response.content)['version'] == 11
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
task = Task.objects.get(id=task.id)
|
task = Task.objects.get(id=task.id)
|
||||||
assert task.version == 11
|
assert task.version == 11
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
|
||||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from .. import factories as f
|
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
def test_archived_filter(client):
|
|
||||||
user = f.UserFactory.create()
|
|
||||||
project = f.ProjectFactory.create(owner=user)
|
|
||||||
f.MembershipFactory.create(project=project, user=user)
|
|
||||||
f.UserStoryFactory.create(project=project)
|
|
||||||
f.UserStoryFactory.create(is_archived=True, project=project)
|
|
||||||
|
|
||||||
client.login(user)
|
|
||||||
|
|
||||||
url = reverse("userstories-list")
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
response = client.get(url, data)
|
|
||||||
assert len(json.loads(response.content.decode('utf-8'))) == 2
|
|
||||||
|
|
||||||
data = {"is_archived": 0}
|
|
||||||
response = client.get(url, data)
|
|
||||||
assert len(json.loads(response.content.decode('utf-8'))) == 1
|
|
||||||
|
|
||||||
data = {"is_archived": 1}
|
|
||||||
response = client.get(url, data)
|
|
||||||
assert len(json.loads(response.content.decode('utf-8'))) == 1
|
|
|
@ -7,7 +7,7 @@ import pytest
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_api_create_project(client):
|
def test_create_project(client):
|
||||||
user = f.create_user()
|
user = f.create_user()
|
||||||
url = reverse("projects-list")
|
url = reverse("projects-list")
|
||||||
data = {"name": "project name", "description": "project description"}
|
data = {"name": "project name", "description": "project description"}
|
||||||
|
@ -18,7 +18,7 @@ def test_api_create_project(client):
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
def test_api_partially_update_project(client):
|
def test_partially_update_project(client):
|
||||||
project = f.create_project()
|
project = f.create_project()
|
||||||
url = reverse("projects-detail", kwargs={"pk": project.pk})
|
url = reverse("projects-detail", kwargs={"pk": project.pk})
|
||||||
data = {"name": ""}
|
data = {"name": ""}
|
||||||
|
|
|
@ -61,3 +61,24 @@ def test_api_create_in_bulk_with_status(client):
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.data[0]["status"] == us.project.default_task_status.id
|
assert response.data[0]["status"] == us.project.default_task_status.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_create_invalid_task(client):
|
||||||
|
# Associated to a milestone and a user story.
|
||||||
|
# But the User Story is not associated with the milestone
|
||||||
|
us_milestone = f.MilestoneFactory.create()
|
||||||
|
us = f.create_userstory(milestone=us_milestone)
|
||||||
|
task_milestone = f.MilestoneFactory.create(project=us.project, owner=us.owner)
|
||||||
|
|
||||||
|
url = reverse("tasks-list")
|
||||||
|
data = {
|
||||||
|
"user_story": us.id,
|
||||||
|
"milestone": task_milestone.id,
|
||||||
|
"subject": "Testing subject",
|
||||||
|
"status": us.project.default_task_status.id,
|
||||||
|
"project": us.project.id
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(us.owner)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import pytest
|
import pytest
|
||||||
import json
|
import json
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from .. import factories as f
|
from .. import factories as f
|
||||||
|
|
||||||
from taiga.users import models
|
from taiga.users import models
|
||||||
|
from taiga.auth.tokens import get_token_for_user
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_api_user_normal_user(client):
|
def test_users_create_through_standard_api(client):
|
||||||
user = f.UserFactory.create(is_superuser=True)
|
user = f.UserFactory.create(is_superuser=True)
|
||||||
|
|
||||||
url = reverse('users-list')
|
url = reverse('users-list')
|
||||||
|
@ -25,7 +27,7 @@ def test_api_user_normal_user(client):
|
||||||
assert response.status_code == 405
|
assert response.status_code == 405
|
||||||
|
|
||||||
|
|
||||||
def test_api_user_patch_same_email(client):
|
def test_update_user_with_same_email(client):
|
||||||
user = f.UserFactory.create(email="same@email.com")
|
user = f.UserFactory.create(email="same@email.com")
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
data = {"email": "same@email.com"}
|
data = {"email": "same@email.com"}
|
||||||
|
@ -37,7 +39,7 @@ def test_api_user_patch_same_email(client):
|
||||||
assert response.data['_error_message'] == 'Duplicated email'
|
assert response.data['_error_message'] == 'Duplicated email'
|
||||||
|
|
||||||
|
|
||||||
def test_api_user_patch_duplicated_email(client):
|
def test_update_user_with_duplicated_email(client):
|
||||||
f.UserFactory.create(email="one@email.com")
|
f.UserFactory.create(email="one@email.com")
|
||||||
user = f.UserFactory.create(email="two@email.com")
|
user = f.UserFactory.create(email="two@email.com")
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
|
@ -50,7 +52,7 @@ def test_api_user_patch_duplicated_email(client):
|
||||||
assert response.data['_error_message'] == 'Duplicated email'
|
assert response.data['_error_message'] == 'Duplicated email'
|
||||||
|
|
||||||
|
|
||||||
def test_api_user_patch_invalid_email(client):
|
def test_update_user_with_invalid_email(client):
|
||||||
user = f.UserFactory.create(email="my@email.com")
|
user = f.UserFactory.create(email="my@email.com")
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
data = {"email": "my@email"}
|
data = {"email": "my@email"}
|
||||||
|
@ -62,7 +64,7 @@ def test_api_user_patch_invalid_email(client):
|
||||||
assert response.data['_error_message'] == 'Not valid email'
|
assert response.data['_error_message'] == 'Not valid email'
|
||||||
|
|
||||||
|
|
||||||
def test_api_user_patch_valid_email(client):
|
def test_update_user_with_valid_email(client):
|
||||||
user = f.UserFactory.create(email="old@email.com")
|
user = f.UserFactory.create(email="old@email.com")
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
data = {"email": "new@email.com"}
|
data = {"email": "new@email.com"}
|
||||||
|
@ -76,7 +78,7 @@ def test_api_user_patch_valid_email(client):
|
||||||
assert user.new_email == "new@email.com"
|
assert user.new_email == "new@email.com"
|
||||||
|
|
||||||
|
|
||||||
def test_api_user_action_change_email_ok(client):
|
def test_validate_requested_email_change(client):
|
||||||
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||||
url = reverse('users-change-email')
|
url = reverse('users-change-email')
|
||||||
data = {"email_token": "change_email_token"}
|
data = {"email_token": "change_email_token"}
|
||||||
|
@ -91,19 +93,17 @@ def test_api_user_action_change_email_ok(client):
|
||||||
assert user.email == "new@email.com"
|
assert user.email == "new@email.com"
|
||||||
|
|
||||||
|
|
||||||
def test_api_user_action_change_email_no_token(client):
|
def test_validate_requested_email_change_without_token(client):
|
||||||
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||||
url = reverse('users-change-email')
|
url = reverse('users-change-email')
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
client.login(user)
|
client.login(user)
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.data['_error_message'] == 'Invalid, are you sure the token is correct and you didn\'t use it before?'
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_user_action_change_email_invalid_token(client):
|
def test_validate_requested_email_change_with_invalid_token(client):
|
||||||
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||||
url = reverse('users-change-email')
|
url = reverse('users-change-email')
|
||||||
data = {"email_token": "invalid_email_token"}
|
data = {"email_token": "invalid_email_token"}
|
||||||
|
@ -112,4 +112,67 @@ def test_api_user_action_change_email_invalid_token(client):
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.data['_error_message'] == 'Invalid, are you sure the token is correct and you didn\'t use it before?'
|
|
||||||
|
|
||||||
|
def test_delete_self_user(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.delete(url)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
user = models.User.objects.get(pk=user.id)
|
||||||
|
assert user.full_name == "Deleted user"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_self_user_with_valid_token(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
url = reverse('users-cancel')
|
||||||
|
cancel_token = get_token_for_user(user, "cancel_account")
|
||||||
|
data = {"cancel_token": cancel_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.full_name == "Deleted user"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_self_user_with_invalid_token(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
url = reverse('users-cancel')
|
||||||
|
data = {"cancel_token": "invalid_cancel_token"}
|
||||||
|
client.login(user)
|
||||||
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
DUMMY_BMP_DATA = b'BM:\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\x04\x00\x00\x00\x13\x0b\x00\x00\x13\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_avatar(client):
|
||||||
|
url = reverse('users-change-avatar')
|
||||||
|
|
||||||
|
user = f.UserFactory()
|
||||||
|
client.login(user)
|
||||||
|
|
||||||
|
with NamedTemporaryFile() as avatar:
|
||||||
|
# Test no avatar send
|
||||||
|
post_data = {}
|
||||||
|
response = client.post(url, post_data)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
# Test invalid file send
|
||||||
|
post_data = {
|
||||||
|
'avatar': avatar
|
||||||
|
}
|
||||||
|
response = client.post(url, post_data)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
# Test empty valid avatar send
|
||||||
|
avatar.write(DUMMY_BMP_DATA)
|
||||||
|
avatar.seek(0)
|
||||||
|
response = client.post(url, post_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
from taiga.base import exceptions as exc
|
||||||
|
from taiga.auth.tokens import get_token_for_user, get_user_for_token
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
def test_valid_token():
|
||||||
|
user = f.UserFactory.create(email="old@email.com")
|
||||||
|
token = get_token_for_user(user, "testing_scope")
|
||||||
|
user_from_token = get_user_for_token(token, "testing_scope")
|
||||||
|
assert user.id == user_from_token.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(raises=exc.NotAuthenticated)
|
||||||
|
def test_invalid_token():
|
||||||
|
user = f.UserFactory.create(email="old@email.com")
|
||||||
|
user_from_token = get_user_for_token("testing_invalid_token", "testing_scope")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(raises=exc.NotAuthenticated)
|
||||||
|
def test_invalid_token_expiration():
|
||||||
|
user = f.UserFactory.create(email="old@email.com")
|
||||||
|
token = get_token_for_user(user, "testing_scope")
|
||||||
|
user_from_token = get_user_for_token(token, "testing_scope", max_age=1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(raises=exc.NotAuthenticated)
|
||||||
|
def test_invalid_token_scope():
|
||||||
|
user = f.UserFactory.create(email="old@email.com")
|
||||||
|
token = get_token_for_user(user, "testing_scope")
|
||||||
|
user_from_token = get_user_for_token(token, "testing_invalid_scope")
|
Loading…
Reference in New Issue