Merge pull request #47 from taigaio/github-auth-2

Github auth
remotes/origin/enhancement/email-actions
David Barragán Merino 2014-06-12 12:35:49 +02:00
commit 9b6b14e6a3
28 changed files with 909 additions and 212 deletions

View File

@ -9,7 +9,7 @@ gunicorn==18.0
psycopg2==2.5.3 psycopg2==2.5.3
pytz==2014.4 pytz==2014.4
six==1.6.1 six==1.6.1
djmail==0.6 djmail==0.7
django-pgjson==0.1.2 django-pgjson==0.1.2
django-jinja==1.0.1 django-jinja==1.0.1
jinja2==2.7.2 jinja2==2.7.2

View File

@ -46,3 +46,10 @@ from .development import *
#EMAIL_HOST_USER = 'youremail@gmail.com' #EMAIL_HOST_USER = 'youremail@gmail.com'
#EMAIL_HOST_PASSWORD = 'yourpassword' #EMAIL_HOST_PASSWORD = 'yourpassword'
#EMAIL_PORT = 587 #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"

View File

@ -28,6 +28,7 @@ from rest_framework import serializers
from taiga.base.decorators import list_route from taiga.base.decorators import list_route
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
from taiga.base.connectors import github
from taiga.users.services import get_and_validate_user from taiga.users.services import get_and_validate_user
from .serializers import PublicRegisterSerializer 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_existing_user
from .services import private_register_for_new_user from .services import private_register_for_new_user
from .services import public_register from .services import public_register
from .services import github_register
from .services import make_auth_response_data from .services import make_auth_response_data
@ -130,11 +132,34 @@ class AuthViewSet(viewsets.ViewSet):
return self._private_register(request) return self._private_register(request)
raise exc.BadRequest(_("invalid register type")) raise exc.BadRequest(_("invalid register type"))
# Login view: /api/v1/auth def _login(self, request):
def create(self, request, **kwargs):
username = request.DATA.get('username', None) username = request.DATA.get('username', None)
password = request.DATA.get('password', None) password = request.DATA.get('password', None)
user = get_and_validate_user(username=username, password=password) user = get_and_validate_user(username=username, password=password)
data = make_auth_response_data(user) data = make_auth_response_data(user)
return Response(data, status=status.HTTP_200_OK) 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"))

View File

@ -17,8 +17,7 @@
from rest_framework import serializers from rest_framework import serializers
class BaseRegisterSerializer(serializers.Serializer): class BaseRegisterSerializer(serializers.Serializer):
first_name = serializers.CharField(max_length=200) full_name = serializers.CharField(max_length=256)
last_name = serializers.CharField(max_length=200)
email = serializers.EmailField(max_length=200) email = serializers.EmailField(max_length=200)
username = serializers.CharField(max_length=200) username = serializers.CharField(max_length=200)
password = serializers.CharField(min_length=4) password = serializers.CharField(min_length=4)

View File

