Merge branch 'master' into stable

remotes/origin/enhancement/email-actions 1.3.0
Alejandro Alonso 2014-11-18 16:05:25 +01:00
commit 57777fc54c
14 changed files with 209 additions and 38 deletions

View File

@ -1,6 +1,6 @@
# Changelog # # Changelog #
## 1.3.0 Dryas hookeriana (Unreleased) ## 1.3.0 Dryas hookeriana (2014-11-18)
### Features ### Features
- GitHub integration (Phase I): - GitHub integration (Phase I):

View File

@ -1 +1,17 @@
# 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 . import celery from . import celery

View File

@ -34,6 +34,7 @@ from djmail.template_mail import MagicMailBuilder
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
from taiga.users.serializers import UserSerializer from taiga.users.serializers import UserSerializer
from taiga.users.services import get_and_validate_user from taiga.users.services import get_and_validate_user
from taiga.base.utils.slug import slugify_uniquely
from .tokens import get_token_for_user from .tokens import get_token_for_user
from .signals import user_registered as user_registered_signal from .signals import user_registered as user_registered_signal
@ -50,7 +51,7 @@ def send_register_email(user) -> bool:
return bool(email.send()) return bool(email.send())
def is_user_already_registered(*, username:str, email:str, github_id:int=None) -> (bool, str): def is_user_already_registered(*, username:str, email:str) -> (bool, str):
""" """
Checks if a specified user is already registred. Checks if a specified user is already registred.
@ -65,9 +66,6 @@ def is_user_already_registered(*, username:str, email:str, github_id:int=None) -
if user_model.objects.filter(email=email): if user_model.objects.filter(email=email):
return (True, _("Email is already in use.")) return (True, _("Email is already in use."))
if github_id and user_model.objects.filter(github_id=github_id):
return (True, _("GitHub id is already in use"))
return (False, None) return (False, None)
@ -182,20 +180,33 @@ def github_register(username:str, email:str, full_name:str, github_id:int, bio:s
:returns: User :returns: User
""" """
user_model = apps.get_model("users", "User") user_model = apps.get_model("users", "User")
user, created = user_model.objects.get_or_create(github_id=github_id,
defaults={"username": username, try:
"email": email, # Github user association exist?
"full_name": full_name, user = user_model.objects.get(github_id=github_id)
"bio": bio}) except user_model.DoesNotExist:
try:
# Is a user with the same email as the github user?
user = user_model.objects.get(email=email)
user.github_id = github_id
user.save(update_fields=["github_id"])
except user_model.DoesNotExist:
# Create a new user
username_unique = slugify_uniquely(username, user_model, slugfield="username")
user = user_model.objects.create(email=email,
username=username_unique,
github_id=github_id,
full_name=full_name,
bio=bio)
send_register_email(user)
user_registered_signal.send(sender=user.__class__, user=user)
if token: if token:
membership = get_membership_by_token(token) membership = get_membership_by_token(token)
membership.user = user membership.user = user
membership.save(update_fields=["user"]) membership.save(update_fields=["user"])
if created:
send_register_email(user)
user_registered_signal.send(sender=user.__class__, user=user)
return user return user

View File

@ -1,3 +1,19 @@
# 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 os import os
from celery import Celery from celery import Celery

View File

@ -1,3 +1,19 @@
# 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 django.conf import settings from django.conf import settings
from .celery import app from .celery import app

View File

@ -110,11 +110,11 @@ class IssuesEventHook(BaseEventHook):
subject = self.payload.get('issue', {}).get('title', None) subject = self.payload.get('issue', {}).get('title', None)
description = self.payload.get('issue', {}).get('body', None) description = self.payload.get('issue', {}).get('body', None)
github_reference = self.payload.get('issue', {}).get('number', None) github_url = self.payload.get('issue', {}).get('html_url', None)
github_user = self.payload.get('issue', {}).get('user', {}).get('id', None) github_user = self.payload.get('issue', {}).get('user', {}).get('id', None)
project_url = self.payload.get('repository', {}).get('html_url', None) project_url = self.payload.get('repository', {}).get('html_url', None)
if not all([subject, github_reference, project_url]): if not all([subject, github_url, project_url]):
raise ActionSyntaxException(_("Invalid issue information")) raise ActionSyntaxException(_("Invalid issue information"))
issue = Issue.objects.create( issue = Issue.objects.create(
@ -125,7 +125,7 @@ class IssuesEventHook(BaseEventHook):
type=self.project.default_issue_type, type=self.project.default_issue_type,
severity=self.project.default_severity, severity=self.project.default_severity,
priority=self.project.default_priority, priority=self.project.default_priority,
external_reference=['github', github_reference], external_reference=['github', github_url],
owner=get_github_user(github_user) owner=get_github_user(github_user)
) )
take_snapshot(issue, user=get_github_user(github_user)) take_snapshot(issue, user=get_github_user(github_user))
@ -139,18 +139,18 @@ class IssueCommentEventHook(BaseEventHook):
if self.payload.get('action', None) != "created": if self.payload.get('action', None) != "created":
raise ActionSyntaxException(_("Invalid issue comment information")) raise ActionSyntaxException(_("Invalid issue comment information"))
github_reference = self.payload.get('issue', {}).get('number', None) github_url = self.payload.get('issue', {}).get('html_url', None)
comment_message = self.payload.get('comment', {}).get('body', None) comment_message = self.payload.get('comment', {}).get('body', None)
github_user = self.payload.get('sender', {}).get('id', None) github_user = self.payload.get('sender', {}).get('id', None)
project_url = self.payload.get('repository', {}).get('html_url', None) project_url = self.payload.get('repository', {}).get('html_url', None)
comment_message = replace_github_references(project_url, comment_message) comment_message = replace_github_references(project_url, comment_message)
if not all([comment_message, github_reference, project_url]): if not all([comment_message, github_url, project_url]):
raise ActionSyntaxException(_("Invalid issue comment information")) raise ActionSyntaxException(_("Invalid issue comment information"))
issues = Issue.objects.filter(external_reference=["github", github_reference]) issues = Issue.objects.filter(external_reference=["github", github_url])
tasks = Task.objects.filter(external_reference=["github", github_reference]) tasks = Task.objects.filter(external_reference=["github", github_url])
uss = UserStory.objects.filter(external_reference=["github", github_reference]) uss = UserStory.objects.filter(external_reference=["github", github_url])
for item in list(issues) + list(tasks) + list(uss): for item in list(issues) + list(tasks) + list(uss):
snapshot = take_snapshot(item, snapshot = take_snapshot(item,

View File

@ -52,7 +52,7 @@ from .extensions.references import TaigaReferencesExtension
# Bleach configuration # Bleach configuration
bleach.ALLOWED_TAGS += ["p", "table", "th", "tr", "td", "h1", "h2", "h3", bleach.ALLOWED_TAGS += ["p", "table", "thead", "tbody", "th", "tr", "td", "h1", "h2", "h3",
"div", "pre", "span", "hr", "dl", "dt", "dd", "sup", "div", "pre", "span", "hr", "dl", "dt", "dd", "sup",
"img", "del", "br", "ins"] "img", "del", "br", "ins"]
@ -74,7 +74,8 @@ def _make_extensions_list(wikilinks_config=None, project=None):
MentionsExtension(), MentionsExtension(),
TaigaReferencesExtension(project), TaigaReferencesExtension(project),
"extra", "extra",
"codehilite"] "codehilite",
"nl2br"]
import diff_match_patch import diff_match_patch

View File

@ -16,7 +16,7 @@
from rest_framework import serializers from rest_framework import serializers
from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin, PgArrayField
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.notifications.validators import WatchersValidator
@ -26,6 +26,7 @@ from . import models
class IssueSerializer(WatchersValidator, serializers.ModelSerializer): class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
tags = PickleField(required=False) tags = PickleField(required=False)
external_reference = PgArrayField(required=False)
is_closed = serializers.Field(source="is_closed") is_closed = serializers.Field(source="is_closed")
comment = serializers.SerializerMethodField("get_comment") comment = serializers.SerializerMethodField("get_comment")
generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories") generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")

