diff --git a/requirements.txt b/requirements.txt index c5ed58b1..96142f5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ gunicorn==18.0 psycopg2==2.5.3 pytz==2014.4 six==1.6.1 -djmail==0.6 +djmail==0.7 django-pgjson==0.1.2 django-jinja==1.0.1 jinja2==2.7.2 diff --git a/settings/local.py.example b/settings/local.py.example index 0c3c4415..9ba66b76 100644 --- a/settings/local.py.example +++ b/settings/local.py.example @@ -46,3 +46,10 @@ from .development import * #EMAIL_HOST_USER = 'youremail@gmail.com' #EMAIL_HOST_PASSWORD = 'yourpassword' #EMAIL_PORT = 587 + +# GITHUP SETTINGS +#GITHUB_URL = "https://github.com/" +#GITHUB_API_URL = "https://api.github.com/" +#GITHUB_API_CLIENT_ID = "yourgithubclientid" +#GITHUB_API_CLIENT_SECRET = "yourgithubclientsecret" + diff --git a/taiga/auth/api.py b/taiga/auth/api.py index 4987ee8b..11a0e41d 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -28,6 +28,7 @@ from rest_framework import serializers from taiga.base.decorators import list_route from taiga.base import exceptions as exc +from taiga.base.connectors import github from taiga.users.services import get_and_validate_user from .serializers import PublicRegisterSerializer @@ -37,6 +38,7 @@ from .serializers import PrivateRegisterForNewUserSerializer from .services import private_register_for_existing_user from .services import private_register_for_new_user from .services import public_register +from .services import github_register from .services import make_auth_response_data @@ -130,11 +132,34 @@ class AuthViewSet(viewsets.ViewSet): return self._private_register(request) raise exc.BadRequest(_("invalid register type")) - # Login view: /api/v1/auth - def create(self, request, **kwargs): + def _login(self, request): username = request.DATA.get('username', None) password = request.DATA.get('password', None) user = get_and_validate_user(username=username, password=password) data = make_auth_response_data(user) return Response(data, status=status.HTTP_200_OK) + + def _github_login(self, request): + code = request.DATA.get('code', None) + token = request.DATA.get('token', None) + + email, user_info = github.me(code) + + user = github_register(username=user_info.username, + email=email, + full_name=user_info.full_name, + github_id=user_info.id, + bio=user_info.bio, + token=token) + data = make_auth_response_data(user) + return Response(data, status=status.HTTP_200_OK) + + # Login view: /api/v1/auth + def create(self, request, **kwargs): + type = request.DATA.get("type", None) + if type == "normal": + return self._login(request) + elif type == "github": + return self._github_login(request) + raise exc.BadRequest(_("invalid login type")) diff --git a/taiga/auth/serializers.py b/taiga/auth/serializers.py index f9b4e367..2b235d1d 100644 --- a/taiga/auth/serializers.py +++ b/taiga/auth/serializers.py @@ -17,8 +17,7 @@ from rest_framework import serializers class BaseRegisterSerializer(serializers.Serializer): - first_name = serializers.CharField(max_length=200) - last_name = serializers.CharField(max_length=200) + full_name = serializers.CharField(max_length=256) email = serializers.EmailField(max_length=200) username = serializers.CharField(max_length=200) password = serializers.CharField(min_length=4) diff --git a/taiga/auth/services.py b/taiga/auth/services.py index f7dda5d7..cebc24e6 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -63,14 +63,18 @@ def send_private_register_email(user, **kwargs) -> bool: return bool(email.send()) -def is_user_already_registred(*, username:str, email:str) -> bool: +def is_user_already_registred(*, username:str, email:str, github_id:int=None) -> bool: """ Checks if a specified user is already registred. """ user_model = get_model("users", "User") - qs = user_model.objects.filter(Q(username=username) | - Q(email=email)) + + or_expr = Q(username=username) | Q(email=email) + if github_id: + or_expr = or_expr | Q(email=email) + + qs = user_model.objects.filter(or_expr) return qs.exists() @@ -90,7 +94,7 @@ def get_membership_by_token(token:str): @tx.atomic -def public_register(username:str, password:str, email:str, first_name:str, last_name:str): +def public_register(username:str, password:str, email:str, full_name:str): """ Given a parsed parameters, try register a new user knowing that it follows a public register flow. @@ -107,8 +111,7 @@ def public_register(username:str, password:str, email:str, first_name:str, last_ user_model = get_model("users", "User") user = user_model(username=username, email=email, - first_name=first_name, - last_name=last_name) + full_name=full_name) user.set_password(password) user.save() @@ -138,21 +141,18 @@ def private_register_for_existing_user(token:str, username:str, password:str): @tx.atomic def private_register_for_new_user(token:str, username:str, email:str, - first_name:str, last_name:str, password:str): + full_name:str, password:str): """ Given a inviation token, try register new user matching the invitation token. """ - - user_model = get_model("users", "User") - if is_user_already_registred(username=username, email=email): raise exc.WrongArguments(_("Username or Email is already in use.")) + user_model = get_model("users", "User") user = user_model(username=username, email=email, - first_name=first_name, - last_name=last_name) + full_name=full_name) user.set_password(password) try: @@ -167,6 +167,30 @@ def private_register_for_new_user(token:str, username:str, email:str, return user +@tx.atomic +def github_register(username:str, email:str, full_name:str, github_id:int, bio:str, token:str=None): + """ + Register a new user from github. + + This can raise `exc.IntegrityError` exceptions in + case of conflics found. + + :returns: User + """ + user_model = get_model("users", "User") + user, created = user_model.objects.get_or_create(github_id=github_id, + defaults={"username": username, + "email": email, + "full_name": full_name, + "bio": bio}) + if token: + membership = get_membership_by_token(token) + membership.user = user + membership.save(update_fields=["user"]) + + return user + + def make_auth_response_data(user) -> dict: """ Given a domain and user, creates data structure diff --git a/taiga/auth/tests.py b/taiga/auth/tests.py index 1c023e3e..face1b4f 100644 --- a/taiga/auth/tests.py +++ b/taiga/auth/tests.py @@ -105,15 +105,13 @@ class AuthServicesTests(test.TestCase): username=self.user1.username, password="secret", email=self.user1.email, - first_name="foo", - last_name="bar") + full_name="foo") user = services.public_register(self.domain, username="foousername", password="foosecret", email="foo@bar.ca", - first_name="Foo", - last_name="Bar") + full_name="Foo") self.assertEqual(user.username, "foousername") self.assertTrue(user.check_password("foosecret")) self.assertTrue(is_user_exists_on_domain(self.domain, user)) @@ -153,8 +151,7 @@ class AuthServicesTests(test.TestCase): username="user2", password="user2", email="user2@bar.ca", - first_name="Foo", - last_name="Bar") + full_name="Foo") membership = membership.__class__.objects.get(pk=membership.pk) @@ -195,8 +192,7 @@ class RegisterApiTests(test.TestCase): data = { "username": "pepe", "password": "pepepepe", - "first_name": "pepe", - "last_name": "pepe", + "full_name": "pepe", "email": "pepe@pepe.com", "type": "public", } @@ -213,8 +209,7 @@ class RegisterApiTests(test.TestCase): data = { "username": "pepe", "password": "pepepepe", - "first_name": "pepe", - "last_name": "pepe", + "full_name": "pepe", "email": "pepe@pepe.com", "type": "public", } @@ -227,8 +222,7 @@ class RegisterApiTests(test.TestCase): data = { "username": "pepe", "password": "pepepepe", - "first_name": "pepe", - "last_name": "pepe", + "full_name": "pepe", "email": "pepe@pepe.com", "type": "private", } @@ -243,8 +237,7 @@ class RegisterApiTests(test.TestCase): data = { "username": "pepe", "password": "pepepepe", - "first_name": "pepe", - "last_name": "pepe", + "full_name": "pepe", "email": "pepe@pepe.com", "type": "private", "existing": False, diff --git a/taiga/base/connectors/__init__.py b/taiga/base/connectors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/base/connectors/exceptions.py b/taiga/base/connectors/exceptions.py new file mode 100644 index 00000000..b7647e67 --- /dev/null +++ b/taiga/base/connectors/exceptions.py @@ -0,0 +1,27 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + +from taiga.base.exceptions import BaseException + +from django.utils.translation import ugettext_lazy as _ + +class ConnectorBaseException(BaseException): + status_code = 400 + default_detail = _("Connection error.") + + +class GitHubApiError(ConnectorBaseException): + pass diff --git a/taiga/base/connectors/github.py b/taiga/base/connectors/github.py new file mode 100644 index 00000000..344a44ce --- /dev/null +++ b/taiga/base/connectors/github.py @@ -0,0 +1,160 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + +import requests +import json + +from collections import namedtuple +from urllib.parse import urljoin + +from django.conf import settings + +from . import exceptions as exc + + +###################################################### +## Data +###################################################### + +CLIENT_ID = getattr(settings, "GITHUB_API_CLIENT_ID", None) +CLIENT_SECRET = getattr(settings, "GITHUB_API_CLIENT_SECRET", None) + +URL = getattr(settings, "GITHUB_URL", "https://github.com/") +API_URL = getattr(settings, "GITHUB_API_URL", "https://api.github.com/") +API_RESOURCES_URLS = { + "login": { + "authorize": "login/oauth/authorize", + "access-token": "login/oauth/access_token" + }, + "user": { + "profile": "user", + "emails": "user/emails" + } +} + + +HEADERS = {"Accept": "application/json",} + +AuthInfo = namedtuple("AuthInfo", ["access_token"]) +User = namedtuple("User", ["id", "username", "full_name", "bio"]) +Email = namedtuple("Email", ["email", "is_primary"]) + + +###################################################### +## utils +###################################################### + +def _build_url(*args, **kwargs) -> str: + """ + Return a valid url. + """ + resource_url = API_RESOURCES_URLS + for key in args: + resource_url = resource_url[key] + + if kwargs: + resource_url = resource_url.format(**kwargs) + + return urljoin(API_URL, resource_url) + + +def _get(url:str, headers:dict) -> dict: + """ + Make a GET call. + """ + response = requests.get(url, headers=headers) + + data = response.json() + if response.status_code != 200: + raise exc.GitHubApiError({"status_code": response.status_code, + "error": data.get("error", "")}) + return data + + +def _post(url:str, params:dict, headers:dict) -> dict: + """ + Make a POST call. + """ + response = requests.post(url, params=params, headers=headers) + + data = response.json() + if response.status_code != 200 or "error" in data: + raise exc.GitHubApiError({"status_code": response.status_code, + "error": data.get("error", "")}) + return data + + +###################################################### +## Simple calls +###################################################### + +def login(access_code:str, client_id:str=CLIENT_ID, client_secret:str=CLIENT_SECRET, + headers:dict=HEADERS): + """ + Get access_token fron an user authorized code, the client id and the client secret key. + (See https://developer.github.com/v3/oauth/#web-application-flow). + """ + url = urljoin(URL, "login/oauth/access_token") + params={"code": access_code, + "client_id": client_id, + "client_secret": client_secret, + "scope": "user:emails"} + data = _post(url, params=params, headers=headers) + return AuthInfo(access_token=data.get("access_token", None)) + + +def get_user_profile(headers:dict=HEADERS): + """ + Get authenticated user info. + (See https://developer.github.com/v3/users/#get-the-authenticated-user). + """ + url = _build_url("user", "profile") + data = _get(url, headers=headers) + return User(id=data.get("id", None), + username=data.get("login", None), + full_name=(data.get("name", None) or ""), + bio=(data.get("bio", None) or "")) + + +def get_user_emails(headers:dict=HEADERS) -> list: + """ + Get a list with all emails of the authenticated user. + (See https://developer.github.com/v3/users/emails/#list-email-addresses-for-a-user). + """ + url = _build_url("user", "emails") + data = _get(url, headers=headers) + return [Email(email=e.get("email", None), is_primary=e.get("primary", False)) + for e in data] + + +###################################################### +## Convined calls +###################################################### + +def me(access_code:str) -> tuple: + """ + Connect to a github account and get all personal info (profile and the primary email). + """ + auth_info = login(access_code) + + headers = HEADERS.copy() + headers["Authorization"] = "token {}".format(auth_info.access_token) + + user = get_user_profile(headers=headers) + emails = get_user_emails(headers=headers) + + primary_email = next(filter(lambda x: x.is_primary, emails)) + return primary_email.email, user diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 347b97dd..1e54602d 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -96,8 +96,7 @@ class IssuesOrdering(filters.FilterBackend): if order_by in ['owner', '-owner', 'assigned_to', '-assigned_to']: return queryset.order_by( - '{}__first_name'.format(order_by), - '{}__last_name'.format(order_by) + '{}__full_name'.format(order_by) ) return queryset diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 838905eb..bb9d3a7b 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -290,8 +290,7 @@ class Command(BaseCommand): def create_user(self, counter): user = User.objects.create( username='user-{0}'.format(counter), - first_name=self.sd.name('es'), - last_name=self.sd.surname('es', number=1), + full_name="{} {}".format(self.sd.name('es'), self.sd.surname('es', number=1)), email=self.sd.email(), token=''.join(random.sample('abcdef0123456789', 10)), color=self.sd.choice(COLOR_CHOICES)) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index aeb2f868..b231e07e 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -109,7 +109,7 @@ class ProjectDetailSerializer(ProjectSerializer): issue_types = IssueTypeSerializer(many=True, required=False) def get_membership(self, obj): - qs = obj.memberships.order_by('user__first_name', 'user__last_name', 'user__username') + qs = obj.memberships.order_by('user__full_name', 'user__username') qs = qs.select_related("role", "user") serializer = ProjectMembershipSerializer(qs, many=True) @@ -117,7 +117,7 @@ class ProjectDetailSerializer(ProjectSerializer): def get_active_membership(self, obj): qs = obj.memberships.filter(user__isnull=False) - qs = qs.order_by('user__first_name', 'user__last_name', 'user__username') + qs = qs.order_by('user__full_name', 'user__username') qs = qs.select_related("role", "user") serializer = ProjectMembershipSerializer(qs, many=True) diff --git a/taiga/projects/tests/tests_api.py b/taiga/projects/tests/tests_api.py index 32a772ba..f2deffff 100644 --- a/taiga/projects/tests/tests_api.py +++ b/taiga/projects/tests/tests_api.py @@ -73,7 +73,7 @@ class ProfileTestCase(test.TestCase): password=self.user3.username) self.assertTrue(response) - data = {"first_name": "Foo Bar"} + data = {"full_name": "Foo Bar"} response = self.client.patch( reverse("users-detail", args=[self.user2.pk]), @@ -86,7 +86,7 @@ class ProfileTestCase(test.TestCase): password=self.user3.username) self.assertTrue(response) - data = {"first_name": "Foo Bar"} + data = {"full_name": "Foo Bar"} response = self.client.patch( reverse("users-detail", args=[self.user3.pk]), content_type="application/json", @@ -99,7 +99,7 @@ class ProfileTestCase(test.TestCase): password=self.user1.username) self.assertTrue(response) - data = {"first_name": "Foo Bar"} + data = {"full_name": "Foo Bar"} response = self.client.patch( reverse("users-detail", args=[self.user3.pk]), content_type="application/json", @@ -112,7 +112,7 @@ class ProfileTestCase(test.TestCase): password=self.user3.username) self.assertTrue(response) - data = {"first_name": "Foo Bar"} + data = {"full_name": "Foo Bar"} response = self.client.delete( reverse("users-detail", args=[self.user2.pk])) self.assertEqual(response.status_code, 404) @@ -122,7 +122,7 @@ class ProfileTestCase(test.TestCase): password=self.user3.username) self.assertTrue(response) - data = {"first_name": "Foo Bar"} + data = {"full_name": "Foo Bar"} response = self.client.delete( reverse("users-detail", args=[self.user3.pk])) @@ -134,7 +134,7 @@ class ProfileTestCase(test.TestCase): password=self.user1.username) self.assertTrue(response) - data = {"first_name": "Foo Bar"} + data = {"full_name": "Foo Bar"} response = self.client.delete( reverse("users-detail", args=[self.user3.pk])) diff --git a/taiga/projects/votes/serializers.py b/taiga/projects/votes/serializers.py index 2bd473f2..1c089aff 100644 --- a/taiga/projects/votes/serializers.py +++ b/taiga/projects/votes/serializers.py @@ -8,4 +8,4 @@ class VoterSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ('id', 'username', 'first_name', 'last_name', 'full_name') + fields = ('id', 'username', 'full_name') diff --git a/taiga/routers.py b/taiga/routers.py index f09b1d55..809544f2 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -20,11 +20,9 @@ router = routers.DefaultRouter(trailing_slash=False) # taiga.users from taiga.users.api import UsersViewSet -from taiga.users.api import PermissionsViewSet from taiga.auth.api import AuthViewSet router.register(r"users", UsersViewSet, base_name="users") -router.register(r"permissions", PermissionsViewSet, base_name="permissions") router.register(r"auth", AuthViewSet, base_name="auth") diff --git a/taiga/users/admin.py b/taiga/users/admin.py index 8339c635..c258e17b 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -47,20 +47,19 @@ admin.site.register(Role, RoleAdmin) class UserAdmin(DjangoUserAdmin): fieldsets = ( (None, {'fields': ('username', 'password')}), - (_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'description', 'photo')}), + (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}), (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags')}), (_('Notifications info'), {'fields': ("notify_level", "notify_changes_by_me",)}), - (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',)}), + (_('Permissions'), {'fields': ('is_active', 'is_superuser',)}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) form = UserChangeForm add_form = UserCreationForm - - -class PermissionAdmin(admin.ModelAdmin): - list_display = ['name', 'content_type', 'codename'] - list_filter = ['content_type'] - + list_display = ('username', 'email', 'full_name') + list_filter = ('is_superuser', 'is_active') + search_fields = ('username', 'full_name', 'email') + ordering = ('username',) + filter_horizontal = () class RoleInline(admin.TabularInline): model = Role @@ -68,4 +67,3 @@ class RoleInline(admin.TabularInline): admin.site.register(User, UserAdmin) -admin.site.register(Permission, PermissionAdmin) diff --git a/taiga/users/api.py b/taiga/users/api.py index ee280d65..0a2ef5d0 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -20,7 +20,6 @@ from django.db.models.loading import get_model from django.db.models import Q from django.shortcuts import get_object_or_404 from django.contrib.auth import logout, login, authenticate -from django.contrib.auth.models import Permission from django.utils.translation import ugettext_lazy as _ from rest_framework.response import Response @@ -35,7 +34,8 @@ from taiga.base import exceptions as exc from taiga.base.api import ModelCrudViewSet, RetrieveModelMixin, ModelListViewSet from .models import User, Role -from .serializers import UserSerializer, RecoverySerializer, PermissionSerializer +from .serializers import UserSerializer, RecoverySerializer + class MembersFilterBackend(BaseFilterBackend): def filter_queryset(self, request, queryset, view): @@ -53,27 +53,6 @@ class MembersFilterBackend(BaseFilterBackend): else: return queryset.filter(pk=request.user.id) -class PermissionsViewSet(ModelListViewSet): - permission_classes = (IsAuthenticated,) - serializer_class = PermissionSerializer - paginate_by = 0 - excluded_codenames = [ - "add_logentry", "change_logentry", "delete_logentry", - "add_group", "change_group", "delete_group", - "add_permission", "change_permission", "delete_permission", - "add_contenttype", "change_contenttype", "delete_contenttype", - "add_message", "change_message", "delete_message", - "add_session", "change_session", "delete_session", - "add_migrationhistory", "change_migrationhistory", "delete_migrationhistory", - "add_version", "change_version", "delete_version", - "add_revision", "change_revision", "delete_revision", - "add_user", "delete_user", - "add_project", - ] - - def get_queryset(self): - return Permission.objects.exclude(codename__in=self.excluded_codenames) - class UsersViewSet(ModelCrudViewSet): permission_classes = (IsAuthenticated,) diff --git a/taiga/users/fixtures/initial_user.json b/taiga/users/fixtures/initial_user.json index 4381031f..459fcc3f 100644 --- a/taiga/users/fixtures/initial_user.json +++ b/taiga/users/fixtures/initial_user.json @@ -4,9 +4,8 @@ "model": "users.user", "fields": { "username": "admin", - "first_name": "", - "last_name": "", - "description": "", + "full_name": "", + "bio": "", "default_language": "", "color": "", "photo": "", @@ -15,10 +14,8 @@ "default_timezone": "", "is_superuser": true, "token": "", - "is_staff": true, + "github_id": null, "last_login": "2013-04-04T07:36:09.880Z", - "groups": [], - "user_permissions": [], "password": "pbkdf2_sha256$10000$oRIbCKOL1U3w$/gaYMnOlc/GnN4mn3UUXvXpk2Hx0vvht6Uqhu46aikI=", "email": "niwi@niwi.be", "date_joined": "2013-04-01T13:48:21.711Z" diff --git a/taiga/users/migrations/0005_auto__del_field_user_first_name__del_field_user_last_name__del_field_u.py b/taiga/users/migrations/0005_auto__del_field_user_first_name__del_field_user_last_name__del_field_u.py new file mode 100644 index 00000000..052d6c44 --- /dev/null +++ b/taiga/users/migrations/0005_auto__del_field_user_first_name__del_field_user_last_name__del_field_u.py @@ -0,0 +1,282 @@ +# -*- 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): + # Deleting field 'User.notify_changes_by_me' + db.delete_column('users_user', 'notify_changes_by_me') + + # Deleting field 'User.notify_level' + db.delete_column('users_user', 'notify_level') + + # Deleting field 'User.is_staff' + db.delete_column('users_user', 'is_staff') + + + # Adding field 'User.github_id' + db.add_column('users_user', 'github_id', + self.gf('django.db.models.fields.IntegerField')(null=True, blank=True), + keep_default=False) + + # Removing M2M table for field groups on 'User' + db.delete_table(db.shorten_name('users_user_groups')) + + # Removing M2M table for field user_permissions on 'User' + db.delete_table(db.shorten_name('users_user_user_permissions')) + + # Adding field 'User.full_name' + db.add_column('users_user', 'full_name', + self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True), + keep_default=False) + + #Copy first_name and last_name into full_name + for user in orm.Users.exclude(first_name="", last_name=""): + if user.first_name and user.last_name: + user.full_name = "{} {}".format(user.first_name, user.last_name) + elif not user.last_name: + user.full_name = user.first_name + else: + user.full_name = user.last_name + user.save() + + # Deleting field 'User.first_name' + db.delete_column('users_user', 'first_name') + + # Deleting field 'User.last_name' + db.delete_column('users_user', 'last_name') + + def backwards(self, orm): + # Adding field 'User.notify_changes_by_me' + db.add_column('users_user', 'notify_changes_by_me', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'User.notify_level' + db.add_column('users_user', 'notify_level', + self.gf('django.db.models.fields.CharField')(default='all_owned_projects', max_length=32), + keep_default=False) + + # Deleting field 'User.github_id' + db.delete_column('users_user', 'github_id') + + # Adding M2M table for field groups on 'User' + m2m_table_name = db.shorten_name('users_user_groups') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm['users.user'], null=False)), + ('group', models.ForeignKey(orm['auth.group'], null=False)) + )) + db.create_unique(m2m_table_name, ['user_id', 'group_id']) + + # Adding M2M table for field user_permissions on 'User' + m2m_table_name = db.shorten_name('users_user_user_permissions') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm['users.user'], null=False)), + ('permission', models.ForeignKey(orm['auth.permission'], null=False)) + )) + db.create_unique(m2m_table_name, ['user_id', 'permission_id']) + + # Adding field 'User.first_name' + db.add_column('users_user', 'first_name', + self.gf('django.db.models.fields.CharField')(default='', max_length=30, blank=True), + keep_default=False) + + # Adding field 'User.last_name' + db.add_column('users_user', 'last_name', + self.gf('django.db.models.fields.CharField')(default='', max_length=30, blank=True), + keep_default=False) + + #Copy full_name into first_name and last_name + for user in orm.Users.exclude(full_name=""): + first_name, last_name = user.full_name.split(maxsplit=1) + + user.first_name = first_name[:30] + user.last_name = last_name[:30] + user.save() + + # Deleting field 'User.full_name' + db.delete_column('users_user', 'full_name') + + # Adding field 'User.is_staff' + db.add_column('users_user', 'is_staff', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + models = { + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'db_table': "'django_content_type'", 'ordering': "('name',)", 'object_name': 'ContentType', 'unique_together': "(('app_label', 'model'),)"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'projects.issuestatus': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'IssueStatus'}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), + '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': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'IssueType'}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), + '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': {'unique_together': "(('user', 'project'),)", 'ordering': "['project', 'role']", 'object_name': 'Membership'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'blank': 'True', 'auto_now_add': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + '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', 'max_length': '60', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['users.User']", 'null': 'True', 'blank': 'True', 'related_name': "'memberships'"}) + }, + 'projects.points': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'Points'}, + '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', 'null': 'True', 'blank': 'True'}) + }, + 'projects.priority': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'Priority'}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), + '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'}, + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['projects.ProjectTemplate']", 'null': 'True', 'blank': 'True', 'related_name': "'projects'"}), + 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.IssueStatus']", 'on_delete': 'models.SET_NULL', 'unique': 'True', 'blank': 'True', 'null': 'True', 'related_name': "'+'"}), + 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.IssueType']", 'on_delete': 'models.SET_NULL', 'unique': 'True', 'blank': 'True', 'null': 'True', 'related_name': "'+'"}), + 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.Points']", 'on_delete': 'models.SET_NULL', 'unique': 'True', 'blank': 'True', 'null': 'True', 'related_name': "'+'"}), + 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.Priority']", 'on_delete': 'models.SET_NULL', 'unique': 'True', 'blank': 'True', 'null': 'True', 'related_name': "'+'"}), + 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.Severity']", 'on_delete': 'models.SET_NULL', 'unique': 'True', 'blank': 'True', 'null': 'True', 'related_name': "'+'"}), + 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.TaskStatus']", 'on_delete': 'models.SET_NULL', 'unique': 'True', 'blank': 'True', 'null': 'True', 'related_name': "'+'"}), + 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['projects.UserStoryStatus']", 'on_delete': 'models.SET_NULL', 'unique': 'True', 'blank': 'True', 'null': 'True', 'related_name': "'+'"}), + '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'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['users.User']", 'through': "orm['projects.Membership']", 'symmetrical': 'False', 'related_name': "'projects'"}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '250', 'unique': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.User']", 'related_name': "'owned_projects'"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'unique': 'True', 'blank': 'True'}), + 'tags': ('picklefield.fields.PickledObjectField', [], {'blank': 'True'}), + 'total_milestones': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), + 'total_story_points': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True'}), + 'videoconferences': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), + 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}) + }, + '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', 'unique': 'True', 'blank': 'True'}), + 'task_statuses': ('django_pgjson.fields.JsonField', [], {}), + 'us_statuses': ('django_pgjson.fields.JsonField', [], {}), + 'videoconferences': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), + 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}) + }, + 'projects.severity': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'Severity'}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), + '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': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'TaskStatus'}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), + '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': {'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']", 'object_name': 'UserStoryStatus'}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), + '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', 'null': 'True', 'blank': 'True'}) + }, + 'users.role': { + 'Meta': {'unique_together': "(('slug', 'project'),)", 'ordering': "['order', 'slug']", 'object_name': 'Role'}, + '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': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'related_name': "'roles'"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']", 'related_name': "'roles'"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'blank': 'True'}) + }, + 'users.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'#be719b'", 'max_length': '9', 'blank': 'True'}), + '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', [], {'default': "''", 'max_length': '20', 'blank': 'True'}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'full_name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}), + 'github_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': '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'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'photo': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}) + } + } + + complete_apps = ['users'] diff --git a/taiga/users/migrations/0005_auto__del_field_user_notify_changes_by_me__del_field_user_notify_level.py b/taiga/users/migrations/0006_rename_description_to_bio.py similarity index 67% rename from taiga/users/migrations/0005_auto__del_field_user_notify_changes_by_me__del_field_user_notify_level.py rename to taiga/users/migrations/0006_rename_description_to_bio.py index df2c84dd..fff41c74 100644 --- a/taiga/users/migrations/0005_auto__del_field_user_notify_changes_by_me__del_field_user_notify_level.py +++ b/taiga/users/migrations/0006_rename_description_to_bio.py @@ -8,48 +8,28 @@ from django.db import models class Migration(SchemaMigration): def forwards(self, orm): - # Deleting field 'User.notify_changes_by_me' - db.delete_column('users_user', 'notify_changes_by_me') - - # Deleting field 'User.notify_level' - db.delete_column('users_user', 'notify_level') - + db.rename_column(u'users_user', 'description', 'bio') def backwards(self, orm): - # Adding field 'User.notify_changes_by_me' - db.add_column('users_user', 'notify_changes_by_me', - self.gf('django.db.models.fields.BooleanField')(default=False), - keep_default=False) - - # Adding field 'User.notify_level' - db.add_column('users_user', 'notify_level', - self.gf('django.db.models.fields.CharField')(default='all_owned_projects', max_length=32), - keep_default=False) - + db.rename_column(u'users_user', 'bio', 'description') models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.Permission']", 'blank': 'True'}) - }, 'auth.permission': { - 'Meta': {'object_name': 'Permission', 'unique_together': "(('content_type', 'codename'),)", 'ordering': "('content_type__app_label', 'content_type__model', 'codename')"}, + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission', 'ordering': "('content_type__app_label', 'content_type__model', 'codename')"}, 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, 'contenttypes.contenttype': { - 'Meta': {'db_table': "'django_content_type'", 'object_name': 'ContentType', 'unique_together': "(('app_label', 'model'),)", 'ordering': "('name',)"}, + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'ordering': "('name',)", 'db_table': "'django_content_type'"}, 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, 'projects.issuestatus': { - 'Meta': {'object_name': 'IssueStatus', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'IssueStatus', 'ordering': "['project', 'order', 'name']"}, 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), @@ -58,7 +38,7 @@ class Migration(SchemaMigration): 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_statuses'", 'to': "orm['projects.Project']"}) }, 'projects.issuetype': { - 'Meta': {'object_name': 'IssueType', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'IssueType', 'ordering': "['project', 'order', 'name']"}, 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), @@ -66,17 +46,17 @@ class Migration(SchemaMigration): 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_types'", 'to': "orm['projects.Project']"}) }, 'projects.membership': { - 'Meta': {'object_name': 'Membership', 'unique_together': "(('user', 'project'),)", 'ordering': "['project', 'role']"}, - 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'default': 'datetime.datetime.now', 'blank': 'True'}), - 'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'Meta': {'unique_together': "(('user', 'project'),)", 'object_name': 'Membership', 'ordering': "['project', 'role']"}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'blank': 'True', 'auto_now_add': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'null': 'True', 'blank': 'True', 'max_length': '255'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['projects.Project']"}), 'role': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['users.Role']"}), - 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '60', 'null': 'True', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'memberships'", 'to': "orm['users.User']", 'null': 'True', 'blank': 'True'}) + 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'null': 'True', 'blank': 'True', 'max_length': '60'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'memberships'", 'null': 'True', 'blank': 'True', 'to': "orm['users.User']"}) }, 'projects.points': { - 'Meta': {'object_name': 'Points', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Points', 'ordering': "['project', 'order', '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'}), @@ -84,7 +64,7 @@ class Migration(SchemaMigration): 'value': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) }, 'projects.priority': { - 'Meta': {'object_name': 'Priority', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Priority', 'ordering': "['project', 'order', 'name']"}, 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), @@ -93,36 +73,36 @@ class Migration(SchemaMigration): }, 'projects.project': { 'Meta': {'object_name': 'Project', 'ordering': "['name']"}, - 'created_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'projects'", 'to': "orm['projects.ProjectTemplate']", 'null': 'True', 'blank': 'True'}), - 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'to': "orm['projects.IssueStatus']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': 'True'}), - 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'to': "orm['projects.IssueType']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': 'True'}), - 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'to': "orm['projects.Points']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': 'True'}), - 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'to': "orm['projects.Priority']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': 'True'}), - 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'to': "orm['projects.Severity']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': 'True'}), - 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'to': "orm['projects.TaskStatus']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': 'True'}), - 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'to': "orm['projects.UserStoryStatus']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': 'True'}), + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'projects'", 'null': 'True', 'blank': 'True', 'to': "orm['projects.ProjectTemplate']"}), + 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'to': "orm['projects.IssueStatus']", 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True'}), + 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'to': "orm['projects.IssueType']", 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True'}), + 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'to': "orm['projects.Points']", 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True'}), + 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'to': "orm['projects.Priority']", 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True'}), + 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'to': "orm['projects.Severity']", 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True'}), + 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'to': "orm['projects.TaskStatus']", 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'unique': 'True'}), + 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'to': "orm['projects.UserStoryStatus']", 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', '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_wiki_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'members': ('django.db.models.fields.related.ManyToManyField', [], {'through': "orm['projects.Membership']", 'related_name': "'projects'", 'to': "orm['users.User']", 'symmetrical': 'False'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'through': "orm['projects.Membership']", 'to': "orm['users.User']", 'symmetrical': 'False', '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'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '250', 'unique': 'True'}), 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'owned_projects'", 'to': "orm['users.User']"}), 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '250', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'blank': 'True', 'unique': 'True'}), 'tags': ('picklefield.fields.PickledObjectField', [], {'blank': 'True'}), 'total_milestones': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 'total_story_points': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True'}), - 'videoconferences': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), - 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}) + 'videoconferences': ('django.db.models.fields.CharField', [], {'null': 'True', 'blank': 'True', 'max_length': '250'}), + 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'null': 'True', 'blank': 'True', 'max_length': '250'}) }, 'projects.projecttemplate': { 'Meta': {'object_name': 'ProjectTemplate', 'ordering': "['name']"}, - 'created_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + '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', [], {}), @@ -139,14 +119,14 @@ class Migration(SchemaMigration): 'priorities': ('django_pgjson.fields.JsonField', [], {}), 'roles': ('django_pgjson.fields.JsonField', [], {}), 'severities': ('django_pgjson.fields.JsonField', [], {}), - 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '250', 'blank': 'True'}), + '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', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), - 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}) + 'videoconferences': ('django.db.models.fields.CharField', [], {'null': 'True', 'blank': 'True', 'max_length': '250'}), + 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'null': 'True', 'blank': 'True', 'max_length': '250'}) }, 'projects.severity': { - 'Meta': {'object_name': 'Severity', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Severity', 'ordering': "['project', 'order', 'name']"}, 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), @@ -154,7 +134,7 @@ class Migration(SchemaMigration): 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'severities'", 'to': "orm['projects.Project']"}) }, 'projects.taskstatus': { - 'Meta': {'object_name': 'TaskStatus', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'TaskStatus', 'ordering': "['project', 'order', 'name']"}, 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), @@ -163,7 +143,7 @@ class Migration(SchemaMigration): 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_statuses'", 'to': "orm['projects.Project']"}) }, 'projects.userstorystatus': { - 'Meta': {'object_name': 'UserStoryStatus', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'UserStoryStatus', 'ordering': "['project', 'order', 'name']"}, 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), @@ -173,38 +153,37 @@ class Migration(SchemaMigration): 'wip_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) }, 'users.role': { - 'Meta': {'object_name': 'Role', 'unique_together': "(('slug', 'project'),)", 'ordering': "['order', 'slug']"}, + 'Meta': {'unique_together': "(('slug', 'project'),)", 'object_name': 'Role', 'ordering': "['order', 'slug']"}, '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': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'to': "orm['auth.Permission']", 'symmetrical': 'False'}), 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'roles'", 'to': "orm['projects.Project']"}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'blank': 'True'}) + 'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '250'}) }, 'users.user': { 'Meta': {'object_name': 'User', 'ordering': "['username']"}, - 'color': ('django.db.models.fields.CharField', [], {'default': "'#e6748a'", 'max_length': '9', 'blank': 'True'}), + 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'color': ('django.db.models.fields.CharField', [], {'default': "'#f18e35'", 'blank': 'True', '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', [], {'default': "''", 'max_length': '20', 'blank': 'True'}), - 'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'user_set'", 'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'default_language': ('django.db.models.fields.CharField', [], {'default': "''", 'blank': 'True', 'max_length': '20'}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'blank': 'True', 'max_length': '20'}), + 'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}), + 'full_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '256'}), + 'github_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'notify_level': ('django.db.models.fields.CharField', [], {'default': "'all_owned_projects'", 'max_length': '32'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'photo': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), - 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'user_set'", 'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + 'photo': ('django.db.models.fields.files.FileField', [], {'null': 'True', 'blank': 'True', 'max_length': '500'}), + 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'null': 'True', 'blank': 'True', 'max_length': '200'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}) } } - complete_apps = ['users'] \ No newline at end of file + complete_apps = ['users'] diff --git a/taiga/users/models.py b/taiga/users/models.py index 99ee552c..d8f5f336 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -17,32 +17,83 @@ from django.db import models from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import UserManager, AbstractUser +from django.contrib.auth.models import UserManager, AbstractBaseUser +from django.core import validators +from django.utils import timezone from taiga.base.utils.slug import slugify_uniquely import random +import re def generate_random_hex_color(): return "#{:06x}".format(random.randint(0,0xFFFFFF)) -class User(AbstractUser): +class PermissionsMixin(models.Model): + """ + A mixin class that adds the fields and methods necessary to support + Django's Permission model using the ModelBackend. + """ + is_superuser = models.BooleanField(_('superuser status'), default=False, + help_text=_('Designates that this user has all permissions without ' + 'explicitly assigning them.')) + + class Meta: + abstract = True + + def has_perm(self, perm, obj=None): + """ + Returns True if the user is superadmin and is active + """ + return self.is_active and self.is_superuser + + def has_perms(self, perm_list, obj=None): + """ + Returns True if the user is superadmin and is active + """ + return self.is_active and self.is_superuser + + def has_module_perms(self, app_label): + """ + Returns True if the user is superadmin and is active + """ + return self.is_active and self.is_superuser + + +class User(AbstractBaseUser, PermissionsMixin): + username = models.CharField(_('username'), max_length=30, unique=True, + help_text=_('Required. 30 characters or fewer. Letters, numbers and ' + '/./-/_ characters'), + validators=[ + validators.RegexValidator(re.compile('^[\w.-]+$'), _('Enter a valid username.'), 'invalid') + ]) + email = models.EmailField(_('email address'), blank=True) + is_active = models.BooleanField(_('active'), default=True, + help_text=_('Designates whether this user should be treated as ' + 'active. Unselect this instead of deleting accounts.')) + + full_name = models.CharField(_('full name'), max_length=256, blank=True) color = models.CharField(max_length=9, null=False, blank=True, default=generate_random_hex_color, verbose_name=_("color")) - description = models.TextField(null=False, blank=True, - verbose_name=_("description")) - photo = models.FileField(upload_to="files/msg", max_length=500, null=True, blank=True, + bio = models.TextField(null=False, blank=True, default="", verbose_name=_("biography")) + photo = models.FileField(upload_to="users/photo", max_length=500, null=True, blank=True, verbose_name=_("photo")) + date_joined = models.DateTimeField(_('date joined'), default=timezone.now) default_language = models.CharField(max_length=20, null=False, blank=True, default="", verbose_name=_("default language")) default_timezone = models.CharField(max_length=20, null=False, blank=True, default="", verbose_name=_("default timezone")) - token = models.CharField(max_length=200, null=True, blank=True, default=None, - verbose_name=_("token")) colorize_tags = models.BooleanField(null=False, blank=True, default=False, verbose_name=_("colorize tags")) + token = models.CharField(max_length=200, null=True, blank=True, default=None, + verbose_name=_("token")) + github_id = models.IntegerField(null=True, blank=True, verbose_name=_("github ID")) + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email'] + objects = UserManager() class Meta: @@ -56,8 +107,12 @@ class User(AbstractUser): def __str__(self): return self.get_full_name() + def get_short_name(self): + "Returns the short name for the user." + return self.username + def get_full_name(self): - return super().get_full_name() or self.username or self.email + return self.full_name or self.username or self.email class Role(models.Model): @@ -104,4 +159,3 @@ def role_post_save(sender, instance, created, **kwargs): unique_projects = set(map(lambda x: x.project, instance.memberships.all())) for project in unique_projects: project.update_role_points() - diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 7a4e09de..58d40489 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -15,7 +15,6 @@ # along with this program. If not, see . from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import Permission from rest_framework import serializers @@ -23,20 +22,12 @@ from taiga.projects.models import Project from .models import User, Role -class PermissionSerializer(serializers.ModelSerializer): - class Meta: - model = Permission - fields = ("id", "name", "codename") - - class UserSerializer(serializers.ModelSerializer): - full_name = serializers.CharField(source="get_full_name", required=False) - class Meta: model = User - fields = ("id", "username", "first_name", "last_name", "full_name", "email", - "color", "description", "default_language", "default_timezone", - "is_active", "photo",) + fields = ('id', 'username', 'full_name', 'email', 'github_id', + 'color', 'bio', 'default_language', 'default_timezone', + 'is_active', 'photo') class RecoverySerializer(serializers.Serializer): diff --git a/taiga/users/services.py b/taiga/users/services.py index decae640..a8349c58 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -19,12 +19,14 @@ This model contains a domain logic for users application. """ from django.db.models.loading import get_model +from django.db.models import Q + from taiga.base import exceptions as exc def get_and_validate_user(*, username:str, password:str) -> bool: """ - Check if user with username exists and specified + Check if user with username/email exists and specified password matchs well with existing user password. if user is valid, user is returned else, corresponding @@ -32,7 +34,8 @@ def get_and_validate_user(*, username:str, password:str) -> bool: """ user_model = get_model("users", "User") - qs = user_model.objects.filter(username=username) + qs = user_model.objects.filter(Q(username=username) | + Q(email=username)) if len(qs) == 0: raise exc.WrongArguments("Username or password does not matches user.") diff --git a/taiga/users/tests/__init__.py b/taiga/users/tests/__init__.py index faf2c90a..1a9dcfcb 100644 --- a/taiga/users/tests/__init__.py +++ b/taiga/users/tests/__init__.py @@ -25,8 +25,7 @@ def create_user(id, save=True, is_superuser=False): instance = model( username="user{0}".format(id), email="user{0}@taiga.io".format(id), - first_name="Foo{0}".format(id), - last_name="Bar{0}".format(id) + full_name="Foo{0} Bar{0}".format(id) ) instance.set_password(instance.username) @@ -46,14 +45,3 @@ def create_user(id, save=True, is_superuser=False): dm.save() return instance - - -def create_domain(name, public_register=False): - domain_model = get_model("domains", "Domain") - - instance = domain_model(name=name, - domain=name, - public_register=public_register) - - instance.save() - return instance diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index fe1054c9..ec11aac6 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -1,8 +1,13 @@ import pytest +from unittest.mock import patch, Mock +from django.db.models.loading import get_model from django.core.urlresolvers import reverse from .. import factories +from taiga.base.connectors import github + + pytestmark = pytest.mark.django_db @@ -10,8 +15,7 @@ pytestmark = pytest.mark.django_db def register_form(): return {"username": "username", "password": "password", - "first_name": "fname", - "last_name": "lname", + "full_name": "fname", "email": "user@email.com", "type": "public"} @@ -41,3 +45,59 @@ def test_respond_201_if_domain_allows_public_registration(client, register_form) response = client.post(reverse("auth-register"), register_form) assert response.status_code == 201 + + +def test_response_200_in_registration_with_github_account(client): + form = {"type": "github", + "code": "xxxxxx"} + + with patch("taiga.base.connectors.github.me") as m_me: + m_me.return_value = ("mmcfly@bttf.com", + github.User(id=1955, + username="mmcfly", + full_name="martin seamus mcfly", + bio="time traveler")) + + response = client.post(reverse("auth-list"), form) + assert response.status_code == 200 + assert response.data["username"] == "mmcfly" + assert response.data["auth_token"] != "" and response.data["auth_token"] != None + assert response.data["email"] == "mmcfly@bttf.com" + assert response.data["full_name"] == "martin seamus mcfly" + assert response.data["bio"] == "time traveler" + assert response.data["github_id"] == 1955 + + +def test_response_200_in_registration_with_github_account_in_a_project(client): + membership_model = get_model("projects", "Membership") + membership = factories.MembershipFactory(user=None) + form = {"type": "github", + "code": "xxxxxx", + "token": membership.token} + + with patch("taiga.base.connectors.github.me") as m_me: + m_me.return_value = ("mmcfly@bttf.com", + github.User(id=1955, + username="mmcfly", + full_name="martin seamus mcfly", + bio="time traveler")) + + response = client.post(reverse("auth-list"), form) + assert response.status_code == 200 + assert membership_model.objects.get(token=form["token"]).user.username == "mmcfly" + + +def test_response_404_in_registration_with_github_account_in_a_project_with_invalid_token(client): + form = {"type": "github", + "code": "xxxxxx", + "token": "123456"} + + with patch("taiga.base.connectors.github.me") as m_me: + m_me.return_value = ("mmcfly@bttf.com", + github.User(id=1955, + username="mmcfly", + full_name="martin seamus mcfly", + bio="time traveler")) + + response = client.post(reverse("auth-list"), form) + assert response.status_code == 404 diff --git a/tests/integration/test_mdrender.py b/tests/integration/test_mdrender.py index cab7b28d..121cf38d 100644 --- a/tests/integration/test_mdrender.py +++ b/tests/integration/test_mdrender.py @@ -14,7 +14,7 @@ dummy_project.slug = "test" def test_proccessor_valid_user_mention(): - factories.UserFactory(username="user1", first_name="test", last_name="name") + factories.UserFactory(username="user1", full_name="test name") result = render(dummy_project, "**@user1**") expected_result = "