@ -63,14 +63,18 @@ def send_private_register_email(user, **kwargs) -> bool:
return bool(email.send()) 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. Checks if a specified user is already registred.
""" """
user_model = get_model("users", "User") 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() return qs.exists()
@ -90,7 +94,7 @@ def get_membership_by_token(token:str):
@tx.atomic @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 Given a parsed parameters, try register a new user
knowing that it follows a public register flow. 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_model = get_model("users", "User")
user = user_model(username=username, user = user_model(username=username,
email=email, email=email,
first_name=first_name, full_name=full_name)
last_name=last_name)
user.set_password(password) user.set_password(password)
user.save() user.save()
@ -138,21 +141,18 @@ def private_register_for_existing_user(token:str, username:str, password:str):
@tx.atomic @tx.atomic
def private_register_for_new_user(token:str, username:str, email:str, 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 Given a inviation token, try register new user matching
the invitation token. the invitation token.
""" """
user_model = get_model("users", "User")
if is_user_already_registred(username=username, email=email): if is_user_already_registred(username=username, email=email):
raise exc.WrongArguments(_("Username or Email is already in use.")) raise exc.WrongArguments(_("Username or Email is already in use."))
user_model = get_model("users", "User")
user = user_model(username=username, user = user_model(username=username,
email=email, email=email,
first_name=first_name, full_name=full_name)
last_name=last_name)
user.set_password(password) user.set_password(password)
try: try:
@ -167,6 +167,30 @@ def private_register_for_new_user(token:str, username:str, email:str,
return user 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: def make_auth_response_data(user) -> dict:
""" """
Given a domain and user, creates data structure Given a domain and user, creates data structure

View File

@ -105,15 +105,13 @@ class AuthServicesTests(test.TestCase):
username=self.user1.username, username=self.user1.username,
password="secret", password="secret",
email=self.user1.email, email=self.user1.email,
first_name="foo", full_name="foo")
last_name="bar")
user = services.public_register(self.domain, user = services.public_register(self.domain,
username="foousername", username="foousername",
password="foosecret", password="foosecret",
email="foo@bar.ca", email="foo@bar.ca",
first_name="Foo", full_name="Foo")
last_name="Bar")
self.assertEqual(user.username, "foousername") self.assertEqual(user.username, "foousername")
self.assertTrue(user.check_password("foosecret")) self.assertTrue(user.check_password("foosecret"))
self.assertTrue(is_user_exists_on_domain(self.domain, user)) self.assertTrue(is_user_exists_on_domain(self.domain, user))
@ -153,8 +151,7 @@ class AuthServicesTests(test.TestCase):
username="user2", username="user2",
password="user2", password="user2",
email="user2@bar.ca", email="user2@bar.ca",
first_name="Foo", full_name="Foo")
last_name="Bar")
membership = membership.__class__.objects.get(pk=membership.pk) membership = membership.__class__.objects.get(pk=membership.pk)
@ -195,8 +192,7 @@ class RegisterApiTests(test.TestCase):
data = { data = {
"username": "pepe", "username": "pepe",
"password": "pepepepe", "password": "pepepepe",
"first_name": "pepe", "full_name": "pepe",
"last_name": "pepe",
"email": "pepe@pepe.com", "email": "pepe@pepe.com",
"type": "public", "type": "public",
} }
@ -213,8 +209,7 @@ class RegisterApiTests(test.TestCase):
data = { data = {
"username": "pepe", "username": "pepe",
"password": "pepepepe", "password": "pepepepe",
"first_name": "pepe", "full_name": "pepe",
"last_name": "pepe",
"email": "pepe@pepe.com", "email": "pepe@pepe.com",
"type": "public", "type": "public",
} }
@ -227,8 +222,7 @@ class RegisterApiTests(test.TestCase):
data = { data = {
"username": "pepe", "username": "pepe",
"password": "pepepepe", "password": "pepepepe",
"first_name": "pepe", "full_name": "pepe",
"last_name": "pepe",
"email": "pepe@pepe.com", "email": "pepe@pepe.com",
"type": "private", "type": "private",
} }
@ -243,8 +237,7 @@ class RegisterApiTests(test.TestCase):
data = { data = {
"username": "pepe", "username": "pepe",
"password": "pepepepe", "password": "pepepepe",
"first_name": "pepe", "full_name": "pepe",
"last_name": "pepe",
"email": "pepe@pepe.com", "email": "pepe@pepe.com",
"type": "private", "type": "private",
"existing": False, "existing": False,

View File

View File

@ -0,0 +1,27 @@
# 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.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

View File

@ -0,0 +1,160 @@
# 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 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

View File

@ -96,8 +96,7 @@ class IssuesOrdering(filters.FilterBackend):
if order_by in ['owner', '-owner', 'assigned_to', '-assigned_to']: if order_by in ['owner', '-owner', 'assigned_to', '-assigned_to']:
return queryset.order_by( return queryset.order_by(
'{}__first_name'.format(order_by), '{}__full_name'.format(order_by)
'{}__last_name'.format(order_by)
) )
return queryset return queryset

View File

@ -290,8 +290,7 @@ class Command(BaseCommand):
def create_user(self, counter): def create_user(self, counter):
user = User.objects.create( user = User.objects.create(
username='user-{0}'.format(counter), username='user-{0}'.format(counter),
first_name=self.sd.name('es'), full_name="{} {}".format(self.sd.name('es'), self.sd.surname('es', number=1)),
last_name=self.sd.surname('es', number=1),
email=self.sd.email(), email=self.sd.email(),
token=''.join(random.sample('abcdef0123456789', 10)), token=''.join(random.sample('abcdef0123456789', 10)),
color=self.sd.choice(COLOR_CHOICES)) color=self.sd.choice(COLOR_CHOICES))

View File

@ -109,7 +109,7 @@ class ProjectDetailSerializer(ProjectSerializer):
issue_types = IssueTypeSerializer(many=True, required=False) issue_types = IssueTypeSerializer(many=True, required=False)
def get_membership(self, obj): 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") qs = qs.select_related("role", "user")
serializer = ProjectMembershipSerializer(qs, many=True) serializer = ProjectMembershipSerializer(qs, many=True)
@ -117,7 +117,7 @@ class ProjectDetailSerializer(ProjectSerializer):
def get_active_membership(self, obj): def get_active_membership(self, obj):
qs = obj.memberships.filter(user__isnull=False) 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") qs = qs.select_related("role", "user")
serializer = ProjectMembershipSerializer(qs, many=True) serializer = ProjectMembershipSerializer(qs, many=True)

View File

@ -73,7 +73,7 @@ class ProfileTestCase(test.TestCase):
password=self.user3.username) password=self.user3.username)
self.assertTrue(response) self.assertTrue(response)
data = {"first_name": "Foo Bar"} data = {"full_name": "Foo Bar"}
response = self.client.patch( response = self.client.patch(
reverse("users-detail", args=[self.user2.pk]), reverse("users-detail", args=[self.user2.pk]),
@ -86,7 +86,7 @@ class ProfileTestCase(test.TestCase):
password=self.user3.username) password=self.user3.username)
self.assertTrue(response) self.assertTrue(response)
data = {"first_name": "Foo Bar"} data = {"full_name": "Foo Bar"}
response = self.client.patch( response = self.client.patch(
reverse("users-detail", args=[self.user3.pk]), reverse("users-detail", args=[self.user3.pk]),
content_type="application/json", content_type="application/json",
@ -99,7 +99,7 @@ class ProfileTestCase(test.TestCase):
password=self.user1.username) password=self.user1.username)
self.assertTrue(response) self.assertTrue(response)
data = {"first_name": "Foo Bar"} data = {"full_name": "Foo Bar"}
response = self.client.patch( response = self.client.patch(
reverse("users-detail", args=[self.user3.pk]), reverse("users-detail", args=[self.user3.pk]),
content_type="application/json", content_type="application/json",
@ -112,7 +112,7 @@ class ProfileTestCase(test.TestCase):
password=self.user3.username) password=self.user3.username)
self.assertTrue(response) self.assertTrue(response)
data = {"first_name": "Foo Bar"} data = {"full_name": "Foo Bar"}
response = self.client.delete( response = self.client.delete(
reverse("users-detail", args=[self.user2.pk])) reverse("users-detail", args=[self.user2.pk]))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@ -122,7 +122,7 @@ class ProfileTestCase(test.TestCase):
password=self.user3.username) password=self.user3.username)
self.assertTrue(response) self.assertTrue(response)
data = {"first_name": "Foo Bar"} data = {"full_name": "Foo Bar"}
response = self.client.delete( response = self.client.delete(
reverse("users-detail", args=[self.user3.pk])) reverse("users-detail", args=[self.user3.pk]))
@ -134,7 +134,7 @@ class ProfileTestCase(test.TestCase):
password=self.user1.username) password=self.user1.username)
self.assertTrue(response) self.assertTrue(response)
data = {"first_name": "Foo Bar"} data = {"full_name": "Foo Bar"}
response = self.client.delete( response = self.client.delete(
reverse("users-detail", args=[self.user3.pk])) reverse("users-detail", args=[self.user3.pk]))