View File

@ -16,7 +16,7 @@
from rest_framework import serializers from rest_framework import serializers
from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin, PgArrayField
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator, TaskStatusExistsValidator from taiga.projects.validators import ProjectExistsValidator, TaskStatusExistsValidator
from taiga.projects.milestones.validators import SprintExistsValidator from taiga.projects.milestones.validators import SprintExistsValidator
@ -28,6 +28,7 @@ from . import models
class TaskSerializer(WatchersValidator, serializers.ModelSerializer): class TaskSerializer(WatchersValidator, serializers.ModelSerializer):
tags = PickleField(required=False, default=[]) tags = PickleField(required=False, default=[])
external_reference = PgArrayField(required=False)
comment = serializers.SerializerMethodField("get_comment") comment = serializers.SerializerMethodField("get_comment")
milestone_slug = serializers.SerializerMethodField("get_milestone_slug") milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")

View File

@ -126,9 +126,15 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
return self.role_points return self.role_points
def get_total_points(self): def get_total_points(self):
not_null_role_points = self.role_points.select_related("points").\
exclude(points__value__isnull=True)
#If we only have None values the sum should be None
if not not_null_role_points:
return None
total = 0.0 total = 0.0
for rp in self.role_points.select_related("points"): for rp in not_null_role_points:
if rp.points.value: total += rp.points.value
total += rp.points.value
return total return total

