Migrate to django 1.8 and make taiga compatible with python 3.5

remotes/origin/logger
David Barragán Merino 2015-10-05 15:41:17 +02:00
parent 9226913caa
commit a7a6bd3a1c
40 changed files with 563 additions and 218 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

@ -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",
]

View File

@ -16,6 +16,8 @@
from .development import * from .development import *
#DEBUG = False
#ADMINS = ( #ADMINS = (
# ("Admin", "example@example.com"), # ("Admin", "example@example.com"),
#) #)

View File

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

View File

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

View File

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

42
taiga/base/mails.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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='+'),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,4 +41,4 @@ class UserCreationForm(DjangoUserCreationForm):
class UserChangeForm(DjangoUserChangeForm): class UserChangeForm(DjangoUserChangeForm):
class Meta: class Meta:
model = User model = User
fields = '__all__'

View File

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

View File

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

View File

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

View File

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

View File

@ -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"] == []

View File

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

View File

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