View File

@ -8,4 +8,4 @@ class VoterSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ('id', 'username', 'first_name', 'last_name', 'full_name') fields = ('id', 'username', 'full_name')

View File

@ -20,11 +20,9 @@ router = routers.DefaultRouter(trailing_slash=False)
# taiga.users # taiga.users
from taiga.users.api import UsersViewSet from taiga.users.api import UsersViewSet
from taiga.users.api import PermissionsViewSet
from taiga.auth.api import AuthViewSet from taiga.auth.api import AuthViewSet
router.register(r"users", UsersViewSet, base_name="users") router.register(r"users", UsersViewSet, base_name="users")
router.register(r"permissions", PermissionsViewSet, base_name="permissions")
router.register(r"auth", AuthViewSet, base_name="auth") router.register(r"auth", AuthViewSet, base_name="auth")

View File

@ -47,20 +47,19 @@ admin.site.register(Role, RoleAdmin)
class UserAdmin(DjangoUserAdmin): class UserAdmin(DjangoUserAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (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')}), (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags')}),
(_('Notifications info'), {'fields': ("notify_level", "notify_changes_by_me",)}), (_('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')}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
) )
form = UserChangeForm form = UserChangeForm
add_form = UserCreationForm add_form = UserCreationForm
list_display = ('username', 'email', 'full_name')
list_filter = ('is_superuser', 'is_active')
class PermissionAdmin(admin.ModelAdmin): search_fields = ('username', 'full_name', 'email')
list_display = ['name', 'content_type', 'codename'] ordering = ('username',)
list_filter = ['content_type'] filter_horizontal = ()
class RoleInline(admin.TabularInline): class RoleInline(admin.TabularInline):
model = Role model = Role
@ -68,4 +67,3 @@ class RoleInline(admin.TabularInline):
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(Permission, PermissionAdmin)