View File

@ -18,7 +18,7 @@ import json
from django.apps import apps from django.apps import apps
from rest_framework import serializers from rest_framework import serializers
from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin, PgArrayField
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator
@ -39,6 +39,7 @@ class RolePointsField(serializers.WritableField):
class UserStorySerializer(WatchersValidator, serializers.ModelSerializer): class UserStorySerializer(WatchersValidator, serializers.ModelSerializer):
tags = PickleField(default=[], required=False) tags = PickleField(default=[], required=False)
external_reference = PgArrayField(required=False)
points = RolePointsField(source="role_points", required=False) points = RolePointsField(source="role_points", required=False)
total_points = serializers.SerializerMethodField("get_total_points") total_points = serializers.SerializerMethodField("get_total_points")
comment = serializers.SerializerMethodField("get_comment") comment = serializers.SerializerMethodField("get_comment")

View File

@ -115,6 +115,78 @@ def test_response_200_in_registration_with_github_account(client, settings):
assert response.data["bio"] == "time traveler" assert response.data["bio"] == "time traveler"
assert response.data["github_id"] == 1955 assert response.data["github_id"] == 1955
def test_response_200_in_registration_with_github_account_and_existed_user_by_email(client, settings):
settings.PUBLIC_REGISTER_ENABLED = False
form = {"type": "github",
"code": "xxxxxx"}
user = factories.UserFactory()
user.email = "mmcfly@bttf.com"
user.github_id = None
user.save()
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"] == user.username
assert response.data["auth_token"] != "" and response.data["auth_token"] != None
assert response.data["email"] == user.email
assert response.data["full_name"] == user.full_name
assert response.data["bio"] == user.bio
assert response.data["github_id"] == 1955
def test_response_200_in_registration_with_github_account_and_existed_user_by_github_id(client, settings):
settings.PUBLIC_REGISTER_ENABLED = False
form = {"type": "github",
"code": "xxxxxx"}
user = factories.UserFactory()
user.github_id = 1955
user.save()
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"] == user.github_id
def test_response_200_in_registration_with_github_account_and_change_github_username(client, settings):
settings.PUBLIC_REGISTER_ENABLED = False
form = {"type": "github",
"code": "xxxxxx"}
user = factories.UserFactory()
user.username = "mmcfly"
user.save()
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-1"
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, settings): def test_response_200_in_registration_with_github_account_in_a_project(client, settings):
settings.PUBLIC_REGISTER_ENABLED = False settings.PUBLIC_REGISTER_ENABLED = False
@ -171,7 +243,6 @@ def test_respond_400_if_username_or_email_is_duplicate(client, settings, registe
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
register_form["username"] = "username" register_form["username"] = "username"
register_form["email"] = "ff@dd.com" register_form["email"] = "ff@dd.com"
response = client.post(reverse("auth-register"), register_form) response = client.post(reverse("auth-register"), register_form)

View File

@ -219,7 +219,7 @@ def test_issues_event_opened_issue(client):
"issue": { "issue": {
"title": "test-title", "title": "test-title",
"body": "test-body", "body": "test-body",
"number": 10, "html_url": "http://github.com/test/project/issues/11",
}, },
"assignee": {}, "assignee": {},
"label": {}, "label": {},
@ -249,7 +249,7 @@ def test_issues_event_other_than_opened_issue(client):
"issue": { "issue": {
"title": "test-title", "title": "test-title",
"body": "test-body", "body": "test-body",
"number": 10, "html_url": "http://github.com/test/project/issues/11",
}, },
"assignee": {}, "assignee": {},
"label": {}, "label": {},
@ -291,17 +291,17 @@ def test_issues_event_bad_issue(client):
def test_issue_comment_event_on_existing_issue_task_and_us(client): def test_issue_comment_event_on_existing_issue_task_and_us(client):
issue = f.IssueFactory.create(external_reference=["github", "10"]) issue = f.IssueFactory.create(external_reference=["github", "http://github.com/test/project/issues/11"])
take_snapshot(issue, user=issue.owner) take_snapshot(issue, user=issue.owner)
task = f.TaskFactory.create(project=issue.project, external_reference=["github", "10"]) task = f.TaskFactory.create(project=issue.project, external_reference=["github", "http://github.com/test/project/issues/11"])
take_snapshot(task, user=task.owner) take_snapshot(task, user=task.owner)
us = f.UserStoryFactory.create(project=issue.project, external_reference=["github", "10"]) us = f.UserStoryFactory.create(project=issue.project, external_reference=["github", "http://github.com/test/project/issues/11"])
take_snapshot(us, user=us.owner) take_snapshot(us, user=us.owner)
payload = { payload = {
"action": "created", "action": "created",
"issue": { "issue": {
"number": 10, "html_url": "http://github.com/test/project/issues/11",
}, },
"comment": { "comment": {
"body": "Test body", "body": "Test body",
@ -346,7 +346,7 @@ def test_issue_comment_event_on_not_existing_issue_task_and_us(client):
payload = { payload = {
"action": "created", "action": "created",
"issue": { "issue": {
"number": 11, "html_url": "http://github.com/test/project/issues/11",
}, },
"comment": { "comment": {
"body": "Test body", "body": "Test body",

View File

@ -200,3 +200,34 @@ def test_archived_filter(client):
data = {"is_archived": 1} data = {"is_archived": 1}
response = client.get(url, data) response = client.get(url, data)
assert len(json.loads(response.content)) == 1 assert len(json.loads(response.content)) == 1
def test_get_total_points(client):
project = f.ProjectFactory.create()
role1 = f.RoleFactory.create(project=project)
role2 = f.RoleFactory.create(project=project)
points1 = f.PointsFactory.create(project=project, value=None)
points2 = f.PointsFactory.create(project=project, value=1)
points3 = f.PointsFactory.create(project=project, value=2)
us_with_points = f.UserStoryFactory.create(project=project)
us_with_points.role_points.all().delete()
f.RolePointsFactory.create(user_story=us_with_points, role=role1, points=points2)
f.RolePointsFactory.create(user_story=us_with_points, role=role2, points=points3)
assert us_with_points.get_total_points() == 3.0
us_without_points = f.UserStoryFactory.create(project=project)
us_without_points.role_points.all().delete()
f.RolePointsFactory.create(user_story=us_without_points, role=role1, points=points1)
f.RolePointsFactory.create(user_story=us_without_points, role=role2, points=points1)
assert us_without_points.get_total_points() is None
us_mixed = f.UserStoryFactory.create(project=project)
us_mixed.role_points.all().delete()
f.RolePointsFactory.create(user_story=us_mixed, role=role1, points=points1)
f.RolePointsFactory.create(user_story=us_mixed, role=role2, points=points2)
assert us_mixed.get_total_points() == 1.0