Migrate to django 1.8 and make taiga compatible with python 3.5
parent
9226913caa
commit
a7a6bd3a1c
|
@ -2,6 +2,7 @@ sudo: false
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "3.4"
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
services:
|
services:
|
||||||
- rabbitmq # will start rabbitmq-server
|
- rabbitmq # will start rabbitmq-server
|
||||||
cache:
|
cache:
|
||||||
|
|
|
@ -28,6 +28,9 @@
|
||||||
|
|
||||||
|
|
||||||
### Misc
|
### Misc
|
||||||
|
- Made compatible with python 3.5.
|
||||||
|
- Migrated to django 1.8.
|
||||||
|
- Update the rest of requirements to the last version.
|
||||||
- API: Mixin fields 'users', 'members' and 'memberships' in ProjectDetailSerializer.
|
- API: Mixin fields 'users', 'members' and 'memberships' in ProjectDetailSerializer.
|
||||||
- API: Add stats/system resource with global server stats (total project, total users....)
|
- API: Add stats/system resource with global server stats (total project, total users....)
|
||||||
- API: Improve and fix some errors in issues/filters_data and userstories/filters_data.
|
- API: Improve and fix some errors in issues/filters_data and userstories/filters_data.
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
factory_boy==2.4.1
|
factory_boy==2.5.2
|
||||||
py==1.4.26
|
py==1.4.30
|
||||||
pytest==2.6.4
|
pytest==2.8.2
|
||||||
pytest-django==2.8.0
|
pytest-django==2.9.1
|
||||||
pytest-pythonpath==0.6
|
pytest-pythonpath==0.7
|
||||||
|
|
||||||
coverage==3.7.1
|
coverage==4.0
|
||||||
coveralls==0.4.2
|
coveralls==1.0
|
||||||
django-slowdown==0.0.1
|
django-slowdown==0.0.1
|
||||||
|
|
||||||
transifex-client==0.11.1.beta
|
transifex-client==0.11.1.beta
|
||||||
|
|
|
@ -1,37 +1,35 @@
|
||||||
Django==1.7.8
|
Django==1.8.5
|
||||||
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
|
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
|
||||||
django-picklefield==0.3.1
|
django-picklefield==0.3.2
|
||||||
django-sampledatahelper==0.2.2
|
django-sampledatahelper==0.3.0
|
||||||
gunicorn==19.3.0
|
gunicorn==19.3.0
|
||||||
psycopg2==2.5.4
|
psycopg2==2.6.1
|
||||||
pillow==2.5.3
|
Pillow==3.0.0
|
||||||
pytz==2014.4
|
pytz==2015.6
|
||||||
six==1.8.0
|
six==1.10.0
|
||||||
amqp==1.4.6
|
amqp==1.4.7
|
||||||
djmail==0.11
|
djmail==0.11
|
||||||
django-pgjson==0.2.2
|
django-pgjson==0.3.1
|
||||||
djorm-pgarray==1.0.4
|
djorm-pgarray==1.2
|
||||||
django-jinja==1.0.4
|
django-jinja==1.4.1
|
||||||
jinja2==2.7.2
|
jinja2==2.8
|
||||||
pygments==1.6
|
pygments==2.0.2
|
||||||
django-sites==0.8
|
django-sites==0.8
|
||||||
Markdown==2.4.1
|
Markdown==2.6.2
|
||||||
fn==0.2.13
|
fn==0.4.3
|
||||||
diff-match-patch==20121119
|
diff-match-patch==20121119
|
||||||
requests==2.4.1
|
requests==2.8.0
|
||||||
django-sr==0.0.4
|
django-sr==0.0.4
|
||||||
easy-thumbnails==2.1
|
easy-thumbnails==2.2
|
||||||
celery==3.1.17
|
celery==3.1.18
|
||||||
redis==2.10.3
|
redis==2.10.3
|
||||||
Unidecode==0.04.16
|
Unidecode==0.04.18
|
||||||
raven==5.1.1
|
raven==5.7.2
|
||||||
bleach==1.4
|
bleach==1.4.2
|
||||||
django-ipware==0.1.0
|
django-ipware==1.1.1
|
||||||
premailer==2.8.1
|
premailer==2.9.6
|
||||||
|
cssutils==1.0.1 # Compatible with python 3.5
|
||||||
django-transactional-cleanup==0.1.15
|
django-transactional-cleanup==0.1.15
|
||||||
lxml==3.4.1
|
lxml==3.5.0b1
|
||||||
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
|
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
|
||||||
pyjwkest==1.0.3
|
pyjwkest==1.0.5
|
||||||
|
|
||||||
# Comment it if you are using python >= 3.4
|
|
||||||
enum34==1.0
|
|
||||||
|
|
|
@ -25,6 +25,8 @@ ADMINS = (
|
||||||
("Admin", "example@example.com"),
|
("Admin", "example@example.com"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "transaction_hooks.backends.postgresql_psycopg2",
|
"ENGINE": "transaction_hooks.backends.postgresql_psycopg2",
|
||||||
|
@ -215,11 +217,29 @@ DEFAULT_FILE_STORAGE = "taiga.base.storage.FileSystemStorage"
|
||||||
|
|
||||||
SECRET_KEY = "aw3+t2r(8(0kkrhg8)gx6i96v5^kv%6cfep9wxfom0%7dy0m9e"
|
SECRET_KEY = "aw3+t2r(8(0kkrhg8)gx6i96v5^kv%6cfep9wxfom0%7dy0m9e"
|
||||||
|
|
||||||
TEMPLATE_LOADERS = [
|
TEMPLATES = [
|
||||||
"django_jinja.loaders.AppLoader",
|
{
|
||||||
"django_jinja.loaders.FileSystemLoader",
|
"BACKEND": "django_jinja.backend.Jinja2",
|
||||||
|
"DIRS": [
|
||||||
|
os.path.join(BASE_DIR, "templates"),
|
||||||
|
],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
'context_processors': [
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.template.context_processors.i18n",
|
||||||
|
"django.template.context_processors.media",
|
||||||
|
"django.template.context_processors.static",
|
||||||
|
"django.template.context_processors.tz",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
"match_extension": ".jinja",
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = [
|
MIDDLEWARE_CLASSES = [
|
||||||
"taiga.base.middleware.cors.CoorsMiddleware",
|
"taiga.base.middleware.cors.CoorsMiddleware",
|
||||||
"taiga.events.middleware.SessionIDMiddleware",
|
"taiga.events.middleware.SessionIDMiddleware",
|
||||||
|
@ -234,22 +254,9 @@ MIDDLEWARE_CLASSES = [
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
TEMPLATE_CONTEXT_PROCESSORS = [
|
|
||||||
"django.contrib.auth.context_processors.auth",
|
|
||||||
"django.core.context_processors.request",
|
|
||||||
"django.core.context_processors.i18n",
|
|
||||||
"django.core.context_processors.media",
|
|
||||||
"django.core.context_processors.static",
|
|
||||||
"django.core.context_processors.tz",
|
|
||||||
"django.contrib.messages.context_processors.messages",
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = "taiga.urls"
|
ROOT_URLCONF = "taiga.urls"
|
||||||
|
|
||||||
TEMPLATE_DIRS = [
|
|
||||||
os.path.join(BASE_DIR, "templates"),
|
|
||||||
]
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
|
|
|
@ -17,8 +17,5 @@
|
||||||
from .common import *
|
from .common import *
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
TEMPLATE_DEBUG = DEBUG
|
|
||||||
|
|
||||||
TEMPLATE_CONTEXT_PROCESSORS += [
|
TEMPLATES[0]["OPTIONS"]['context_processors'] += "django.template.context_processors.debug"
|
||||||
"django.core.context_processors.debug",
|
|
||||||
]
|
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
from .development import *
|
from .development import *
|
||||||
|
|
||||||
|
#DEBUG = False
|
||||||
|
|
||||||
#ADMINS = (
|
#ADMINS = (
|
||||||
# ("Admin", "example@example.com"),
|
# ("Admin", "example@example.com"),
|
||||||
#)
|
#)
|
||||||
|
|
|
@ -28,9 +28,8 @@ from django.db import transaction as tx
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
|
||||||
|
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
|
from taiga.base.mails import mail_builder
|
||||||
from taiga.users.serializers import UserAdminSerializer
|
from taiga.users.serializers import UserAdminSerializer
|
||||||
from taiga.users.services import get_and_validate_user
|
from taiga.users.services import get_and_validate_user
|
||||||
|
|
||||||
|
@ -57,8 +56,7 @@ def send_register_email(user) -> bool:
|
||||||
"""
|
"""
|
||||||
cancel_token = get_token_for_user(user, "cancel_account")
|
cancel_token = get_token_for_user(user, "cancel_account")
|
||||||
context = {"user": user, "cancel_token": cancel_token}
|
context = {"user": user, "cancel_token": cancel_token}
|
||||||
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
email = mail_builder.registered_user(user, context)
|
||||||
email = mbuilder.registered_user(user, context)
|
|
||||||
return bool(email.send())
|
return bool(email.send())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1005,7 +1005,7 @@ class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer)))
|
||||||
m2m_data[field_name] = attrs.pop(field_name)
|
m2m_data[field_name] = attrs.pop(field_name)
|
||||||
|
|
||||||
# Forward m2m relations
|
# Forward m2m relations
|
||||||
for field in meta.many_to_many + meta.virtual_fields:
|
for field in list(meta.many_to_many) + meta.virtual_fields:
|
||||||
if field.name in attrs:
|
if field.name in attrs:
|
||||||
m2m_data[field.name] = attrs.pop(field.name)
|
m2m_data[field.name] = attrs.pop(field.name)
|
||||||
|
|
||||||
|
|
|
@ -447,7 +447,7 @@ class APIView(View):
|
||||||
|
|
||||||
|
|
||||||
def api_server_error(request, *args, **kwargs):
|
def api_server_error(request, *args, **kwargs):
|
||||||
if settings.DEBUG is False and request.META['CONTENT_TYPE'] == "application/json":
|
if settings.DEBUG is False and request.META.get('CONTENT_TYPE', None) == "application/json":
|
||||||
return HttpResponse(json.dumps({"error": _("Server application error")}),
|
return HttpResponse(json.dumps({"error": _("Server application error")}),
|
||||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
return server_error(request, *args, **kwargs)
|
return server_error(request, *args, **kwargs)
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 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 djmail import template_mail
|
||||||
|
import premailer
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
# Hide CSS warnings messages if debug mode is disable
|
||||||
|
if not getattr(settings, "DEBUG", False):
|
||||||
|
premailer.premailer.cssutils.log.setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
|
class InlineCSSTemplateMail(template_mail.TemplateMail):
|
||||||
|
def _render_message_body_as_html(self, context):
|
||||||
|
html = super()._render_message_body_as_html(context)
|
||||||
|
|
||||||
|
# Transform CSS into line style attributes
|
||||||
|
return premailer.transform(html)
|
||||||
|
|
||||||
|
|
||||||
|
class MagicMailBuilder(template_mail.MagicMailBuilder):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
mail_builder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
|
@ -22,7 +22,7 @@ from django.db.models.loading import get_model
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
from taiga.base.mails import mail_builder
|
||||||
|
|
||||||
from taiga.projects.models import Project, Membership
|
from taiga.projects.models import Project, Membership
|
||||||
from taiga.projects.history.models import HistoryEntry
|
from taiga.projects.history.models import HistoryEntry
|
||||||
|
@ -47,11 +47,12 @@ class Command(BaseCommand):
|
||||||
locale = options.get('locale')
|
locale = options.get('locale')
|
||||||
test_email = args[0]
|
test_email = args[0]
|
||||||
|
|
||||||
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
|
||||||
|
|
||||||
# Register email
|
# Register email
|
||||||
context = {"lang": locale, "user": User.objects.all().order_by("?").first(), "cancel_token": "cancel-token"}
|
context = {"lang": locale,
|
||||||
email = mbuilder.registered_user(test_email, context)
|
"user": User.objects.all().order_by("?").first(),
|
||||||
|
"cancel_token": "cancel-token"}
|
||||||
|
|
||||||
|
email = mail_builder.registered_user(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Membership invitation
|
# Membership invitation
|
||||||
|
@ -60,12 +61,13 @@ class Command(BaseCommand):
|
||||||
membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example"
|
membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example"
|
||||||
|
|
||||||
context = {"lang": locale, "membership": membership}
|
context = {"lang": locale, "membership": membership}
|
||||||
email = mbuilder.membership_invitation(test_email, context)
|
email = mail_builder.membership_invitation(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Membership notification
|
# Membership notification
|
||||||
context = {"lang": locale, "membership": Membership.objects.order_by("?").filter(user__isnull=False).first()}
|
context = {"lang": locale,
|
||||||
email = mbuilder.membership_notification(test_email, context)
|
"membership": Membership.objects.order_by("?").filter(user__isnull=False).first()}
|
||||||
|
email = mail_builder.membership_notification(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Feedback
|
# Feedback
|
||||||
|
@ -81,17 +83,17 @@ class Command(BaseCommand):
|
||||||
"key2": "value2",
|
"key2": "value2",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
email = mbuilder.feedback_notification(test_email, context)
|
email = mail_builder.feedback_notification(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Password recovery
|
# Password recovery
|
||||||
context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
|
context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
|
||||||
email = mbuilder.password_recovery(test_email, context)
|
email = mail_builder.password_recovery(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Change email
|
# Change email
|
||||||
context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
|
context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
|
||||||
email = mbuilder.change_email(test_email, context)
|
email = mail_builder.change_email(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Export/Import emails
|
# Export/Import emails
|
||||||
|
@ -102,7 +104,7 @@ class Command(BaseCommand):
|
||||||
"error_subject": "Error generating project dump",
|
"error_subject": "Error generating project dump",
|
||||||
"error_message": "Error generating project dump",
|
"error_message": "Error generating project dump",
|
||||||
}
|
}
|
||||||
email = mbuilder.export_error(test_email, context)
|
email = mail_builder.export_error(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
context = {
|
context = {
|
||||||
"lang": locale,
|
"lang": locale,
|
||||||
|
@ -110,7 +112,7 @@ class Command(BaseCommand):
|
||||||
"error_subject": "Error importing project dump",
|
"error_subject": "Error importing project dump",
|
||||||
"error_message": "Error importing project dump",
|
"error_message": "Error importing project dump",
|
||||||
}
|
}
|
||||||
email = mbuilder.import_error(test_email, context)
|
email = mail_builder.import_error(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
deletion_date = timezone.now() + datetime.timedelta(seconds=60*60*24)
|
deletion_date = timezone.now() + datetime.timedelta(seconds=60*60*24)
|
||||||
|
@ -121,7 +123,7 @@ class Command(BaseCommand):
|
||||||
"project": Project.objects.all().order_by("?").first(),
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
"deletion_date": deletion_date,
|
"deletion_date": deletion_date,
|
||||||
}
|
}
|
||||||
email = mbuilder.dump_project(test_email, context)
|
email = mail_builder.dump_project(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
@ -129,7 +131,7 @@ class Command(BaseCommand):
|
||||||
"user": User.objects.all().order_by("?").first(),
|
"user": User.objects.all().order_by("?").first(),
|
||||||
"project": Project.objects.all().order_by("?").first(),
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
}
|
}
|
||||||
email = mbuilder.load_dump(test_email, context)
|
email = mail_builder.load_dump(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Notification emails
|
# Notification emails
|
||||||
|
|
|
@ -43,7 +43,7 @@ def get_neighbors(obj, results_set=None):
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
SELECT * FROM
|
SELECT * FROM
|
||||||
(SELECT "id" as id, ROW_NUMBER() OVER()
|
(SELECT "col1" as id, ROW_NUMBER() OVER()
|
||||||
FROM (%s) as ID_AND_ROW)
|
FROM (%s) as ID_AND_ROW)
|
||||||
AS SELECTED_ID_AND_ROW
|
AS SELECTED_ID_AND_ROW
|
||||||
""" % (base_sql)
|
""" % (base_sql)
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 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.apps import apps
|
||||||
|
from django.contrib.contenttypes.management import update_contenttypes
|
||||||
|
|
||||||
|
|
||||||
|
def update_all_contenttypes(**kwargs):
|
||||||
|
for app_config in apps.get_app_configs():
|
||||||
|
update_contenttypes(app_config, **kwargs)
|
|
@ -27,6 +27,8 @@ from . import events
|
||||||
|
|
||||||
def on_save_any_model(sender, instance, created, **kwargs):
|
def on_save_any_model(sender, instance, created, **kwargs):
|
||||||
# Ignore any object that can not have project_id
|
# Ignore any object that can not have project_id
|
||||||
|
if not hasattr(instance, "project_id"):
|
||||||
|
return
|
||||||
content_type = get_typename_for_model_instance(instance)
|
content_type = get_typename_for_model_instance(instance)
|
||||||
|
|
||||||
# Ignore any other events
|
# Ignore any other events
|
||||||
|
|
|
@ -25,8 +25,7 @@ from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
from taiga.base.mails import mail_builder
|
||||||
|
|
||||||
from taiga.celery import app
|
from taiga.celery import app
|
||||||
|
|
||||||
from .service import render_project
|
from .service import render_project
|
||||||
|
@ -40,7 +39,6 @@ import resource
|
||||||
|
|
||||||
@app.task(bind=True)
|
@app.task(bind=True)
|
||||||
def dump_project(self, user, project):
|
def dump_project(self, user, project):
|
||||||
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
|
||||||
path = "exports/{}/{}-{}.json".format(project.pk, project.slug, self.request.id)
|
path = "exports/{}/{}-{}.json".format(project.pk, project.slug, self.request.id)
|
||||||
storage_path = default_storage.path(path)
|
storage_path = default_storage.path(path)
|
||||||
|
|
||||||
|
@ -56,7 +54,7 @@ def dump_project(self, user, project):
|
||||||
"error_message": _("Error generating project dump"),
|
"error_message": _("Error generating project dump"),
|
||||||
"project": project
|
"project": project
|
||||||
}
|
}
|
||||||
email = mbuilder.export_error(user, ctx)
|
email = mail_builder.export_error(user, ctx)
|
||||||
email.send()
|
email.send()
|
||||||
logger.error('Error generating dump %s (by %s)', project.slug, user, exc_info=sys.exc_info())
|
logger.error('Error generating dump %s (by %s)', project.slug, user, exc_info=sys.exc_info())
|
||||||
return
|
return
|
||||||
|
@ -68,7 +66,7 @@ def dump_project(self, user, project):
|
||||||
"user": user,
|
"user": user,
|
||||||
"deletion_date": deletion_date
|
"deletion_date": deletion_date
|
||||||
}
|
}
|
||||||
email = mbuilder.dump_project(user, ctx)
|
email = mail_builder.dump_project(user, ctx)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,8 +77,6 @@ def delete_project_dump(project_id, project_slug, task_id):
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def load_project_dump(user, dump):
|
def load_project_dump(user, dump):
|
||||||
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
project = dict_to_project(dump, user.email)
|
project = dict_to_project(dump, user.email)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -89,11 +85,11 @@ def load_project_dump(user, dump):
|
||||||
"error_subject": _("Error loading project dump"),
|
"error_subject": _("Error loading project dump"),
|
||||||
"error_message": _("Error loading project dump"),
|
"error_message": _("Error loading project dump"),
|
||||||
}
|
}
|
||||||
email = mbuilder.import_error(user, ctx)
|
email = mail_builder.import_error(user, ctx)
|
||||||
email.send()
|
email.send()
|
||||||
logger.error('Error loading dump %s (by %s)', project.slug, user, exc_info=sys.exc_info())
|
logger.error('Error loading dump %s (by %s)', project.slug, user, exc_info=sys.exc_info())
|
||||||
return
|
return
|
||||||
|
|
||||||
ctx = {"user": user, "project": project}
|
ctx = {"user": user, "project": project}
|
||||||
email = mbuilder.load_dump(user, ctx)
|
email = mail_builder.load_dump(user, ctx)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
from taiga.base.mails import mail_builder
|
||||||
|
|
||||||
|
|
||||||
def send_feedback(feedback_entry, extra, reply_to=[]):
|
def send_feedback(feedback_entry, extra, reply_to=[]):
|
||||||
|
@ -30,7 +30,6 @@ def send_feedback(feedback_entry, extra, reply_to=[]):
|
||||||
"extra": extra
|
"extra": extra
|
||||||
}
|
}
|
||||||
|
|
||||||
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
email = mail_builder.feedback_notification(support_email, ctx)
|
||||||
email = mbuilder.feedback_notification(support_email, ctx)
|
|
||||||
email.extra_headers["Reply-To"] = ", ".join(reply_to)
|
email.extra_headers["Reply-To"] = ", ".join(reply_to)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
|
@ -21,10 +21,7 @@ from django_sites import get_by_id as get_site_by_id
|
||||||
from taiga.front.urls import urls
|
from taiga.front.urls import urls
|
||||||
|
|
||||||
|
|
||||||
register = library.Library()
|
@library.global_function(name="resolve_front_url")
|
||||||
|
|
||||||
|
|
||||||
@register.global_function(name="resolve_front_url")
|
|
||||||
def resolve(type, *args):
|
def resolve(type, *args):
|
||||||
site = get_site_by_id("front")
|
site = get_site_by_id("front")
|
||||||
url_tmpl = "{scheme}//{domain}{url}"
|
url_tmpl = "{scheme}//{domain}{url}"
|
||||||
|
|
|
@ -75,11 +75,11 @@ def _make_extensions_list(project=None):
|
||||||
MentionsExtension(),
|
MentionsExtension(),
|
||||||
TaigaReferencesExtension(project),
|
TaigaReferencesExtension(project),
|
||||||
TargetBlankLinkExtension(),
|
TargetBlankLinkExtension(),
|
||||||
"extra",
|
"markdown.extensions.extra",
|
||||||
"codehilite",
|
"markdown.extensions.codehilite",
|
||||||
"sane_lists",
|
"markdown.extensions.sane_lists",
|
||||||
"toc",
|
"markdown.extensions.toc",
|
||||||
"nl2br"]
|
"markdown.extensions.nl2br"]
|
||||||
|
|
||||||
|
|
||||||
import diff_match_patch
|
import diff_match_patch
|
||||||
|
|
|
@ -18,10 +18,8 @@ from django_jinja import library
|
||||||
from jinja2 import Markup
|
from jinja2 import Markup
|
||||||
from taiga.mdrender.service import render
|
from taiga.mdrender.service import render
|
||||||
|
|
||||||
register = library.Library()
|
|
||||||
|
|
||||||
|
@library.global_function
|
||||||
@register.global_function
|
|
||||||
def mdrender(project, text) -> str:
|
def mdrender(project, text) -> str:
|
||||||
if text:
|
if text:
|
||||||
return Markup(render(project, text))
|
return Markup(render(project, text))
|
||||||
|
|
|
@ -18,8 +18,6 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from django_jinja import library
|
from django_jinja import library
|
||||||
|
|
||||||
register = library.Library()
|
|
||||||
|
|
||||||
|
|
||||||
EXTRA_FIELD_VERBOSE_NAMES = {
|
EXTRA_FIELD_VERBOSE_NAMES = {
|
||||||
"description_diff": _("description"),
|
"description_diff": _("description"),
|
||||||
|
@ -29,7 +27,7 @@ EXTRA_FIELD_VERBOSE_NAMES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.global_function
|
@library.global_function
|
||||||
def verbose_name(obj_class, field_name):
|
def verbose_name(obj_class, field_name):
|
||||||
if field_name in EXTRA_FIELD_VERBOSE_NAMES:
|
if field_name in EXTRA_FIELD_VERBOSE_NAMES:
|
||||||
return EXTRA_FIELD_VERBOSE_NAMES[field_name]
|
return EXTRA_FIELD_VERBOSE_NAMES[field_name]
|
||||||
|
@ -39,6 +37,7 @@ def verbose_name(obj_class, field_name):
|
||||||
except Exception:
|
except Exception:
|
||||||
return field_name
|
return field_name
|
||||||
|
|
||||||
@register.global_function
|
|
||||||
|
@library.global_function
|
||||||
def lists_diff(list1, list2):
|
def lists_diff(list1, list2):
|
||||||
return (list(set(list1) - set(list2)))
|
return (list(set(list1) - set(list2)))
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.management import update_all_contenttypes
|
from taiga.base.utils.contenttypes import update_all_contenttypes
|
||||||
|
|
||||||
def create_notifications(apps, schema_editor):
|
def create_notifications(apps, schema_editor):
|
||||||
update_all_contenttypes(verbosity=0)
|
update_all_contenttypes(verbosity=0)
|
||||||
|
|
|
@ -21,7 +21,6 @@ from django.core.management.base import BaseCommand
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.webdesign import lorem_ipsum
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from sampledatahelper.helper import SampleDataHelper
|
from sampledatahelper.helper import SampleDataHelper
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.management import update_all_contenttypes
|
from taiga.base.utils.contenttypes import update_all_contenttypes
|
||||||
|
|
||||||
def create_notifications(apps, schema_editor):
|
def create_notifications(apps, schema_editor):
|
||||||
update_all_contenttypes(verbosity=0)
|
update_all_contenttypes(verbosity=0)
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notifications', '0004_watched'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='historychangenotification',
|
||||||
|
name='history_entries',
|
||||||
|
field=models.ManyToManyField(verbose_name='history entries', to='history.HistoryEntry', related_name='+'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='historychangenotification',
|
||||||
|
name='notify_users',
|
||||||
|
field=models.ManyToManyField(verbose_name='notify users', to=settings.AUTH_USER_MODEL, related_name='+'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -61,10 +61,10 @@ class HistoryChangeNotification(models.Model):
|
||||||
verbose_name=_("created date time"))
|
verbose_name=_("created date time"))
|
||||||
updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True,
|
updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True,
|
||||||
verbose_name=_("updated date time"))
|
verbose_name=_("updated date time"))
|
||||||
history_entries = models.ManyToManyField("history.HistoryEntry", null=True, blank=True,
|
history_entries = models.ManyToManyField("history.HistoryEntry",
|
||||||
verbose_name=_("history entries"),
|
verbose_name=_("history entries"),
|
||||||
related_name="+")
|
related_name="+")
|
||||||
notify_users = models.ManyToManyField("users.User", null=True, blank=True,
|
notify_users = models.ManyToManyField("users.User",
|
||||||
verbose_name=_("notify users"),
|
verbose_name=_("notify users"),
|
||||||
related_name="+")
|
related_name="+")
|
||||||
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
|
|
@ -28,9 +28,8 @@ from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from djmail import template_mail
|
|
||||||
|
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
|
from taiga.base.mails import InlineCSSTemplateMail
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
from taiga.projects.notifications.choices import NotifyLevel
|
||||||
from taiga.projects.history.choices import HistoryType
|
from taiga.projects.history.choices import HistoryType
|
||||||
from taiga.projects.history.services import (make_key_from_model_object,
|
from taiga.projects.history.services import (make_key_from_model_object,
|
||||||
|
@ -202,7 +201,7 @@ def _make_template_mail(name:str):
|
||||||
of it.
|
of it.
|
||||||
"""
|
"""
|
||||||
cls = type("InlineCSSTemplateMail",
|
cls = type("InlineCSSTemplateMail",
|
||||||
(template_mail.InlineCSSTemplateMail,),
|
(InlineCSSTemplateMail,),
|
||||||
{"name": name})
|
{"name": name})
|
||||||
|
|
||||||
return cls()
|
return cls()
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
from taiga.base.mails import mail_builder
|
||||||
|
|
||||||
|
|
||||||
def send_invitation(invitation):
|
def send_invitation(invitation):
|
||||||
"""Send an invitation email"""
|
"""Send an invitation email"""
|
||||||
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
|
||||||
if invitation.user:
|
if invitation.user:
|
||||||
template = mbuilder.membership_notification
|
template = mail_builder.membership_notification
|
||||||
email = template(invitation.user, {"membership": invitation})
|
email = template(invitation.user, {"membership": invitation})
|
||||||
else:
|
else:
|
||||||
template = mbuilder.membership_invitation
|
template = mail_builder.membership_invitation
|
||||||
email = template(invitation.email, {"membership": invitation})
|
email = template(invitation.email, {"membership": invitation})
|
||||||
|
|
||||||
email.send()
|
email.send()
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.management import update_all_contenttypes
|
from taiga.base.utils.contenttypes import update_all_contenttypes
|
||||||
|
|
||||||
def create_notifications(apps, schema_editor):
|
def create_notifications(apps, schema_editor):
|
||||||
update_all_contenttypes(verbosity=0)
|
update_all_contenttypes(verbosity=0)
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.management import update_all_contenttypes
|
from taiga.base.utils.contenttypes import update_all_contenttypes
|
||||||
|
|
||||||
def create_notifications(apps, schema_editor):
|
def create_notifications(apps, schema_editor):
|
||||||
update_all_contenttypes(verbosity=0)
|
update_all_contenttypes(verbosity=0)
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.management import update_all_contenttypes
|
from taiga.base.utils.contenttypes import update_all_contenttypes
|
||||||
|
|
||||||
def create_notifications(apps, schema_editor):
|
def create_notifications(apps, schema_editor):
|
||||||
update_all_contenttypes(verbosity=0)
|
update_all_contenttypes(verbosity=0)
|
||||||
|
|
|
@ -33,13 +33,11 @@ from taiga.base.api import ModelCrudViewSet
|
||||||
from taiga.base.filters import PermissionBasedFilterBackend
|
from taiga.base.filters import PermissionBasedFilterBackend
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.filters import MembersFilterBackend
|
from taiga.base.filters import MembersFilterBackend
|
||||||
|
from taiga.base.mails import mail_builder
|
||||||
from taiga.projects.votes import services as votes_service
|
from taiga.projects.votes import services as votes_service
|
||||||
|
|
||||||
from easy_thumbnails.source_generators import pil_image
|
from easy_thumbnails.source_generators import pil_image
|
||||||
|
|
||||||
from djmail.template_mail import MagicMailBuilder
|
|
||||||
from djmail.template_mail import InlineCSSTemplateMail
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
@ -189,8 +187,7 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
user.token = str(uuid.uuid1())
|
user.token = str(uuid.uuid1())
|
||||||
user.save(update_fields=["token"])
|
user.save(update_fields=["token"])
|
||||||
|
|
||||||
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
email = mail_builder.password_recovery(user, {"user": user})
|
||||||
email = mbuilder.password_recovery(user, {"user": user})
|
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
return response.Ok({"detail": _("Mail sended successful!")})
|
return response.Ok({"detail": _("Mail sended successful!")})
|
||||||
|
@ -314,8 +311,7 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
request.user.email_token = str(uuid.uuid1())
|
request.user.email_token = str(uuid.uuid1())
|
||||||
request.user.new_email = new_email
|
request.user.new_email = new_email
|
||||||
request.user.save(update_fields=["email_token", "new_email"])
|
request.user.save(update_fields=["email_token", "new_email"])
|
||||||
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
email = mail_builder.change_email(request.user.new_email, {"user": request.user,
|
||||||
email = mbuilder.change_email(request.user.new_email, {"user": request.user,
|
|
||||||
"lang": request.user.lang})
|
"lang": request.user.lang})
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
|
|
|
@ -41,4 +41,4 @@ class UserCreationForm(DjangoUserCreationForm):
|
||||||
class UserChangeForm(DjangoUserChangeForm):
|
class UserChangeForm(DjangoUserChangeForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
fields = '__all__'
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import django.contrib.auth.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0013_auto_20150901_1600'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='user',
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='last_login',
|
||||||
|
field=models.DateTimeField(verbose_name='last login', blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='new_email',
|
||||||
|
field=models.EmailField(verbose_name='new email address', blank=True, null=True, max_length=254),
|
||||||
|
),
|
||||||
|
]
|
|
@ -355,7 +355,7 @@ def test_watching_users_to_notify_on_issue_modification_6():
|
||||||
assert users == {watching_user, issue.owner}
|
assert users == {watching_user, issue.owner}
|
||||||
|
|
||||||
|
|
||||||
def test_send_notifications_using_services_method(settings, mail):
|
def test_send_notifications_using_services_method_for_user_stories(settings, mail):
|
||||||
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1
|
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1
|
||||||
|
|
||||||
project = f.ProjectFactory.create()
|
project = f.ProjectFactory.create()
|
||||||
|
@ -363,38 +363,34 @@ def test_send_notifications_using_services_method(settings, mail):
|
||||||
member1 = f.MembershipFactory.create(project=project, role=role)
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
member2 = f.MembershipFactory.create(project=project, role=role)
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
|
||||||
history_change = MagicMock()
|
|
||||||
history_change.user = {"pk": member1.user.pk}
|
|
||||||
history_change.comment = ""
|
|
||||||
history_change.type = HistoryType.change
|
|
||||||
history_change.is_hidden = False
|
|
||||||
|
|
||||||
history_create = MagicMock()
|
|
||||||
history_create.user = {"pk": member1.user.pk}
|
|
||||||
history_create.comment = ""
|
|
||||||
history_create.type = HistoryType.create
|
|
||||||
history_create.is_hidden = False
|
|
||||||
|
|
||||||
history_delete = MagicMock()
|
|
||||||
history_delete.user = {"pk": member1.user.pk}
|
|
||||||
history_delete.comment = ""
|
|
||||||
history_delete.type = HistoryType.delete
|
|
||||||
history_delete.is_hidden = False
|
|
||||||
|
|
||||||
# Issues
|
|
||||||
issue = f.IssueFactory.create(project=project, owner=member2.user)
|
|
||||||
take_snapshot(issue, user=issue.owner)
|
|
||||||
services.send_notifications(issue,
|
|
||||||
history=history_create)
|
|
||||||
|
|
||||||
services.send_notifications(issue,
|
|
||||||
history=history_change)
|
|
||||||
|
|
||||||
services.send_notifications(issue,
|
|
||||||
history=history_delete)
|
|
||||||
|
|
||||||
# Userstories
|
|
||||||
us = f.UserStoryFactory.create(project=project, owner=member2.user)
|
us = f.UserStoryFactory.create(project=project, owner=member2.user)
|
||||||
|
history_change = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.change,
|
||||||
|
key="userstories.userstory:{}".format(us.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
history_create = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.create,
|
||||||
|
key="userstories.userstory:{}".format(us.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
history_delete = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="test:delete",
|
||||||
|
type=HistoryType.delete,
|
||||||
|
key="userstories.userstory:{}".format(us.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
take_snapshot(us, user=us.owner)
|
take_snapshot(us, user=us.owner)
|
||||||
services.send_notifications(us,
|
services.send_notifications(us,
|
||||||
history=history_create)
|
history=history_create)
|
||||||
|
@ -405,56 +401,18 @@ def test_send_notifications_using_services_method(settings, mail):
|
||||||
services.send_notifications(us,
|
services.send_notifications(us,
|
||||||
history=history_delete)
|
history=history_delete)
|
||||||
|
|
||||||
# Tasks
|
assert models.HistoryChangeNotification.objects.count() == 3
|
||||||
task = f.TaskFactory.create(project=project, owner=member2.user)
|
|
||||||
take_snapshot(task, user=task.owner)
|
|
||||||
services.send_notifications(task,
|
|
||||||
history=history_create)
|
|
||||||
|
|
||||||
services.send_notifications(task,
|
|
||||||
history=history_change)
|
|
||||||
|
|
||||||
services.send_notifications(task,
|
|
||||||
history=history_delete)
|
|
||||||
|
|
||||||
# Wiki pages
|
|
||||||
wiki = f.WikiPageFactory.create(project=project, owner=member2.user)
|
|
||||||
take_snapshot(wiki, user=wiki.owner)
|
|
||||||
services.send_notifications(wiki,
|
|
||||||
history=history_create)
|
|
||||||
|
|
||||||
services.send_notifications(wiki,
|
|
||||||
history=history_change)
|
|
||||||
|
|
||||||
services.send_notifications(wiki,
|
|
||||||
history=history_delete)
|
|
||||||
|
|
||||||
assert models.HistoryChangeNotification.objects.count() == 12
|
|
||||||
assert len(mail.outbox) == 0
|
assert len(mail.outbox) == 0
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
services.process_sync_notifications()
|
services.process_sync_notifications()
|
||||||
assert len(mail.outbox) == 12
|
assert len(mail.outbox) == 3
|
||||||
|
|
||||||
# test headers
|
# test headers
|
||||||
events = [issue, us, task, wiki]
|
|
||||||
domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"]
|
domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"]
|
||||||
i = 0
|
|
||||||
for msg in mail.outbox:
|
for msg in mail.outbox:
|
||||||
# each event has 3 msgs
|
|
||||||
event = events[math.floor(i / 3)]
|
|
||||||
|
|
||||||
# each set of 3 should have the same headers
|
|
||||||
if i % 3 == 0:
|
|
||||||
if hasattr(event, 'ref'):
|
|
||||||
e_slug = event.ref
|
|
||||||
elif hasattr(event, 'slug'):
|
|
||||||
e_slug = event.slug
|
|
||||||
else:
|
|
||||||
e_slug = 'taiga-system'
|
|
||||||
|
|
||||||
m_id = "{project_slug}/{msg_id}".format(
|
m_id = "{project_slug}/{msg_id}".format(
|
||||||
project_slug=project.slug,
|
project_slug=project.slug,
|
||||||
msg_id=e_slug
|
msg_id=us.ref
|
||||||
)
|
)
|
||||||
|
|
||||||
message_id = "<{m_id}/".format(m_id=m_id)
|
message_id = "<{m_id}/".format(m_id=m_id)
|
||||||
|
@ -490,7 +448,286 @@ def test_send_notifications_using_services_method(settings, mail):
|
||||||
msg_ts = datetime.datetime.fromtimestamp(int(msg_time))
|
msg_ts = datetime.datetime.fromtimestamp(int(msg_time))
|
||||||
assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index')
|
assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index')
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
def test_send_notifications_using_services_method_for_tasks(settings, mail):
|
||||||
|
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1
|
||||||
|
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=['view_issues', 'view_us', 'view_tasks', 'view_wiki_pages'])
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
|
||||||
|
task = f.TaskFactory.create(project=project, owner=member2.user)
|
||||||
|
history_change = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.change,
|
||||||
|
key="tasks.task:{}".format(task.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
history_create = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.create,
|
||||||
|
key="tasks.task:{}".format(task.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
history_delete = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="test:delete",
|
||||||
|
type=HistoryType.delete,
|
||||||
|
key="tasks.task:{}".format(task.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
take_snapshot(task, user=task.owner)
|
||||||
|
services.send_notifications(task,
|
||||||
|
history=history_create)
|
||||||
|
|
||||||
|
services.send_notifications(task,
|
||||||
|
history=history_change)
|
||||||
|
|
||||||
|
services.send_notifications(task,
|
||||||
|
history=history_delete)
|
||||||
|
|
||||||
|
assert models.HistoryChangeNotification.objects.count() == 3
|
||||||
|
assert len(mail.outbox) == 0
|
||||||
|
time.sleep(1)
|
||||||
|
services.process_sync_notifications()
|
||||||
|
assert len(mail.outbox) == 3
|
||||||
|
|
||||||
|
# test headers
|
||||||
|
domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"]
|
||||||
|
for msg in mail.outbox:
|
||||||
|
m_id = "{project_slug}/{msg_id}".format(
|
||||||
|
project_slug=project.slug,
|
||||||
|
msg_id=task.ref
|
||||||
|
)
|
||||||
|
|
||||||
|
message_id = "<{m_id}/".format(m_id=m_id)
|
||||||
|
message_id_domain = "@{domain}>".format(domain=domain)
|
||||||
|
in_reply_to = "<{m_id}@{domain}>".format(m_id=m_id, domain=domain)
|
||||||
|
list_id = "Taiga/{p_name} <taiga.{p_slug}@{domain}>" \
|
||||||
|
.format(p_name=project.name, p_slug=project.slug, domain=domain)
|
||||||
|
|
||||||
|
assert msg.extra_headers
|
||||||
|
headers = msg.extra_headers
|
||||||
|
|
||||||
|
# can't test the time part because it's set when sending
|
||||||
|
# check what we can
|
||||||
|
assert 'Message-ID' in headers
|
||||||
|
assert message_id in headers.get('Message-ID')
|
||||||
|
assert message_id_domain in headers.get('Message-ID')
|
||||||
|
|
||||||
|
assert 'In-Reply-To' in headers
|
||||||
|
assert in_reply_to == headers.get('In-Reply-To')
|
||||||
|
assert 'References' in headers
|
||||||
|
assert in_reply_to == headers.get('References')
|
||||||
|
|
||||||
|
assert 'List-ID' in headers
|
||||||
|
assert list_id == headers.get('List-ID')
|
||||||
|
|
||||||
|
assert 'Thread-Index' in headers
|
||||||
|
# always is b64 encoded 22 bytes
|
||||||
|
assert len(base64.b64decode(headers.get('Thread-Index'))) == 22
|
||||||
|
|
||||||
|
# hashes should match for identical ids and times
|
||||||
|
# we check the actual method in test_ms_thread_id()
|
||||||
|
msg_time = headers.get('Message-ID').split('/')[2].split('@')[0]
|
||||||
|
msg_ts = datetime.datetime.fromtimestamp(int(msg_time))
|
||||||
|
assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index')
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_notifications_using_services_method_for_issues(settings, mail):
|
||||||
|
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1
|
||||||
|
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=['view_issues', 'view_us', 'view_tasks', 'view_wiki_pages'])
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
|
||||||
|
issue = f.IssueFactory.create(project=project, owner=member2.user)
|
||||||
|
history_change = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.change,
|
||||||
|
key="issues.issue:{}".format(issue.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
history_create = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.create,
|
||||||
|
key="issues.issue:{}".format(issue.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
history_delete = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="test:delete",
|
||||||
|
type=HistoryType.delete,
|
||||||
|
key="issues.issue:{}".format(issue.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
take_snapshot(issue, user=issue.owner)
|
||||||
|
services.send_notifications(issue,
|
||||||
|
history=history_create)
|
||||||
|
|
||||||
|
services.send_notifications(issue,
|
||||||
|
history=history_change)
|
||||||
|
|
||||||
|
services.send_notifications(issue,
|
||||||
|
history=history_delete)
|
||||||
|
|
||||||
|
assert models.HistoryChangeNotification.objects.count() == 3
|
||||||
|
assert len(mail.outbox) == 0
|
||||||
|
time.sleep(1)
|
||||||
|
services.process_sync_notifications()
|
||||||
|
assert len(mail.outbox) == 3
|
||||||
|
|
||||||
|
# test headers
|
||||||
|
domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"]
|
||||||
|
for msg in mail.outbox:
|
||||||
|
m_id = "{project_slug}/{msg_id}".format(
|
||||||
|
project_slug=project.slug,
|
||||||
|
msg_id=issue.ref
|
||||||
|
)
|
||||||
|
|
||||||
|
message_id = "<{m_id}/".format(m_id=m_id)
|
||||||
|
message_id_domain = "@{domain}>".format(domain=domain)
|
||||||
|
in_reply_to = "<{m_id}@{domain}>".format(m_id=m_id, domain=domain)
|
||||||
|
list_id = "Taiga/{p_name} <taiga.{p_slug}@{domain}>" \
|
||||||
|
.format(p_name=project.name, p_slug=project.slug, domain=domain)
|
||||||
|
|
||||||
|
assert msg.extra_headers
|
||||||
|
headers = msg.extra_headers
|
||||||
|
|
||||||
|
# can't test the time part because it's set when sending
|
||||||
|
# check what we can
|
||||||
|
assert 'Message-ID' in headers
|
||||||
|
assert message_id in headers.get('Message-ID')
|
||||||
|
assert message_id_domain in headers.get('Message-ID')
|
||||||
|
|
||||||
|
assert 'In-Reply-To' in headers
|
||||||
|
assert in_reply_to == headers.get('In-Reply-To')
|
||||||
|
assert 'References' in headers
|
||||||
|
assert in_reply_to == headers.get('References')
|
||||||
|
|
||||||
|
assert 'List-ID' in headers
|
||||||
|
assert list_id == headers.get('List-ID')
|
||||||
|
|
||||||
|
assert 'Thread-Index' in headers
|
||||||
|
# always is b64 encoded 22 bytes
|
||||||
|
assert len(base64.b64decode(headers.get('Thread-Index'))) == 22
|
||||||
|
|
||||||
|
# hashes should match for identical ids and times
|
||||||
|
# we check the actual method in test_ms_thread_id()
|
||||||
|
msg_time = headers.get('Message-ID').split('/')[2].split('@')[0]
|
||||||
|
msg_ts = datetime.datetime.fromtimestamp(int(msg_time))
|
||||||
|
assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index')
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_notifications_using_services_method_for_wiki_pages(settings, mail):
|
||||||
|
settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1
|
||||||
|
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=['view_issues', 'view_us', 'view_tasks', 'view_wiki_pages'])
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
|
||||||
|
wiki = f.WikiPageFactory.create(project=project, owner=member2.user)
|
||||||
|
history_change = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.change,
|
||||||
|
key="wiki.wikipage:{}".format(wiki.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
history_create = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="",
|
||||||
|
type=HistoryType.create,
|
||||||
|
key="wiki.wikipage:{}".format(wiki.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
history_delete = f.HistoryEntryFactory.create(
|
||||||
|
user={"pk": member1.user.id},
|
||||||
|
comment="test:delete",
|
||||||
|
type=HistoryType.delete,
|
||||||
|
key="wiki.wikipage:{}".format(wiki.id),
|
||||||
|
is_hidden=False,
|
||||||
|
diff=[]
|
||||||
|
)
|
||||||
|
take_snapshot(wiki, user=wiki.owner)
|
||||||
|
services.send_notifications(wiki,
|
||||||
|
history=history_create)
|
||||||
|
|
||||||
|
services.send_notifications(wiki,
|
||||||
|
history=history_change)
|
||||||
|
|
||||||
|
services.send_notifications(wiki,
|
||||||
|
history=history_delete)
|
||||||
|
|
||||||
|
assert models.HistoryChangeNotification.objects.count() == 3
|
||||||
|
assert len(mail.outbox) == 0
|
||||||
|
time.sleep(1)
|
||||||
|
services.process_sync_notifications()
|
||||||
|
assert len(mail.outbox) == 3
|
||||||
|
|
||||||
|
# test headers
|
||||||
|
domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"]
|
||||||
|
for msg in mail.outbox:
|
||||||
|
m_id = "{project_slug}/{msg_id}".format(
|
||||||
|
project_slug=project.slug,
|
||||||
|
msg_id=wiki.slug
|
||||||
|
)
|
||||||
|
|
||||||
|
message_id = "<{m_id}/".format(m_id=m_id)
|
||||||
|
message_id_domain = "@{domain}>".format(domain=domain)
|
||||||
|
in_reply_to = "<{m_id}@{domain}>".format(m_id=m_id, domain=domain)
|
||||||
|
list_id = "Taiga/{p_name} <taiga.{p_slug}@{domain}>" \
|
||||||
|
.format(p_name=project.name, p_slug=project.slug, domain=domain)
|
||||||
|
|
||||||
|
assert msg.extra_headers
|
||||||
|
headers = msg.extra_headers
|
||||||
|
|
||||||
|
# can't test the time part because it's set when sending
|
||||||
|
# check what we can
|
||||||
|
assert 'Message-ID' in headers
|
||||||
|
assert message_id in headers.get('Message-ID')
|
||||||
|
assert message_id_domain in headers.get('Message-ID')
|
||||||
|
|
||||||
|
assert 'In-Reply-To' in headers
|
||||||
|
assert in_reply_to == headers.get('In-Reply-To')
|
||||||
|
assert 'References' in headers
|
||||||
|
assert in_reply_to == headers.get('References')
|
||||||
|
|
||||||
|
assert 'List-ID' in headers
|
||||||
|
assert list_id == headers.get('List-ID')
|
||||||
|
|
||||||
|
assert 'Thread-Index' in headers
|
||||||
|
# always is b64 encoded 22 bytes
|
||||||
|
assert len(base64.b64decode(headers.get('Thread-Index'))) == 22
|
||||||
|
|
||||||
|
# hashes should match for identical ids and times
|
||||||
|
# we check the actual method in test_ms_thread_id()
|
||||||
|
msg_time = headers.get('Message-ID').split('/')[2].split('@')[0]
|
||||||
|
msg_ts = datetime.datetime.fromtimestamp(int(msg_time))
|
||||||
|
assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index')
|
||||||
|
|
||||||
|
|
||||||
def test_resource_notification_test(client, settings, mail):
|
def test_resource_notification_test(client, settings, mail):
|
||||||
|
|
|
@ -200,7 +200,6 @@ def test_update_project_timeline():
|
||||||
project = factories.ProjectFactory.create(name="test project timeline")
|
project = factories.ProjectFactory.create(name="test project timeline")
|
||||||
history_services.take_snapshot(project, user=project.owner)
|
history_services.take_snapshot(project, user=project.owner)
|
||||||
project.add_watcher(user_watcher)
|
project.add_watcher(user_watcher)
|
||||||
print("PPPP")
|
|
||||||
project.name = "test project timeline updated"
|
project.name = "test project timeline updated"
|
||||||
project.save()
|
project.save()
|
||||||
history_services.take_snapshot(project, user=project.owner)
|
history_services.take_snapshot(project, user=project.owner)
|
||||||
|
|
|
@ -483,9 +483,6 @@ def test_get_voted_list_valid_info_for_project():
|
||||||
|
|
||||||
assert project_vote_info["is_private"] == project.is_private
|
assert project_vote_info["is_private"] == project.is_private
|
||||||
|
|
||||||
import pprint
|
|
||||||
pprint.pprint(project_vote_info)
|
|
||||||
|
|
||||||
assert project_vote_info["is_fan"] == False
|
assert project_vote_info["is_fan"] == False
|
||||||
assert project_vote_info["is_watcher"] == False
|
assert project_vote_info["is_watcher"] == False
|
||||||
assert project_vote_info["total_watchers"] == 0
|
assert project_vote_info["total_watchers"] == 0
|
||||||
|
|
|
@ -57,7 +57,7 @@ def test_create_userstory_with_watchers(client):
|
||||||
data = {"subject": "Test user story", "project": project.id, "watchers": [user_watcher.id]}
|
data = {"subject": "Test user story", "project": project.id, "watchers": [user_watcher.id]}
|
||||||
client.login(user)
|
client.login(user)
|
||||||
response = client.json.post(url, json.dumps(data))
|
response = client.json.post(url, json.dumps(data))
|
||||||
print(response.data)
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
assert response.data["watchers"] == []
|
assert response.data["watchers"] == []
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ def test_export_issue_finish_date(client):
|
||||||
issue = f.IssueFactory.create(finished_date="2014-10-22")
|
issue = f.IssueFactory.create(finished_date="2014-10-22")
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
render_project(issue.project, output)
|
render_project(issue.project, output)
|
||||||
print(output.getvalue())
|
|
||||||
project_data = json.loads(output.getvalue())
|
project_data = json.loads(output.getvalue())
|
||||||
finish_date = project_data["issues"][0]["finished_date"]
|
finish_date = project_data["issues"][0]["finished_date"]
|
||||||
assert finish_date == "2014-10-22T00:00:00+0000"
|
assert finish_date == "2014-10-22T00:00:00+0000"
|
||||||
|
|
|
@ -165,8 +165,8 @@ def test_render_relative_image():
|
||||||
|
|
||||||
|
|
||||||
def test_render_triple_quote_code():
|
def test_render_triple_quote_code():
|
||||||
expected_result = "<div class=\"codehilite\"><pre><span class=\"n\">print</span><span class=\"p\">(</span><span class=\"s\">\"test\"</span><span class=\"p\">)</span>\n</pre></div>"
|
expected_result = "<div class=\"codehilite\"><pre><span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s\">\"test\"</span><span class=\"p\">)</span>\n</pre></div>"
|
||||||
assert render(dummy_project, "```\nprint(\"test\")\n```") == expected_result
|
assert render(dummy_project, "```python\nprint(\"test\")\n```") == expected_result
|
||||||
|
|
||||||
|
|
||||||
def test_render_triple_quote_and_lang_code():
|
def test_render_triple_quote_and_lang_code():
|
||||||
|
|
Loading…
Reference in New Issue