View File

@ -20,7 +20,6 @@ from django.db.models.loading import get_model
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.contrib.auth import logout, login, authenticate from django.contrib.auth import logout, login, authenticate
from django.contrib.auth.models import Permission
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.response import Response 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 taiga.base.api import ModelCrudViewSet, RetrieveModelMixin, ModelListViewSet
from .models import User, Role from .models import User, Role
from .serializers import UserSerializer, RecoverySerializer, PermissionSerializer from .serializers import UserSerializer, RecoverySerializer
class MembersFilterBackend(BaseFilterBackend): class MembersFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
@ -53,27 +53,6 @@ class MembersFilterBackend(BaseFilterBackend):
else: else:
return queryset.filter(pk=request.user.id) 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): class UsersViewSet(ModelCrudViewSet):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)

View File

@ -4,9 +4,8 @@
"model": "users.user", "model": "users.user",
"fields": { "fields": {
"username": "admin", "username": "admin",
"first_name": "", "full_name": "",
"last_name": "", "bio": "",
"description": "",
"default_language": "", "default_language": "",
"color": "", "color": "",
"photo": "", "photo": "",
@ -15,10 +14,8 @@
"default_timezone": "", "default_timezone": "",
"is_superuser": true, "is_superuser": true,
"token": "", "token": "",
"is_staff": true, "github_id": null,
"last_login": "2013-04-04T07:36:09.880Z", "last_login": "2013-04-04T07:36:09.880Z",
"groups": [],
"user_permissions": [],
"password": "pbkdf2_sha256$10000$oRIbCKOL1U3w$/gaYMnOlc/GnN4mn3UUXvXpk2Hx0vvht6Uqhu46aikI=", "password": "pbkdf2_sha256$10000$oRIbCKOL1U3w$/gaYMnOlc/GnN4mn3UUXvXpk2Hx0vvht6Uqhu46aikI=",
"email": "niwi@niwi.be", "email": "niwi@niwi.be",
"date_joined": "2013-04-01T13:48:21.711Z" "date_joined": "2013-04-01T13:48:21.711Z"

View File

@ -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']

View File

@ -8,48 +8,28 @@ from django.db import models
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Deleting field 'User.notify_changes_by_me' db.rename_column(u'users_user', 'description', 'bio')
db.delete_column('users_user', 'notify_changes_by_me')
# Deleting field 'User.notify_level'
db.delete_column('users_user', 'notify_level')
def backwards(self, orm): def backwards(self, orm):
# Adding field 'User.notify_changes_by_me' db.rename_column(u'users_user', 'bio', 'description')
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)
models = { 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': { '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'}), 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}, },
'contenttypes.contenttype': { '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'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}, },
'projects.issuestatus': { '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'}), 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), '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']"}) 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_statuses'", 'to': "orm['projects.Project']"})
}, },
'projects.issuetype': { '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'}), 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), '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']"}) 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_types'", 'to': "orm['projects.Project']"})
}, },
'projects.membership': { 'projects.membership': {
'Meta': {'object_name': 'Membership', 'unique_together': "(('user', 'project'),)", 'ordering': "['project', 'role']"}, 'Meta': {'unique_together': "(('user', 'project'),)", 'object_name': 'Membership', 'ordering': "['project', 'role']"},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'default': 'datetime.datetime.now', 'blank': 'True'}), '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'}), 'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'null': 'True', 'blank': 'True', 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['projects.Project']"}), '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']"}), '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'}), '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'", 'to': "orm['users.User']", 'null': 'True', 'blank': 'True'}) 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'memberships'", 'null': 'True', 'blank': 'True', 'to': "orm['users.User']"})
}, },
'projects.points': { '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'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), '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'}) 'value': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True', 'blank': 'True'})
}, },
'projects.priority': { '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'}), 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
@ -93,36 +73,36 @@ class Migration(SchemaMigration):
}, },
'projects.project': { 'projects.project': {
'Meta': {'object_name': 'Project', 'ordering': "['name']"}, 'Meta': {'object_name': 'Project', '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'}),
'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'projects'", 'to': "orm['projects.ProjectTemplate']", 'null': 'True', 'blank': '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', [], {'related_name': "'+'", 'to': "orm['projects.IssueStatus']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': 'True'}), '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', [], {'related_name': "'+'", 'to': "orm['projects.IssueType']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': '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', [], {'related_name': "'+'", 'to': "orm['projects.Points']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': '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', [], {'related_name': "'+'", 'to': "orm['projects.Priority']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': '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', [], {'related_name': "'+'", 'to': "orm['projects.Severity']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': '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', [], {'related_name': "'+'", 'to': "orm['projects.TaskStatus']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': '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', [], {'related_name': "'+'", 'to': "orm['projects.UserStoryStatus']", 'blank': 'True', 'unique': 'True', 'on_delete': 'models.SET_NULL', 'null': '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', [], {}), 'description': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_backlog_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_backlog_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_issues_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_kanban_activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_wiki_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), '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'}), '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']"}), 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'owned_projects'", 'to': "orm['users.User']"}),
'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), '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'}), 'tags': ('picklefield.fields.PickledObjectField', [], {'blank': 'True'}),
'total_milestones': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', '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'}), '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': ('django.db.models.fields.CharField', [], {'null': 'True', 'blank': 'True', 'max_length': '250'}),
'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}) 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'null': 'True', 'blank': 'True', 'max_length': '250'})
}, },
'projects.projecttemplate': { 'projects.projecttemplate': {
'Meta': {'object_name': 'ProjectTemplate', 'ordering': "['name']"}, '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_options': ('django_pgjson.fields.JsonField', [], {}),
'default_owner_role': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 'default_owner_role': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'description': ('django.db.models.fields.TextField', [], {}), 'description': ('django.db.models.fields.TextField', [], {}),
@ -139,14 +119,14 @@ class Migration(SchemaMigration):
'priorities': ('django_pgjson.fields.JsonField', [], {}), 'priorities': ('django_pgjson.fields.JsonField', [], {}),
'roles': ('django_pgjson.fields.JsonField', [], {}), 'roles': ('django_pgjson.fields.JsonField', [], {}),
'severities': ('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', [], {}), 'task_statuses': ('django_pgjson.fields.JsonField', [], {}),
'us_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': ('django.db.models.fields.CharField', [], {'null': 'True', 'blank': 'True', 'max_length': '250'}),
'videoconferences_salt': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}) 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'null': 'True', 'blank': 'True', 'max_length': '250'})
}, },
'projects.severity': { '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'}), 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), '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']"}) 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'severities'", 'to': "orm['projects.Project']"})
}, },
'projects.taskstatus': { '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'}), 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), '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']"}) 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_statuses'", 'to': "orm['projects.Project']"})
}, },
'projects.userstorystatus': { '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'}), 'color': ('django.db.models.fields.CharField', [], {'default': "'#999999'", 'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), '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'}) 'wip_limit': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'})
}, },
'users.role': { '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'}), 'computable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'to': "orm['auth.Permission']", 'symmetrical': 'False'}), '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']"}), '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': { 'users.user': {
'Meta': {'object_name': 'User', 'ordering': "['username']"}, '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'}), 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), '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_language': ('django.db.models.fields.CharField', [], {'default': "''", 'blank': 'True', 'max_length': '20'}),
'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}), 'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'blank': 'True', 'max_length': '20'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 'full_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '256'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'github_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'user_set'", 'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': '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'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), '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'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'photo': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), 'photo': ('django.db.models.fields.files.FileField', [], {'null': 'True', 'blank': 'True', 'max_length': '500'}),
'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}), 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'null': 'True', 'blank': 'True', 'max_length': '200'}),
'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', [], {'max_length': '30', 'unique': 'True'})
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
} }
} }
complete_apps = ['users'] complete_apps = ['users']