@user1

" assert result == expected_result @@ -26,6 +26,6 @@ def test_proccessor_invalid_user_mention(): def test_render_and_extract_mentions(): - user = factories.UserFactory(username="user1", first_name="test", last_name="name") + user = factories.UserFactory(username="user1", full_name="test") (_, extracted) = render_and_extract(dummy_project, "**@user1**") assert extracted['mentions'] == [user] diff --git a/tests/integration/test_neighbors.py b/tests/integration/test_neighbors.py index a55dd267..5b91309e 100644 --- a/tests/integration/test_neighbors.py +++ b/tests/integration/test_neighbors.py @@ -22,12 +22,12 @@ def teardown_module(): class TestGetAttribute: def test_no_attribute(self, object): - object.first_name = "name" + object.full_name = "name" with pytest.raises(AttributeError): n.get_attribute(object, "name") with pytest.raises(AttributeError): - n.get_attribute(object, "first_name__last_name") + n.get_attribute(object, "full_name__last_name") def test_one_level(self, object): object.name = "name" @@ -35,14 +35,14 @@ class TestGetAttribute: def test_two_levels(self, object): object.name = object - object.name.first_name = "first name" - assert n.get_attribute(object, "name__first_name") == object.name.first_name + object.name.full_name = "first name" + assert n.get_attribute(object, "name__full_name") == object.name.full_name def test_three_levels(self, object): object.info = object object.info.name = object - object.info.name.first_name = "first name" - assert n.get_attribute(object, "info__name__first_name") == object.info.name.first_name + object.info.name.full_name = "first name" + assert n.get_attribute(object, "info__name__full_name") == object.info.name.full_name def test_transform_field_into_lookup(): @@ -237,14 +237,14 @@ class TestIssues: def test_ordering_by_owner(self): project = f.ProjectFactory.create() - owner1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") - owner2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") + owner1 = f.UserFactory.create(full_name="Chuck Norris") + owner2 = f.UserFactory.create(full_name="George Of The Jungle") issue1 = f.IssueFactory.create(project=project, owner=owner2) issue2 = f.IssueFactory.create(project=project, owner=owner1) issue3 = f.IssueFactory.create(project=project, owner=owner1) - issues = Issue.objects.filter(project=project).order_by("owner__first_name") + issues = Issue.objects.filter(project=project).order_by("owner__full_name") issue2_neighbors = n.get_neighbors(issue2, results_set=issues) issue3_neighbors = n.get_neighbors(issue3, results_set=issues) @@ -256,14 +256,14 @@ class TestIssues: def test_ordering_by_owner_desc(self): project = f.ProjectFactory.create() - owner1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") - owner2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") + owner1 = f.UserFactory.create(full_name="Chuck Norris") + owner2 = f.UserFactory.create(full_name="George Of The Jungle") issue1 = f.IssueFactory.create(project=project, owner=owner2) issue2 = f.IssueFactory.create(project=project, owner=owner1) issue3 = f.IssueFactory.create(project=project, owner=owner1) - issues = Issue.objects.filter(project=project).order_by("-owner__first_name") + issues = Issue.objects.filter(project=project).order_by("-owner__full_name") issue1_neighbors = n.get_neighbors(issue1, results_set=issues) issue3_neighbors = n.get_neighbors(issue3, results_set=issues) @@ -275,14 +275,14 @@ class TestIssues: def test_ordering_by_assigned_to(self): project = f.ProjectFactory.create() - assigned_to1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") - assigned_to2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") + assigned_to1 = f.UserFactory.create(full_name="Chuck Norris") + assigned_to2 = f.UserFactory.create(full_name="George Of The Jungle") issue1 = f.IssueFactory.create(project=project, assigned_to=assigned_to2) issue2 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) issue3 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) - issues = Issue.objects.filter(project=project).order_by("assigned_to__first_name") + issues = Issue.objects.filter(project=project).order_by("assigned_to__full_name") issue2_neighbors = n.get_neighbors(issue2, results_set=issues) issue3_neighbors = n.get_neighbors(issue3, results_set=issues) @@ -294,14 +294,14 @@ class TestIssues: def test_ordering_by_assigned_to_desc(self): project = f.ProjectFactory.create() - assigned_to1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") - assigned_to2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") + assigned_to1 = f.UserFactory.create(full_name="Chuck Norris") + assigned_to2 = f.UserFactory.create(full_name="George Of The Jungle") issue1 = f.IssueFactory.create(project=project, assigned_to=assigned_to2) issue2 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) issue3 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) - issues = Issue.objects.filter(project=project).order_by("-assigned_to__first_name") + issues = Issue.objects.filter(project=project).order_by("-assigned_to__full_name") issue1_neighbors = n.get_neighbors(issue1, results_set=issues) issue3_neighbors = n.get_neighbors(issue3, results_set=issues) diff --git a/tests/unit/test_connectors_github.py b/tests/unit/test_connectors_github.py new file mode 100644 index 00000000..47c8a8e0 --- /dev/null +++ b/tests/unit/test_connectors_github.py @@ -0,0 +1,135 @@ +import pytest + +from unittest.mock import patch, Mock +from taiga.base.connectors import github +from taiga.base.connectors import exceptions as exc + + +def test_url_builder(): + assert (github._build_url("login", "authorize") == + "https://api.github.com/login/oauth/authorize") + assert (github._build_url("login","access-token") == + "https://api.github.com/login/oauth/access_token") + assert (github._build_url("user", "profile") == + "https://api.github.com/user") + assert (github._build_url("user", "emails") == + "https://api.github.com/user/emails") + + +def test_login_success(): + with patch("taiga.base.connectors.github.requests") as m_requests: + m_requests.post.return_value = m_response = Mock() + m_response.status_code = 200 + m_response.json.return_value = {"access_token": "xxxxxxxx"} + + auth_info = github.login("*access-code*", "**client-id**", "*client-secret*", github.HEADERS) + + assert auth_info.access_token == "xxxxxxxx" + m_requests.post.assert_called_once_with("https://github.com/login/oauth/access_token", + headers=github.HEADERS, + params={'code': '*access-code*', + 'scope': 'user:emails', + 'client_id': '**client-id**', + 'client_secret': '*client-secret*'}) + + +def test_login_whit_errors(): + with pytest.raises(exc.GitHubApiError) as e, \ + patch("taiga.base.connectors.github.requests") as m_requests: + m_requests.post.return_value = m_response = Mock() + m_response.status_code = 200 + m_response.json.return_value = {"error": "Invalid credentials"} + + auth_info = github.login("*access-code*", "**client-id**", "*ient-secret*", github.HEADERS) + assert e.value.status_code == 400 + assert e.value.detail["status_code"] == 200 + assert e.value.detail["error"] == "Invalid credentials" + + +def test_get_user_profile_success(): + with patch("taiga.base.connectors.github.requests") as m_requests: + m_requests.get.return_value = m_response = Mock() + m_response.status_code = 200 + m_response.json.return_value = {"id": 1955, + "login": "mmcfly", + "name": "martin seamus mcfly", + "bio": "time traveler"} + + user_profile = github.get_user_profile(github.HEADERS) + + assert user_profile.id == 1955 + assert user_profile.username == "mmcfly" + assert user_profile.full_name == "martin seamus mcfly" + assert user_profile.bio == "time traveler" + m_requests.get.assert_called_once_with("https://api.github.com/user", + headers=github.HEADERS) + + +def test_get_user_profile_whit_errors(): + with pytest.raises(exc.GitHubApiError) as e, \ + patch("taiga.base.connectors.github.requests") as m_requests: + m_requests.get.return_value = m_response = Mock() + m_response.status_code = 401 + m_response.json.return_value = {"error": "Invalid credentials"} + + auth_info = github.get_user_profile(github.HEADERS) + assert e.value.status_code == 400 + assert e.value.detail["status_code"] == 401 + assert e.value.detail["error"] == "Invalid credentials" + + +def test_get_user_emails_success(): + with patch("taiga.base.connectors.github.requests") as m_requests: + m_requests.get.return_value = m_response = Mock() + m_response.status_code = 200 + m_response.json.return_value = [{"email": "darth-vader@bttf.com", "primary": False}, + {"email": "mmcfly@bttf.com", "primary": True}] + + emails = github.get_user_emails(github.HEADERS) + + assert len(emails) == 2 + assert emails[0].email == "darth-vader@bttf.com" + assert not emails[0].is_primary + assert emails[1].email == "mmcfly@bttf.com" + assert emails[1].is_primary + m_requests.get.assert_called_once_with("https://api.github.com/user/emails", + headers=github.HEADERS) + + +def test_get_user_emails_whit_errors(): + with pytest.raises(exc.GitHubApiError) as e, \ + patch("taiga.base.connectors.github.requests") as m_requests: + m_requests.get.return_value = m_response = Mock() + m_response.status_code = 401 + m_response.json.return_value = {"error": "Invalid credentials"} + + emails = github.get_user_emails(github.HEADERS) + assert e.value.status_code == 400 + assert e.value.detail["status_code"] == 401 + assert e.value.detail["error"] == "Invalid credentials" + + +def test_me(): + with patch("taiga.base.connectors.github.login") as m_login, \ + patch("taiga.base.connectors.github.get_user_profile") as m_get_user_profile, \ + patch("taiga.base.connectors.github.get_user_emails") as m_get_user_emails: + m_login.return_value = github.AuthInfo(access_token="xxxxxxxx") + m_get_user_profile.return_value = github.User(id=1955, + username="mmcfly", + full_name="martin seamus mcfly", + bio="time traveler") + m_get_user_emails.return_value = [github.Email(email="darth-vader@bttf.com", is_primary=False), + github.Email(email="mmcfly@bttf.com", is_primary=True)] + + email, user = github.me("**access-code**") + + assert email == "mmcfly@bttf.com" + assert user.id == 1955 + assert user.username == "mmcfly" + assert user.full_name == "martin seamus mcfly" + assert user.bio == "time traveler" + + headers = github.HEADERS.copy() + headers["Authorization"] = "token xxxxxxxx" + m_get_user_profile.assert_called_once_with(headers=headers) + m_get_user_emails.assert_called_once_with(headers=headers)