View File

@ -17,32 +17,83 @@
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ 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 from taiga.base.utils.slug import slugify_uniquely
import random import random
import re
def generate_random_hex_color(): def generate_random_hex_color():
return "#{:06x}".format(random.randint(0,0xFFFFFF)) 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, color = models.CharField(max_length=9, null=False, blank=True, default=generate_random_hex_color,
verbose_name=_("color")) verbose_name=_("color"))
description = models.TextField(null=False, blank=True, bio = models.TextField(null=False, blank=True, default="", verbose_name=_("biography"))
verbose_name=_("description")) photo = models.FileField(upload_to="users/photo", max_length=500, null=True, blank=True,
photo = models.FileField(upload_to="files/msg", max_length=500, null=True, blank=True,
verbose_name=_("photo")) verbose_name=_("photo"))
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
default_language = models.CharField(max_length=20, null=False, blank=True, default="", default_language = models.CharField(max_length=20, null=False, blank=True, default="",
verbose_name=_("default language")) verbose_name=_("default language"))
default_timezone = models.CharField(max_length=20, null=False, blank=True, default="", default_timezone = models.CharField(max_length=20, null=False, blank=True, default="",
verbose_name=_("default timezone")) 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, colorize_tags = models.BooleanField(null=False, blank=True, default=False,
verbose_name=_("colorize tags")) 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() objects = UserManager()
class Meta: class Meta:
@ -56,8 +107,12 @@ class User(AbstractUser):
def __str__(self): def __str__(self):
return self.get_full_name() 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): 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): 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())) unique_projects = set(map(lambda x: x.project, instance.memberships.all()))
for project in unique_projects: for project in unique_projects:
project.update_role_points() project.update_role_points()

View File

@ -15,7 +15,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import Permission
from rest_framework import serializers from rest_framework import serializers
@ -23,20 +22,12 @@ from taiga.projects.models import Project
from .models import User, Role from .models import User, Role
class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
fields = ("id", "name", "codename")
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source="get_full_name", required=False)
class Meta: class Meta:
model = User model = User
fields = ("id", "username", "first_name", "last_name", "full_name", "email", fields = ('id', 'username', 'full_name', 'email', 'github_id',
"color", "description", "default_language", "default_timezone", 'color', 'bio', 'default_language', 'default_timezone',
"is_active", "photo",) 'is_active', 'photo')
class RecoverySerializer(serializers.Serializer): class RecoverySerializer(serializers.Serializer):

View File

@ -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.loading import get_model
from django.db.models import Q
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
def get_and_validate_user(*, username:str, password:str) -> bool: 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. password matchs well with existing user password.
if user is valid, user is returned else, corresponding 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") 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: if len(qs) == 0:
raise exc.WrongArguments("Username or password does not matches user.") raise exc.WrongArguments("Username or password does not matches user.")

View File

@ -25,8 +25,7 @@ def create_user(id, save=True, is_superuser=False):
instance = model( instance = model(
username="user{0}".format(id), username="user{0}".format(id),
email="user{0}@taiga.io".format(id), email="user{0}@taiga.io".format(id),
first_name="Foo{0}".format(id), full_name="Foo{0} Bar{0}".format(id)
last_name="Bar{0}".format(id)
) )
instance.set_password(instance.username) instance.set_password(instance.username)
@ -46,14 +45,3 @@ def create_user(id, save=True, is_superuser=False):
dm.save() dm.save()
return instance 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

View File

@ -1,8 +1,13 @@
import pytest import pytest
from unittest.mock import patch, Mock
from django.db.models.loading import get_model
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from .. import factories from .. import factories
from taiga.base.connectors import github
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -10,8 +15,7 @@ pytestmark = pytest.mark.django_db
def register_form(): def register_form():
return {"username": "username", return {"username": "username",
"password": "password", "password": "password",
"first_name": "fname", "full_name": "fname",
"last_name": "lname",
"email": "user@email.com", "email": "user@email.com",
"type": "public"} "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) response = client.post(reverse("auth-register"), register_form)
assert response.status_code == 201 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

View File

@ -14,7 +14,7 @@ dummy_project.slug = "test"
def test_proccessor_valid_user_mention(): 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**") result = render(dummy_project, "**@user1**")
expected_result = "<p><strong><a alt=\"test name\" class=\"mention\" href=\"/#/profile/user1\" title=\"test name\">&commat;user1</a></strong></p>" expected_result = "<p><strong><a alt=\"test name\" class=\"mention\" href=\"/#/profile/user1\" title=\"test name\">&commat;user1</a></strong></p>"
assert result == expected_result assert result == expected_result
@ -26,6 +26,6 @@ def test_proccessor_invalid_user_mention():
def test_render_and_extract_mentions(): 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**") (_, extracted) = render_and_extract(dummy_project, "**@user1**")
assert extracted['mentions'] == [user] assert extracted['mentions'] == [user]

View File

@ -22,12 +22,12 @@ def teardown_module():
class TestGetAttribute: class TestGetAttribute:
def test_no_attribute(self, object): def test_no_attribute(self, object):
object.first_name = "name" object.full_name = "name"
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
n.get_attribute(object, "name") n.get_attribute(object, "name")
with pytest.raises(AttributeError): 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): def test_one_level(self, object):
object.name = "name" object.name = "name"
@ -35,14 +35,14 @@ class TestGetAttribute:
def test_two_levels(self, object): def test_two_levels(self, object):
object.name = object object.name = object
object.name.first_name = "first name" object.name.full_name = "first name"
assert n.get_attribute(object, "name__first_name") == object.name.first_name assert n.get_attribute(object, "name__full_name") == object.name.full_name
def test_three_levels(self, object): def test_three_levels(self, object):
object.info = object object.info = object
object.info.name = object object.info.name = object
object.info.name.first_name = "first name" object.info.name.full_name = "first name"
assert n.get_attribute(object, "info__name__first_name") == object.info.name.first_name assert n.get_attribute(object, "info__name__full_name") == object.info.name.full_name
def test_transform_field_into_lookup(): def test_transform_field_into_lookup():
@ -237,14 +237,14 @@ class TestIssues:
def test_ordering_by_owner(self): def test_ordering_by_owner(self):
project = f.ProjectFactory.create() project = f.ProjectFactory.create()
owner1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") owner1 = f.UserFactory.create(full_name="Chuck Norris")
owner2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") owner2 = f.UserFactory.create(full_name="George Of The Jungle")
issue1 = f.IssueFactory.create(project=project, owner=owner2) issue1 = f.IssueFactory.create(project=project, owner=owner2)
issue2 = f.IssueFactory.create(project=project, owner=owner1) issue2 = f.IssueFactory.create(project=project, owner=owner1)
issue3 = 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) issue2_neighbors = n.get_neighbors(issue2, results_set=issues)
issue3_neighbors = n.get_neighbors(issue3, 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): def test_ordering_by_owner_desc(self):
project = f.ProjectFactory.create() project = f.ProjectFactory.create()
owner1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") owner1 = f.UserFactory.create(full_name="Chuck Norris")
owner2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") owner2 = f.UserFactory.create(full_name="George Of The Jungle")
issue1 = f.IssueFactory.create(project=project, owner=owner2) issue1 = f.IssueFactory.create(project=project, owner=owner2)
issue2 = f.IssueFactory.create(project=project, owner=owner1) issue2 = f.IssueFactory.create(project=project, owner=owner1)
issue3 = 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) issue1_neighbors = n.get_neighbors(issue1, results_set=issues)
issue3_neighbors = n.get_neighbors(issue3, 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): def test_ordering_by_assigned_to(self):
project = f.ProjectFactory.create() project = f.ProjectFactory.create()
assigned_to1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") assigned_to1 = f.UserFactory.create(full_name="Chuck Norris")
assigned_to2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") assigned_to2 = f.UserFactory.create(full_name="George Of The Jungle")
issue1 = f.IssueFactory.create(project=project, assigned_to=assigned_to2) issue1 = f.IssueFactory.create(project=project, assigned_to=assigned_to2)
issue2 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) issue2 = f.IssueFactory.create(project=project, assigned_to=assigned_to1)
issue3 = 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) issue2_neighbors = n.get_neighbors(issue2, results_set=issues)
issue3_neighbors = n.get_neighbors(issue3, 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): def test_ordering_by_assigned_to_desc(self):
project = f.ProjectFactory.create() project = f.ProjectFactory.create()
assigned_to1 = f.UserFactory.create(first_name="Chuck", last_name="Norris") assigned_to1 = f.UserFactory.create(full_name="Chuck Norris")
assigned_to2 = f.UserFactory.create(first_name="George", last_name="Of The Jungle") assigned_to2 = f.UserFactory.create(full_name="George Of The Jungle")
issue1 = f.IssueFactory.create(project=project, assigned_to=assigned_to2) issue1 = f.IssueFactory.create(project=project, assigned_to=assigned_to2)
issue2 = f.IssueFactory.create(project=project, assigned_to=assigned_to1) issue2 = f.IssueFactory.create(project=project, assigned_to=assigned_to1)
issue3 = 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) issue1_neighbors = n.get_neighbors(issue1, results_set=issues)
issue3_neighbors = n.get_neighbors(issue3, results_set=issues) issue3_neighbors = n.get_neighbors(issue3, results_set=issues)

View File

@ -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)