Merge branch 'master' into stable
Conflicts: taiga/projects/serializers.pyremotes/origin/enhancement/email-actions 1.5.0
commit
be710a4545
|
@ -11,3 +11,4 @@ media
|
||||||
.coverage
|
.coverage
|
||||||
.cache
|
.cache
|
||||||
.\#*
|
.\#*
|
||||||
|
.project
|
||||||
|
|
|
@ -8,6 +8,7 @@ addons:
|
||||||
before_script:
|
before_script:
|
||||||
- psql -c 'create database taiga;' -U postgres
|
- psql -c 'create database taiga;' -U postgres
|
||||||
install:
|
install:
|
||||||
|
- sudo apt-get update
|
||||||
- sudo apt-get install postgresql-plpython-9.3
|
- sudo apt-get install postgresql-plpython-9.3
|
||||||
- pip install -r requirements-devel.txt --use-mirrors
|
- pip install -r requirements-devel.txt --use-mirrors
|
||||||
script:
|
script:
|
||||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,5 +1,19 @@
|
||||||
# Changelog #
|
# Changelog #
|
||||||
|
|
||||||
|
## 1.5.0 Betula Pendula - FOSDEM 2015 (2015-01-29)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Improving SQL queries and performance.
|
||||||
|
- Now you can export and import projects between Taiga instances.
|
||||||
|
- Email redesign.
|
||||||
|
- Support for archived status (not shown by default in Kanban).
|
||||||
|
- Removing files from filesystem when deleting attachments.
|
||||||
|
- Support for contrib plugins (existing yet: slack, hall and gogs).
|
||||||
|
- Webhooks added (crazy integrations are welcome).
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
- Lots of small and not so small bugfixes.
|
||||||
|
|
||||||
|
|
||||||
## 1.4.0 Abies veitchii (2014-12-10)
|
## 1.4.0 Abies veitchii (2014-12-10)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
[](https://travis-ci.org/taigaio/taiga-back "Travis Badge")
|
[](https://travis-ci.org/taigaio/taiga-back "Travis Badge")
|
||||||
|
|
||||||
[](https://travis-ci.org/taigaio/taiga-back "Coveralls")
|
[](https://coveralls.io/r/taigaio/taiga-back?branch=master "Coveralls")
|
||||||
|
|
||||||
## Setup development environment ##
|
## Setup development environment ##
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ Taiga only runs with python 3.4+
|
||||||
Initial auth data: admin/123123
|
Initial auth data: admin/123123
|
||||||
|
|
||||||
If you want a complete environment for production usage, you can try the taiga bootstrapping
|
If you want a complete environment for production usage, you can try the taiga bootstrapping
|
||||||
scripts https://github.com/taigaio/taiga-scripts (warning: alpha state)
|
scripts https://github.com/taigaio/taiga-scripts (warning: alpha state). All the information about the different installation methods (production, development, vagrant, docker...) can be found here http://taigaio.github.io/taiga-doc/dist/#_installation_guide.
|
||||||
|
|
||||||
## Community ##
|
## Community ##
|
||||||
|
|
||||||
|
|
|
@ -19,14 +19,16 @@ Markdown==2.4.1
|
||||||
fn==0.2.13
|
fn==0.2.13
|
||||||
diff-match-patch==20121119
|
diff-match-patch==20121119
|
||||||
requests==2.4.1
|
requests==2.4.1
|
||||||
|
django-sr==0.0.4
|
||||||
easy-thumbnails==2.1
|
easy-thumbnails==2.1
|
||||||
celery==3.1.12
|
celery==3.1.17
|
||||||
redis==2.10.3
|
redis==2.10.3
|
||||||
Unidecode==0.04.16
|
Unidecode==0.04.16
|
||||||
raven==5.1.1
|
raven==5.1.1
|
||||||
bleach==1.4
|
bleach==1.4
|
||||||
django-ipware==0.1.0
|
django-ipware==0.1.0
|
||||||
|
premailer==2.8.1
|
||||||
|
django-transactional-cleanup==0.1.12
|
||||||
|
|
||||||
# Comment it if you are using python >= 3.4
|
# Comment it if you are using python >= 3.4
|
||||||
enum34==1.0
|
enum34==1.0
|
||||||
|
|
|
@ -33,7 +33,7 @@ LANGUAGES = (
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
"ENGINE": "transaction_hooks.backends.postgresql_psycopg2",
|
||||||
"NAME": "taiga",
|
"NAME": "taiga",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,12 +197,16 @@ INSTALLED_APPS = [
|
||||||
"taiga.hooks.github",
|
"taiga.hooks.github",
|
||||||
"taiga.hooks.gitlab",
|
"taiga.hooks.gitlab",
|
||||||
"taiga.hooks.bitbucket",
|
"taiga.hooks.bitbucket",
|
||||||
|
"taiga.webhooks",
|
||||||
|
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"djmail",
|
"djmail",
|
||||||
"django_jinja",
|
"django_jinja",
|
||||||
|
"django_jinja.contrib._humanize",
|
||||||
|
"sr",
|
||||||
"easy_thumbnails",
|
"easy_thumbnails",
|
||||||
"raven.contrib.django.raven_compat",
|
"raven.contrib.django.raven_compat",
|
||||||
|
"django_transactional_cleanup",
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "taiga.wsgi.application"
|
WSGI_APPLICATION = "taiga.wsgi.application"
|
||||||
|
@ -300,7 +304,8 @@ REST_FRAMEWORK = {
|
||||||
"DEFAULT_THROTTLE_RATES": {
|
"DEFAULT_THROTTLE_RATES": {
|
||||||
"anon": None,
|
"anon": None,
|
||||||
"user": None,
|
"user": None,
|
||||||
"import-mode": None
|
"import-mode": None,
|
||||||
|
"import-dump-mode": "1/minute",
|
||||||
},
|
},
|
||||||
"FILTER_BACKEND": "taiga.base.filters.FilterBackend",
|
"FILTER_BACKEND": "taiga.base.filters.FilterBackend",
|
||||||
"EXCEPTION_HANDLER": "taiga.base.exceptions.exception_handler",
|
"EXCEPTION_HANDLER": "taiga.base.exceptions.exception_handler",
|
||||||
|
@ -362,6 +367,13 @@ PROJECT_MODULES_CONFIGURATORS = {
|
||||||
BITBUCKET_VALID_ORIGIN_IPS = ["131.103.20.165", "131.103.20.166"]
|
BITBUCKET_VALID_ORIGIN_IPS = ["131.103.20.165", "131.103.20.166"]
|
||||||
GITLAB_VALID_ORIGIN_IPS = []
|
GITLAB_VALID_ORIGIN_IPS = []
|
||||||
|
|
||||||
|
EXPORTS_TTL = 60 * 60 * 24 # 24 hours
|
||||||
|
CELERY_ENABLED = False
|
||||||
|
WEBHOOKS_ENABLED = False
|
||||||
|
|
||||||
|
from .sr import *
|
||||||
|
|
||||||
|
|
||||||
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
||||||
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
SR = {
|
||||||
|
"taigaio_url": "https://taiga.io",
|
||||||
|
"social": {
|
||||||
|
"twitter_url": "https://twitter.com/taigaio",
|
||||||
|
"github_url": "https://github.com/taigaio",
|
||||||
|
},
|
||||||
|
"support": {
|
||||||
|
"url": "https://taiga.io/support",
|
||||||
|
"email": "support@taiga.io",
|
||||||
|
"mailing_list": "https://groups.google.com/forum/#!forum/taigaio",
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ from .development import *
|
||||||
SKIP_SOUTH_TESTS = True
|
SKIP_SOUTH_TESTS = True
|
||||||
SOUTH_TESTS_MIGRATE = False
|
SOUTH_TESTS_MIGRATE = False
|
||||||
CELERY_ALWAYS_EAGER = True
|
CELERY_ALWAYS_EAGER = True
|
||||||
|
CELERY_ENABLED = False
|
||||||
|
|
||||||
MEDIA_ROOT = "/tmp"
|
MEDIA_ROOT = "/tmp"
|
||||||
|
|
||||||
|
@ -28,5 +29,6 @@ INSTALLED_APPS = INSTALLED_APPS + ["tests"]
|
||||||
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
|
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
|
||||||
"anon": None,
|
"anon": None,
|
||||||
"user": None,
|
"user": None,
|
||||||
"import-mode": None
|
"import-mode": None,
|
||||||
|
"import-dump-mode": None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ 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
|
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
||||||
|
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.users.serializers import UserSerializer
|
from taiga.users.serializers import UserSerializer
|
||||||
|
@ -46,7 +46,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()
|
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
||||||
email = mbuilder.registered_user(user.email, context)
|
email = mbuilder.registered_user(user.email, context)
|
||||||
return bool(email.send())
|
return bool(email.send())
|
||||||
|
|
||||||
|
|
|
@ -204,6 +204,52 @@ class IsProjectMemberFilterBackend(FilterBackend):
|
||||||
|
|
||||||
return super().filter_queryset(request, queryset.distinct(), view)
|
return super().filter_queryset(request, queryset.distinct(), view)
|
||||||
|
|
||||||
|
class BaseIsProjectAdminFilterBackend(object):
|
||||||
|
def get_project_ids(self, request, view):
|
||||||
|
project_id = None
|
||||||
|
if hasattr(view, "filter_fields") and "project" in view.filter_fields:
|
||||||
|
project_id = request.QUERY_PARAMS.get("project", None)
|
||||||
|
|
||||||
|
if request.user.is_authenticated() and request.user.is_superuser:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not request.user.is_authenticated():
|
||||||
|
return []
|
||||||
|
|
||||||
|
memberships_qs = Membership.objects.filter(user=request.user, is_owner=True)
|
||||||
|
if project_id:
|
||||||
|
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||||
|
|
||||||
|
projects_list = [membership.project_id for membership in memberships_qs]
|
||||||
|
|
||||||
|
return projects_list
|
||||||
|
|
||||||
|
|
||||||
|
class IsProjectAdminFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend):
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
project_ids = self.get_project_ids(request, view)
|
||||||
|
if project_ids is None:
|
||||||
|
queryset = queryset
|
||||||
|
elif project_ids == []:
|
||||||
|
queryset = queryset.none()
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(project_id__in=project_ids)
|
||||||
|
|
||||||
|
return super().filter_queryset(request, queryset.distinct(), view)
|
||||||
|
|
||||||
|
|
||||||
|
class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend):
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
project_ids = self.get_project_ids(request, view)
|
||||||
|
if project_ids is None:
|
||||||
|
queryset = queryset
|
||||||
|
elif project_ids == []:
|
||||||
|
queryset = queryset.none()
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(webhook__project_id__in=project_ids)
|
||||||
|
|
||||||
|
return super().filter_queryset(request, queryset, view)
|
||||||
|
|
||||||
|
|
||||||
class TagsFilter(FilterBackend):
|
class TagsFilter(FilterBackend):
|
||||||
def __init__(self, filter_name='tags'):
|
def __init__(self, filter_name='tags'):
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.db.models.loading import get_model
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
||||||
|
|
||||||
|
from taiga.projects.models import Project, Membership
|
||||||
|
from taiga.projects.history.models import HistoryEntry
|
||||||
|
from taiga.projects.history.services import get_history_queryset_by_model_instance
|
||||||
|
from taiga.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
args = '<email>'
|
||||||
|
help = 'Send an example of all emails'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if len(args) != 1:
|
||||||
|
print("Usage: ./manage.py test_emails <email-address>")
|
||||||
|
return
|
||||||
|
|
||||||
|
test_email = args[0]
|
||||||
|
|
||||||
|
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
||||||
|
|
||||||
|
# Register email
|
||||||
|
context = {"user": User.objects.all().order_by("?").first(), "cancel_token": "cancel-token"}
|
||||||
|
email = mbuilder.registered_user(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
# Membership invitation
|
||||||
|
membership = Membership.objects.order_by("?").filter(user__isnull=True).first()
|
||||||
|
membership.invited_by = User.objects.all().order_by("?").first()
|
||||||
|
membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example"
|
||||||
|
|
||||||
|
context = {"membership": membership}
|
||||||
|
email = mbuilder.membership_invitation(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
# Membership notification
|
||||||
|
context = {"membership": Membership.objects.order_by("?").filter(user__isnull=False).first()}
|
||||||
|
email = mbuilder.membership_notification(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
# Feedback
|
||||||
|
context = {
|
||||||
|
"feedback_entry": {
|
||||||
|
"full_name": "Test full name",
|
||||||
|
"email": "test@email.com",
|
||||||
|
"comment": "Test comment",
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
email = mbuilder.feedback_notification(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
# Password recovery
|
||||||
|
context = {"user": User.objects.all().order_by("?").first()}
|
||||||
|
email = mbuilder.password_recovery(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
# Change email
|
||||||
|
context = {"user": User.objects.all().order_by("?").first()}
|
||||||
|
email = mbuilder.change_email(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
# Export/Import emails
|
||||||
|
context = {
|
||||||
|
"user": User.objects.all().order_by("?").first(),
|
||||||
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
|
"error_subject": "Error generating project dump",
|
||||||
|
"error_message": "Error generating project dump",
|
||||||
|
}
|
||||||
|
email = mbuilder.export_error(test_email, context)
|
||||||
|
email.send()
|
||||||
|
context = {
|
||||||
|
"user": User.objects.all().order_by("?").first(),
|
||||||
|
"error_subject": "Error importing project dump",
|
||||||
|
"error_message": "Error importing project dump",
|
||||||
|
}
|
||||||
|
email = mbuilder.import_error(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
deletion_date = timezone.now() + datetime.timedelta(seconds=60*60*24)
|
||||||
|
context = {
|
||||||
|
"url": "http://dummyurl.com",
|
||||||
|
"user": User.objects.all().order_by("?").first(),
|
||||||
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
|
"deletion_date": deletion_date,
|
||||||
|
}
|
||||||
|
email = mbuilder.dump_project(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"user": User.objects.all().order_by("?").first(),
|
||||||
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
|
}
|
||||||
|
email = mbuilder.load_dump(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
# Notification emails
|
||||||
|
notification_emails = [
|
||||||
|
("issues.Issue", "issues/issue-change"),
|
||||||
|
("issues.Issue", "issues/issue-create"),
|
||||||
|
("issues.Issue", "issues/issue-delete"),
|
||||||
|
("tasks.Task", "tasks/task-change"),
|
||||||
|
("tasks.Task", "tasks/task-create"),
|
||||||
|
("tasks.Task", "tasks/task-delete"),
|
||||||
|
("userstories.UserStory", "userstories/userstory-change"),
|
||||||
|
("userstories.UserStory", "userstories/userstory-create"),
|
||||||
|
("userstories.UserStory", "userstories/userstory-delete"),
|
||||||
|
("milestones.Milestone", "milestones/milestone-change"),
|
||||||
|
("milestones.Milestone", "milestones/milestone-create"),
|
||||||
|
("milestones.Milestone", "milestones/milestone-delete"),
|
||||||
|
("wiki.WikiPage", "wiki/wikipage-change"),
|
||||||
|
("wiki.WikiPage", "wiki/wikipage-create"),
|
||||||
|
("wiki.WikiPage", "wiki/wikipage-delete"),
|
||||||
|
]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
|
"changer": User.objects.all().order_by("?").first(),
|
||||||
|
"history_entries": HistoryEntry.objects.all().order_by("?")[0:5],
|
||||||
|
"user": User.objects.all().order_by("?").first(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for notification_email in notification_emails:
|
||||||
|
model = get_model(*notification_email[0].split("."))
|
||||||
|
snapshot = {
|
||||||
|
"subject": "Tests subject",
|
||||||
|
"ref": 123123,
|
||||||
|
"name": "Tests name",
|
||||||
|
"slug": "test-slug"
|
||||||
|
}
|
||||||
|
queryset = model.objects.all().order_by("?")
|
||||||
|
for obj in queryset:
|
||||||
|
end = False
|
||||||
|
entries = get_history_queryset_by_model_instance(obj).filter(is_snapshot=True).order_by("?")
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
if entry.snapshot:
|
||||||
|
snapshot = entry.snapshot
|
||||||
|
end = True
|
||||||
|
break
|
||||||
|
if end:
|
||||||
|
break
|
||||||
|
context["snapshot"] = snapshot
|
||||||
|
|
||||||
|
cls = type("InlineCSSTemplateMail", (InlineCSSTemplateMail,), {"name": notification_email[1]})
|
||||||
|
email = cls()
|
||||||
|
email.send(test_email, context)
|
|
@ -21,7 +21,7 @@ from rest_framework import serializers
|
||||||
from .neighbors import get_neighbors
|
from .neighbors import get_neighbors
|
||||||
|
|
||||||
|
|
||||||
class PickleField(serializers.WritableField):
|
class TagsField(serializers.WritableField):
|
||||||
"""
|
"""
|
||||||
Pickle objects serializer.
|
Pickle objects serializer.
|
||||||
"""
|
"""
|
||||||
|
@ -29,7 +29,11 @@ class PickleField(serializers.WritableField):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def from_native(self, data):
|
def from_native(self, data):
|
||||||
return data
|
if not data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
ret = sum([tag.split(",") for tag in data], [])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class JsonField(serializers.WritableField):
|
class JsonField(serializers.WritableField):
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
|
@ -0,0 +1,453 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>{{ _("Taiga") }}</title>
|
||||||
|
<style type="text/css">
|
||||||
|
/* /\/\/\/\/\/\/\/\/ CLIENT-SPECIFIC STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
#outlook a{padding:0;} /* Force Outlook to provide a "view in browser" message */
|
||||||
|
.ReadMsgBody{width:100%;} .ExternalClass{width:100%;} /* Force Hotmail to display emails at full width */
|
||||||
|
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;} /* Force Hotmail to display normal line spacing */
|
||||||
|
body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;} /* Prevent WebKit and Windows mobile changing default text sizes */
|
||||||
|
table, td{mso-table-lspace:0pt; mso-table-rspace:0pt;} /* Remove spacing between tables in Outlook 2007 and up */
|
||||||
|
img{-ms-interpolation-mode:bicubic;} /* Allow smoother rendering of resized image in Internet Explorer */
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ RESET STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
body{margin:0; padding:0;}
|
||||||
|
img{border:0; height:auto; line-height:100%; outline:none; text-decoration:none;}
|
||||||
|
table{border-collapse:collapse !important;}
|
||||||
|
body, #bodyTable, #bodyCell{height:100% !important; margin:0; padding:0; width:100% !important;}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ TEMPLATE STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
#bodyCell{padding:20px;}
|
||||||
|
#templateContainer{width:600px;}
|
||||||
|
|
||||||
|
/* ========== Page Styles ========== */
|
||||||
|
|
||||||
|
body, #bodyTable{
|
||||||
|
background-color:#f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section email border
|
||||||
|
*/
|
||||||
|
#templateContainer{
|
||||||
|
background-color:#FFF;
|
||||||
|
border:1px solid #CDCDCD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 1
|
||||||
|
*/
|
||||||
|
h1{
|
||||||
|
color: #6e6e6e !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial;
|
||||||
|
font-size:25px;
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @style heading 2
|
||||||
|
*/
|
||||||
|
h2{
|
||||||
|
color: #6e6e6e !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial;
|
||||||
|
font-size:20px;
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Page
|
||||||
|
* @section heading 3
|
||||||
|
*/
|
||||||
|
h3{
|
||||||
|
color:#6e6e6e !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:16px;
|
||||||
|
font-weight:normal;
|
||||||
|
line-height:100%;
|
||||||
|
font-weight:bold;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Page
|
||||||
|
* @section heading 4
|
||||||
|
*/
|
||||||
|
h4{
|
||||||
|
color:#808080 !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:14px;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ========== Header Styles ========== */
|
||||||
|
|
||||||
|
.headerContent {
|
||||||
|
text-align: center;
|
||||||
|
color:#b8b8b8 !important;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:14px;
|
||||||
|
margin-bottom:16px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerImage{
|
||||||
|
height:auto;
|
||||||
|
width:80px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Body Styles ========== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Body
|
||||||
|
* @section body style
|
||||||
|
* @tip Set the background color and borders for your email's body area.
|
||||||
|
*/
|
||||||
|
#templateBody{
|
||||||
|
background-color:#FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body text
|
||||||
|
*/
|
||||||
|
.bodyContent{
|
||||||
|
color:#505050;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:16px;
|
||||||
|
line-height:150%;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
padding-top:20px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link
|
||||||
|
*/
|
||||||
|
.bodyContent a:link, .bodyContent a:visited, /* Yahoo! Mail Override */ .bodyContent a .yshortcuts /* Yahoo! Mail Override */{
|
||||||
|
color:#699b05;
|
||||||
|
font-weight:normal;
|
||||||
|
text-decoration:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link button class
|
||||||
|
*/
|
||||||
|
a.button {
|
||||||
|
background: #699b05;
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: .8rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button:hover {
|
||||||
|
background: #aad400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyContent img{
|
||||||
|
display:inline;
|
||||||
|
height:auto;
|
||||||
|
max-width:560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row h1,
|
||||||
|
.update-row h2,
|
||||||
|
.update-row h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row tr {
|
||||||
|
border-bottom: 1px solid #cdcdcd;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row tr:first-child,
|
||||||
|
.update-row tr:last-child {
|
||||||
|
border-bottom: 3px solid #cdcdcd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row td {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row td.update-row-name {
|
||||||
|
width: 40%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row td.update-row-from {
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:13px;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:link, .social-links a:visited{
|
||||||
|
color:#699b05;
|
||||||
|
font-weight:normal;
|
||||||
|
text-decoration:underline;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Footer Styles ========== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer style
|
||||||
|
*/
|
||||||
|
#templateFooter{
|
||||||
|
background-color:#555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer text
|
||||||
|
*/
|
||||||
|
.footerContent{
|
||||||
|
color:#f5f5f5;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:10px;
|
||||||
|
line-height:150%;
|
||||||
|
padding-top:20px;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Footer
|
||||||
|
* @section footer link
|
||||||
|
* @tip Set the styling for your email's footer links. Choose a color that helps them stand out from your text.
|
||||||
|
*/
|
||||||
|
.footerContent a:link, .footerContent a:visited, /* Yahoo! Mail Override */ .footerContent a .yshortcuts, .footerContent a span /* Yahoo! Mail Override */{
|
||||||
|
/*@editable*/ color:#699b05;
|
||||||
|
/*@editable*/ font-weight:normal;
|
||||||
|
/*@editable*/ text-decoration:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ MOBILE STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px){
|
||||||
|
/* /\/\/\/\/\/\/ CLIENT-SPECIFIC MOBILE STYLES /\/\/\/\/\/\/ */
|
||||||
|
body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:none !important;} /* Prevent Webkit platforms from changing default text sizes */
|
||||||
|
body{width:100% !important; min-width:100% !important;} /* Prevent iOS Mail from adding padding to the body */
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/ MOBILE RESET STYLES /\/\/\/\/\/\/ */
|
||||||
|
#bodyCell{padding:10px !important;}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/ MOBILE TEMPLATE STYLES /\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
/* ======== Page Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section template width
|
||||||
|
*/
|
||||||
|
#templateContainer{
|
||||||
|
max-width:600px !important;
|
||||||
|
width:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 1
|
||||||
|
*/
|
||||||
|
h1{
|
||||||
|
font-size:18px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 2
|
||||||
|
*/
|
||||||
|
h2{
|
||||||
|
font-size:16px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 3
|
||||||
|
*/
|
||||||
|
h3{
|
||||||
|
font-size:14px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ======== Header Styles ======== */
|
||||||
|
|
||||||
|
#templatePreheader{display:none !important;} /* Hide the template preheader to save space */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section header image
|
||||||
|
*/
|
||||||
|
#headerImage{
|
||||||
|
height:auto !important;
|
||||||
|
max-width:600px !important;
|
||||||
|
width:20% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======== Body Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Mobile Styles
|
||||||
|
* @section body image
|
||||||
|
* @tip Make the main body image fluid for portrait or landscape view adaptability, and set the image's original width as the max-width. If a fluid setting doesn't work, set the image width to half its original size instead.
|
||||||
|
*/
|
||||||
|
#bodyImage{
|
||||||
|
height:auto !important;
|
||||||
|
max-width:560px !important;
|
||||||
|
width:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body text
|
||||||
|
*/
|
||||||
|
.bodyContent{
|
||||||
|
font-size:16px !important;
|
||||||
|
line-height:125% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link button class
|
||||||
|
*/
|
||||||
|
.bodyContent a.button {
|
||||||
|
font-size:14px !important;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======== Footer Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer text
|
||||||
|
*/
|
||||||
|
.footerContent{
|
||||||
|
font-size:14px !important;
|
||||||
|
line-height:115% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerContent a{display:block !important;} /* Place footer social and utility links on their own lines, for easier access */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
|
||||||
|
<center>
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" id="bodyCell">
|
||||||
|
<!-- BEGIN TEMPLATE // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" id="templateContainer">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<!-- BEGIN HEADER // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateHeader">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="headerContent">
|
||||||
|
<img src="{{ static("emails/top-bg-update.png") }}" />
|
||||||
|
<a href="{{ resolve_front_url("home") }}" title="Taiga">
|
||||||
|
<img src="{{ static("emails/logo-color.png") }}" id="headerImage" alt="Taiga logo" />
|
||||||
|
</a>
|
||||||
|
{% block body %}
|
||||||
|
{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% block social %}
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="social-links">
|
||||||
|
<a href="{{ sr("social.twitter_url") }}" title="{{ _("Follow us on Twitter") }}" style="color: #9dce0a">{{ _("Twitter") }}</a>
|
||||||
|
<a href="{{ sr("social.github_url") }}" title="{{ _("Get the code on GitHub") }}" style="color: #9dce0a">{{ _("GitHub") }}</a>
|
||||||
|
<a href="{{ sr("taigaio_url") }}" title="{{ _("Visit our website") }}" style="color: #9dce0a">{{ _("Taiga.io") }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endblock %}
|
||||||
|
</table>
|
||||||
|
<!-- // END HEADER -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<!-- BEGIN FOOTER // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateFooter">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="footerContent">
|
||||||
|
{% block footer %}
|
||||||
|
{% trans support_url=sr("support.url"),
|
||||||
|
support_email=sr("support.email"),
|
||||||
|
mailing_list_url=sr("support.mailing_list") %}
|
||||||
|
<strong>Taiga Support:</strong>
|
||||||
|
<a href="{{ support_url }}" title="Support page" style="color: #9dce0a">{{ support_url}}</a>
|
||||||
|
<br>
|
||||||
|
<strong>Contact us:</strong>
|
||||||
|
<a href="mailto:{{ support_email }}" title="Supporti email" style="color: #9dce0a">
|
||||||
|
{{ support_email }}
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<strong>Mailing list:</strong>
|
||||||
|
<a href="{{ mailing_list_url }}" title="Mailing list" style="color: #9dce0a">
|
||||||
|
{{ mailing_list_url }}
|
||||||
|
</a>
|
||||||
|
{% endtrans %}
|
||||||
|
{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- // END FOOTER -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- // END TEMPLATE -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,159 +0,0 @@
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
|
||||||
"http://www.w3.org/TR/html4/loose.dtd">
|
|
||||||
|
|
||||||
{% set home_url = resolve_front_url("home") %}
|
|
||||||
{% set home_url_name = "Taiga" %}
|
|
||||||
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<!-- So that mobile webkit will display zoomed in -->
|
|
||||||
<meta name="viewport" content="initial-scale=1.0">
|
|
||||||
<!-- disable auto telephone linking in iOS -->
|
|
||||||
<meta name="format-detection" content="telephone=no">
|
|
||||||
|
|
||||||
<title>Taiga</title>
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
/* Resets: see reset.css for details */
|
|
||||||
body {-webkit-text-size-adjust:none; -ms-text-size-adjust:none;}
|
|
||||||
body {margin:0; padding:0;}
|
|
||||||
table {border-spacing:0;}
|
|
||||||
table td {border-collapse:collapse;}
|
|
||||||
|
|
||||||
/* Constrain email width for small screens */
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
table[class="container"] {
|
|
||||||
width: 95% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Give content more room on mobile */
|
|
||||||
@media screen and (max-width: 480px) {
|
|
||||||
td[class="container-padding"] {
|
|
||||||
padding-left: 12px !important;
|
|
||||||
padding-right: 12px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Styles for forcing columns to rows */
|
|
||||||
@media only screen and (max-width : 600px) {
|
|
||||||
|
|
||||||
/* force container columns to (horizontal) blocks */
|
|
||||||
td[class="force-col"] {
|
|
||||||
display: block;
|
|
||||||
padding-right: 0 !important;
|
|
||||||
}
|
|
||||||
table[class="col-3"] {
|
|
||||||
/* unset table align="left/right" */
|
|
||||||
float: none !important;
|
|
||||||
width: 100% !important;
|
|
||||||
|
|
||||||
/* change left/right padding and margins to top/bottom ones */
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* remove bottom border for last column/row */
|
|
||||||
table[id="last-col-3"] {
|
|
||||||
border-bottom: none !important;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* align images right and shrink them a bit */
|
|
||||||
img[class="col-3-img"] {
|
|
||||||
float: right;
|
|
||||||
margin-left: 6px;
|
|
||||||
max-width: 130px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body style="margin:20px auto; padding:10px 0;" bgcolor="#eee" leftmargin="0" topmargin="0"
|
|
||||||
marginwidth="0" marginheight="0">
|
|
||||||
<!-- background -->
|
|
||||||
<table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" bgcolor="#eee">
|
|
||||||
<tr>
|
|
||||||
<td align="center" valign="top" bgcolor="#eee" style="background-color: #eee;">
|
|
||||||
|
|
||||||
<!-- container -->
|
|
||||||
<table border="0" width="600" cellpadding="0" cellspacing="0" class="container"
|
|
||||||
bgcolor="#f1f1f1">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th border="0" class="container-padding" bgcolor="#669933"
|
|
||||||
style="background-color: #669900; margin-top: 20px;
|
|
||||||
padding: 20px; font-size: 13px; line-height: 20px;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
color: #FFF; border-bottom: 5px solid #333;" align="left">
|
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0"
|
|
||||||
class="table-header">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="{{ home_url }}"
|
|
||||||
title="{{ home_url_name }}">
|
|
||||||
<img src="{{ static("emails/email-logo.png") }}" alt="Taiga" height="32" />
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td border="0" style="background: #FFFFFF; padding: 20px;
|
|
||||||
font-size: 14px; line-height: 20px;
|
|
||||||
margin-bottom: 180px;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
color: #6c6c6c;" align="left">
|
|
||||||
<!-- BODY -->
|
|
||||||
{% block body %}{% endblock %}
|
|
||||||
{#
|
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0"
|
|
||||||
class="table-body">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<h1>{{ project_name }}</h1>
|
|
||||||
<h2>{{ type }}: {{ subject }}</h2>
|
|
||||||
<p>Updated fields by <b>{{ user.get_full_name() }}</b></p>
|
|
||||||
{% block body_changes %}
|
|
||||||
<ul>
|
|
||||||
<li><b>severity</b>: from "10" to "project 2 - Normal".</li>
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
#}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td border="0" style="background-color: #fff; padding: 0 20px;
|
|
||||||
font-size: 14px; line-height: 20px;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
color: #CCC;" align="left">
|
|
||||||
{% block footer %}
|
|
||||||
{#
|
|
||||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
|
||||||
More info at:
|
|
||||||
<a href="{{ final_url }}" style="color: #666;">
|
|
||||||
{{ final_url_name }}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
#}
|
|
||||||
{% endblock %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,427 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>{{ _("You have been Taigatized") }}</title>
|
||||||
|
<style type="text/css">
|
||||||
|
/* /\/\/\/\/\/\/\/\/ CLIENT-SPECIFIC STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
#outlook a{padding:0;} /* Force Outlook to provide a "view in browser" message */
|
||||||
|
.ReadMsgBody{width:100%;} .ExternalClass{width:100%;} /* Force Hotmail to display emails at full width */
|
||||||
|
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;} /* Force Hotmail to display normal line spacing */
|
||||||
|
body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;} /* Prevent WebKit and Windows mobile changing default text sizes */
|
||||||
|
table, td{mso-table-lspace:0pt; mso-table-rspace:0pt;} /* Remove spacing between tables in Outlook 2007 and up */
|
||||||
|
img{-ms-interpolation-mode:bicubic;} /* Allow smoother rendering of resized image in Internet Explorer */
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ RESET STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
body{margin:0; padding:0;}
|
||||||
|
img{border:0; height:auto; line-height:100%; outline:none; text-decoration:none;}
|
||||||
|
table{border-collapse:collapse !important;}
|
||||||
|
body, #bodyTable, #bodyCell{height:100% !important; margin:0; padding:0; width:100% !important;}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ TEMPLATE STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
#bodyCell{padding:20px;}
|
||||||
|
#templateContainer{width:600px;}
|
||||||
|
|
||||||
|
/* ========== Page Styles ========== */
|
||||||
|
|
||||||
|
body, #bodyTable{
|
||||||
|
background-color:#f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section email border
|
||||||
|
*/
|
||||||
|
#templateContainer{
|
||||||
|
border:1px solid #CDCDCD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 1
|
||||||
|
*/
|
||||||
|
h1{
|
||||||
|
color: #fff !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial;
|
||||||
|
font-size:25px;
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @style heading 2
|
||||||
|
*/
|
||||||
|
h2{
|
||||||
|
color: #b8b8b8 !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial;
|
||||||
|
font-size:20px;
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 3
|
||||||
|
*/
|
||||||
|
h3{
|
||||||
|
color:#6e6e6e !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:16px;
|
||||||
|
font-weight:normal;
|
||||||
|
line-height:100%;
|
||||||
|
font-weight:bold;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 4
|
||||||
|
*/
|
||||||
|
h4{
|
||||||
|
color:#808080 !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:14px;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ========== Header Styles ========== */
|
||||||
|
|
||||||
|
.headerContent {
|
||||||
|
text-align: center;
|
||||||
|
color:#fff !important;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:14px;
|
||||||
|
margin-bottom:16px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerImage{
|
||||||
|
height:auto;
|
||||||
|
width:80px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Body Styles ========== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body style
|
||||||
|
*/
|
||||||
|
#templateBody{
|
||||||
|
background-color:#FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body text
|
||||||
|
*/
|
||||||
|
.bodyContent{
|
||||||
|
color:#505050;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:16px;
|
||||||
|
line-height:150%;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
padding-top:20px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link
|
||||||
|
*/
|
||||||
|
.bodyContent a:link, .bodyContent a:visited, /* Yahoo! Mail Override */ .bodyContent a .yshortcuts /* Yahoo! Mail Override */{
|
||||||
|
color:#699b05;
|
||||||
|
font-weight:normal;
|
||||||
|
text-decoration:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link button class
|
||||||
|
*/
|
||||||
|
.bodyContent a.button {
|
||||||
|
background: #699b05;
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: .8rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyContent a.button:hover {
|
||||||
|
background: #aad400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyContent img{
|
||||||
|
display:inline;
|
||||||
|
height:auto;
|
||||||
|
max-width:560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:13px;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:link, .social-links a:visited{
|
||||||
|
color:#699b05;
|
||||||
|
font-weight:normal;
|
||||||
|
text-decoration:underline;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Footer Styles ========== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer style
|
||||||
|
*/
|
||||||
|
#templateFooter{
|
||||||
|
background-color:#555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer text
|
||||||
|
*/
|
||||||
|
.footerContent{
|
||||||
|
color:#f5f5f5;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:10px;
|
||||||
|
line-height:150%;
|
||||||
|
padding-top:20px;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer link
|
||||||
|
*/
|
||||||
|
.footerContent a:link, .footerContent a:visited, /* Yahoo! Mail Override */ .footerContent a .yshortcuts, .footerContent a span /* Yahoo! Mail Override */{
|
||||||
|
color:#699b05;
|
||||||
|
font-weight:normal;
|
||||||
|
text-decoration:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ MOBILE STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px){
|
||||||
|
/* /\/\/\/\/\/\/ CLIENT-SPECIFIC MOBILE STYLES /\/\/\/\/\/\/ */
|
||||||
|
body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:none !important;} /* Prevent Webkit platforms from changing default text sizes */
|
||||||
|
body{width:100% !important; min-width:100% !important;} /* Prevent iOS Mail from adding padding to the body */
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/ MOBILE RESET STYLES /\/\/\/\/\/\/ */
|
||||||
|
#bodyCell{padding:10px !important;}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/ MOBILE TEMPLATE STYLES /\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
/* ======== Page Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section template width
|
||||||
|
*/
|
||||||
|
#templateContainer{
|
||||||
|
max-width:600px !important;
|
||||||
|
/*@editable*/ width:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 1
|
||||||
|
*/
|
||||||
|
h1{
|
||||||
|
font-size:18px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 2
|
||||||
|
*/
|
||||||
|
h2{
|
||||||
|
font-size:16px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 3
|
||||||
|
*/
|
||||||
|
h3{
|
||||||
|
font-size:14px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ======== Header Styles ======== */
|
||||||
|
|
||||||
|
#templatePreheader{display:none !important;} /* Hide the template preheader to save space */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section header image
|
||||||
|
*/
|
||||||
|
#headerImage{
|
||||||
|
height:auto !important;
|
||||||
|
max-width:600px !important;
|
||||||
|
width:20% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======== Body Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body image
|
||||||
|
*/
|
||||||
|
#bodyImage{
|
||||||
|
height:auto !important;
|
||||||
|
max-width:560px !important;
|
||||||
|
width:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body text
|
||||||
|
*/
|
||||||
|
.bodyContent{
|
||||||
|
font-size:16px !important;
|
||||||
|
line-height:125% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link button class
|
||||||
|
*/
|
||||||
|
.bodyContent a.button {
|
||||||
|
font-size:14px !important;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======== Footer Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer text
|
||||||
|
*/
|
||||||
|
.footerContent{
|
||||||
|
font-size:14px !important;
|
||||||
|
line-height:115% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerContent a{display:block !important;} /* Place footer social and utility links on their own lines, for easier access */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
|
||||||
|
<center>
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" id="bodyCell">
|
||||||
|
<!-- BEGIN TEMPLATE // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" id="templateContainer">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<!-- BEGIN HEADER // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateHeader">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="headerContent" background="{{ static("emails/top-bg-hero.png") }}" style="background-position: center center">
|
||||||
|
<a href="{{ resolve_front_url("home") }}" title="Taiga">
|
||||||
|
<img src="{{ static("emails/logo.png") }}" alt="Taiga logo" id="headerImage" />
|
||||||
|
</a>
|
||||||
|
{% trans %}
|
||||||
|
<h1>You have been Taigatized!</h1>
|
||||||
|
<p>Welcome to Taiga, an Open Source, Agile Project Management Tool</p>
|
||||||
|
{% endtrans %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- // END HEADER -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateBody">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="bodyContent">
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% block social %}
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="social-links">
|
||||||
|
<a href="{{ sr("social.twitter_url") }}" title="{{ _("Follow us on Twitter") }}" style="color: #9dce0a">{{ _("Twitter") }}</a>
|
||||||
|
<a href="{{ sr("social.github_url") }}" title="{{ _("Get the code on GitHub") }}" style="color: #9dce0a">{{ _("GitHub") }}</a>
|
||||||
|
<a href="{{ sr("taigaio_url") }}" title="{{ _("Visit our website") }}" style="color: #9dce0a">{{ _("Taiga.io") }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endblock %}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<!-- BEGIN FOOTER // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateFooter">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="footerContent">
|
||||||
|
{% block footer %}
|
||||||
|
{% trans support_url=sr("support.url"),
|
||||||
|
support_email=sr("support.email"),
|
||||||
|
mailing_list_url=sr("support.mailing_list") %}
|
||||||
|
<strong>Taiga Support:</strong>
|
||||||
|
<a href="{{ support_url }}" title="Support page" style="color: #9dce0a">{{ support_url}}</a>
|
||||||
|
<br>
|
||||||
|
<strong>Contact us:</strong>
|
||||||
|
<a href="mailto:{{ support_email }}" title="Supporti email" style="color: #9dce0a">
|
||||||
|
{{ support_email }}
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<strong>Mailing list:</strong>
|
||||||
|
<a href="{{ mailing_list_url }}" title="Mailing list" style="color: #9dce0a">
|
||||||
|
{{ mailing_list_url }}
|
||||||
|
</a>
|
||||||
|
{% endtrans %}
|
||||||
|
{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- // END FOOTER -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- // END TEMPLATE -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,489 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>{{ _("[Taiga] Updates") }}</title>
|
||||||
|
<style type="text/css">
|
||||||
|
/* /\/\/\/\/\/\/\/\/ CLIENT-SPECIFIC STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
#outlook a{padding:0;} /* Force Outlook to provide a "view in browser" message */
|
||||||
|
.ReadMsgBody{width:100%;} .ExternalClass{width:100%;} /* Force Hotmail to display emails at full width */
|
||||||
|
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;} /* Force Hotmail to display normal line spacing */
|
||||||
|
body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;} /* Prevent WebKit and Windows mobile changing default text sizes */
|
||||||
|
table, td{mso-table-lspace:0pt; mso-table-rspace:0pt;} /* Remove spacing between tables in Outlook 2007 and up */
|
||||||
|
img{-ms-interpolation-mode:bicubic;} /* Allow smoother rendering of resized image in Internet Explorer */
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ RESET STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
body{margin:0; padding:0;}
|
||||||
|
img{border:0; height:auto; line-height:100%; outline:none; text-decoration:none;}
|
||||||
|
table{border-collapse:collapse !important;}
|
||||||
|
body, #bodyTable, #bodyCell{height:100% !important; margin:0; padding:0; width:100% !important;}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ TEMPLATE STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
#bodyCell{padding:20px;}
|
||||||
|
#templateContainer{width:600px;}
|
||||||
|
|
||||||
|
/* ========== Page Styles ========== */
|
||||||
|
|
||||||
|
body, #bodyTable{
|
||||||
|
background-color:#f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section email border
|
||||||
|
*/
|
||||||
|
#templateContainer{
|
||||||
|
background-color:#FFF;
|
||||||
|
border:1px solid #CDCDCD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 1
|
||||||
|
*/
|
||||||
|
h1{
|
||||||
|
color: #6e6e6e !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial;
|
||||||
|
font-size:25px;
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @style heading 2
|
||||||
|
*/
|
||||||
|
h2{
|
||||||
|
color: #6e6e6e !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial;
|
||||||
|
font-size:20px;
|
||||||
|
font-style:normal;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Page
|
||||||
|
* @section heading 3
|
||||||
|
*/
|
||||||
|
h3{
|
||||||
|
color:#6e6e6e !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:16px;
|
||||||
|
font-weight:normal;
|
||||||
|
line-height:100%;
|
||||||
|
font-weight:bold;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Page
|
||||||
|
* @section heading 4
|
||||||
|
*/
|
||||||
|
h4{
|
||||||
|
color:#808080 !important;
|
||||||
|
display:block;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:14px;
|
||||||
|
font-weight:bold;
|
||||||
|
line-height:100%;
|
||||||
|
letter-spacing:normal;
|
||||||
|
margin-top:0;
|
||||||
|
margin-right:0;
|
||||||
|
margin-bottom:16px;
|
||||||
|
margin-left:0;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ========== Header Styles ========== */
|
||||||
|
|
||||||
|
.headerContent {
|
||||||
|
text-align: center;
|
||||||
|
color:#b8b8b8 !important;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:14px;
|
||||||
|
margin-bottom:16px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerImage{
|
||||||
|
height:auto;
|
||||||
|
width:80px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Body Styles ========== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Body
|
||||||
|
* @section body style
|
||||||
|
* @tip Set the background color and borders for your email's body area.
|
||||||
|
*/
|
||||||
|
#templateBody{
|
||||||
|
background-color:#FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body text
|
||||||
|
*/
|
||||||
|
.bodyContent{
|
||||||
|
color:#505050;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:16px;
|
||||||
|
line-height:150%;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
padding-top:20px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link
|
||||||
|
*/
|
||||||
|
.bodyContent a:link, .bodyContent a:visited, /* Yahoo! Mail Override */ .bodyContent a .yshortcuts /* Yahoo! Mail Override */{
|
||||||
|
color:#699b05;
|
||||||
|
font-weight:normal;
|
||||||
|
text-decoration:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link button class
|
||||||
|
*/
|
||||||
|
a.button {
|
||||||
|
background: #699b05;
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: .8rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button:hover {
|
||||||
|
background: #aad400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyContent img{
|
||||||
|
display:inline;
|
||||||
|
height:auto;
|
||||||
|
max-width:560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row h1,
|
||||||
|
.update-row h2,
|
||||||
|
.update-row h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row tr {
|
||||||
|
border-bottom: 1px solid #cdcdcd;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row tr:first-child,
|
||||||
|
.update-row tr:last-child {
|
||||||
|
border-bottom: 3px solid #cdcdcd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row td {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row td.update-row-name {
|
||||||
|
width: 40%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-row td.update-row-from {
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:13px;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:link, .social-links a:visited{
|
||||||
|
color:#699b05;
|
||||||
|
font-weight:normal;
|
||||||
|
text-decoration:underline;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Footer Styles ========== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer style
|
||||||
|
*/
|
||||||
|
#templateFooter{
|
||||||
|
background-color:#555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer text
|
||||||
|
*/
|
||||||
|
.footerContent{
|
||||||
|
color:#f5f5f5;
|
||||||
|
font-family: 'Open Sans', Arial, Helvetica;
|
||||||
|
font-size:10px;
|
||||||
|
line-height:150%;
|
||||||
|
padding-top:20px;
|
||||||
|
padding-right:20px;
|
||||||
|
padding-bottom:20px;
|
||||||
|
padding-left:20px;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Footer
|
||||||
|
* @section footer link
|
||||||
|
* @tip Set the styling for your email's footer links. Choose a color that helps them stand out from your text.
|
||||||
|
*/
|
||||||
|
.footerContent a:link, .footerContent a:visited, /* Yahoo! Mail Override */ .footerContent a .yshortcuts, .footerContent a span /* Yahoo! Mail Override */{
|
||||||
|
/*@editable*/ color:#699b05;
|
||||||
|
/*@editable*/ font-weight:normal;
|
||||||
|
/*@editable*/ text-decoration:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/\/\/ MOBILE STYLES /\/\/\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px){
|
||||||
|
/* /\/\/\/\/\/\/ CLIENT-SPECIFIC MOBILE STYLES /\/\/\/\/\/\/ */
|
||||||
|
body, table, td, p, a, li, blockquote{-webkit-text-size-adjust:none !important;} /* Prevent Webkit platforms from changing default text sizes */
|
||||||
|
body{width:100% !important; min-width:100% !important;} /* Prevent iOS Mail from adding padding to the body */
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/ MOBILE RESET STYLES /\/\/\/\/\/\/ */
|
||||||
|
#bodyCell{padding:10px !important;}
|
||||||
|
|
||||||
|
/* /\/\/\/\/\/\/ MOBILE TEMPLATE STYLES /\/\/\/\/\/\/ */
|
||||||
|
|
||||||
|
/* ======== Page Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section template width
|
||||||
|
*/
|
||||||
|
#templateContainer{
|
||||||
|
max-width:600px !important;
|
||||||
|
width:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 1
|
||||||
|
*/
|
||||||
|
h1{
|
||||||
|
font-size:18px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 2
|
||||||
|
*/
|
||||||
|
h2{
|
||||||
|
font-size:16px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section heading 3
|
||||||
|
*/
|
||||||
|
h3{
|
||||||
|
font-size:14px !important;
|
||||||
|
line-height:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ======== Header Styles ======== */
|
||||||
|
|
||||||
|
#templatePreheader{display:none !important;} /* Hide the template preheader to save space */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section header image
|
||||||
|
*/
|
||||||
|
#headerImage{
|
||||||
|
height:auto !important;
|
||||||
|
max-width:600px !important;
|
||||||
|
width:20% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======== Body Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tab Mobile Styles
|
||||||
|
* @section body image
|
||||||
|
* @tip Make the main body image fluid for portrait or landscape view adaptability, and set the image's original width as the max-width. If a fluid setting doesn't work, set the image width to half its original size instead.
|
||||||
|
*/
|
||||||
|
#bodyImage{
|
||||||
|
height:auto !important;
|
||||||
|
max-width:560px !important;
|
||||||
|
width:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body text
|
||||||
|
*/
|
||||||
|
.bodyContent{
|
||||||
|
font-size:16px !important;
|
||||||
|
line-height:125% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section body link button class
|
||||||
|
*/
|
||||||
|
.bodyContent a.button {
|
||||||
|
font-size:14px !important;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======== Footer Styles ======== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @section footer text
|
||||||
|
*/
|
||||||
|
.footerContent{
|
||||||
|
font-size:14px !important;
|
||||||
|
line-height:115% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerContent a{display:block !important;} /* Place footer social and utility links on their own lines, for easier access */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
|
||||||
|
<center>
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" id="bodyCell">
|
||||||
|
<!-- BEGIN TEMPLATE // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" id="templateContainer">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<!-- BEGIN HEADER // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateHeader">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="headerContent">
|
||||||
|
<img src="{{ static("emails/top-bg-update.png") }}" />
|
||||||
|
<a href="{{ resolve_front_url("home") }}"
|
||||||
|
title="Taiga">
|
||||||
|
<img src="{{ static("emails/logo-color.png") }}" id="headerImage" alt="Taiga logo" />
|
||||||
|
</a>
|
||||||
|
{% block head %}
|
||||||
|
{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- // END HEADER -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<!-- BEGIN BODY // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateBody">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="bodyContent">
|
||||||
|
<table class="update-row" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
{% block body %}
|
||||||
|
<tr>
|
||||||
|
<th colspan="2"><h2>{{ _("Updates") }}</h2></th>
|
||||||
|
</tr>
|
||||||
|
{% for entry in history_entries%}
|
||||||
|
{% if entry.comment %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
{% trans comment=mdrender(project, entry.comment) %}
|
||||||
|
<h3>comment:</h3>
|
||||||
|
<p>{{ comment }}</p>
|
||||||
|
{% endtrans %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% set changed_fields = entry.values_diff %}
|
||||||
|
{% if changed_fields %}
|
||||||
|
{% include "emails/includes/fields_diff-html.jinja" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% block social %}
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="social-links">
|
||||||
|
<a href="{{ sr("social.twitter_url") }}" title="{{ _("Follow us on Twitter") }}" style="color: #9dce0a">{{ _("Twitter") }}</a>
|
||||||
|
<a href="{{ sr("social.github_url") }}" title="{{ _("Get the code on GitHub") }}" style="color: #9dce0a">{{ _("GitHub") }}</a>
|
||||||
|
<a href="{{ sr("taigaio_url") }}" title="{{ _("Visit our website") }}" style="color: #9dce0a">{{ _("Taiga.io") }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endblock %}
|
||||||
|
</table>
|
||||||
|
<!-- // END BODY -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<!-- BEGIN FOOTER // -->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateFooter">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" class="footerContent">
|
||||||
|
{% block footer %}
|
||||||
|
{% trans support_url=sr("support.url"),
|
||||||
|
support_email=sr("support.email"),
|
||||||
|
mailing_list_url=sr("support.mailing_list") %}
|
||||||
|
<strong>Taiga Support:</strong>
|
||||||
|
<a href="{{ support_url }}" title="Support page" style="color: #9dce0a">{{ support_url}}</a>
|
||||||
|
<br>
|
||||||
|
<strong>Contact us:</strong>
|
||||||
|
<a href="mailto:{{ support_email }}" title="Supporti email" style="color: #9dce0a">
|
||||||
|
{{ support_email }}
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<strong>Mailing list:</strong>
|
||||||
|
<a href="{{ mailing_list_url }}" title="Mailing list" style="color: #9dce0a">
|
||||||
|
{{ mailing_list_url }}
|
||||||
|
</a>
|
||||||
|
{% endtrans %}
|
||||||
|
{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- // END FOOTER -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- // END TEMPLATE -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% for entry in history_entries %}
|
||||||
|
{% if entry.comment %}
|
||||||
|
{% trans comment=entry.comment %}
|
||||||
|
Comment: {{ comment }}
|
||||||
|
{% endtrans %}
|
||||||
|
{% endif %}
|
||||||
|
{% set changed_fields = entry.values_diff %}
|
||||||
|
{% if changed_fields %}
|
||||||
|
{% include "emails/includes/fields_diff-text.jinja" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
|
@ -31,6 +31,13 @@ def get_typename_for_model_class(model:object, for_concrete_model=True) -> str:
|
||||||
|
|
||||||
return "{0}.{1}".format(model._meta.app_label, model._meta.model_name)
|
return "{0}.{1}".format(model._meta.app_label, model._meta.model_name)
|
||||||
|
|
||||||
|
def get_typename_for_model_instance(model_instance):
|
||||||
|
"""
|
||||||
|
Get content type tuple from model instance.
|
||||||
|
"""
|
||||||
|
ct = ContentType.objects.get_for_model(model_instance)
|
||||||
|
return ".".join([ct.app_label, ct.model])
|
||||||
|
|
||||||
|
|
||||||
def reload_attribute(model_instance, attr_name):
|
def reload_attribute(model_instance, attr_name):
|
||||||
"""Fetch the stored value of a model instance attribute.
|
"""Fetch the stored value of a model instance attribute.
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base import routers
|
||||||
|
|
||||||
|
router = routers.DefaultRouter(trailing_slash=False)
|
|
@ -18,6 +18,7 @@ import collections
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
|
from taiga.base.utils.db import get_typename_for_model_instance
|
||||||
from . import middleware as mw
|
from . import middleware as mw
|
||||||
from . import backends
|
from . import backends
|
||||||
|
|
||||||
|
@ -32,14 +33,6 @@ watched_types = set([
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def _get_type_for_model(model_instance):
|
|
||||||
"""
|
|
||||||
Get content type tuple from model instance.
|
|
||||||
"""
|
|
||||||
ct = ContentType.objects.get_for_model(model_instance)
|
|
||||||
return ".".join([ct.app_label, ct.model])
|
|
||||||
|
|
||||||
|
|
||||||
def emit_event(data:dict, routing_key:str, *,
|
def emit_event(data:dict, routing_key:str, *,
|
||||||
sessionid:str=None, channel:str="events"):
|
sessionid:str=None, channel:str="events"):
|
||||||
if not sessionid:
|
if not sessionid:
|
||||||
|
@ -64,7 +57,7 @@ def emit_event_for_model(obj, *, type:str="change", channel:str="events",
|
||||||
assert hasattr(obj, "project_id")
|
assert hasattr(obj, "project_id")
|
||||||
|
|
||||||
if not content_type:
|
if not content_type:
|
||||||
content_type = _get_type_for_model(obj)
|
content_type = get_typename_for_model_instance(obj)
|
||||||
|
|
||||||
projectid = getattr(obj, "project_id")
|
projectid = getattr(obj, "project_id")
|
||||||
pk = getattr(obj, "pk", None)
|
pk = getattr(obj, "pk", None)
|
||||||
|
|
|
@ -17,13 +17,15 @@
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from taiga.base.utils.db import get_typename_for_model_instance
|
||||||
|
|
||||||
from . import middleware as mw
|
from . import middleware as mw
|
||||||
from . import events
|
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
|
||||||
content_type = events._get_type_for_model(instance)
|
content_type = get_typename_for_model_instance(instance)
|
||||||
|
|
||||||
# Ignore any other events
|
# Ignore any other events
|
||||||
if content_type not in events.watched_types:
|
if content_type not in events.watched_types:
|
||||||
|
@ -39,7 +41,7 @@ def on_save_any_model(sender, instance, created, **kwargs):
|
||||||
|
|
||||||
def on_delete_any_model(sender, instance, **kwargs):
|
def on_delete_any_model(sender, instance, **kwargs):
|
||||||
# Ignore any object that can not have project_id
|
# Ignore any object that can not have project_id
|
||||||
content_type = events._get_type_for_model(instance)
|
content_type = get_typename_for_model_instance(instance)
|
||||||
|
|
||||||
# Ignore any other changes
|
# Ignore any other changes
|
||||||
if content_type not in events.watched_types:
|
if content_type not in events.watched_types:
|
||||||
|
|
|
@ -14,33 +14,74 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework.exceptions import APIException
|
import json
|
||||||
|
import codecs
|
||||||
|
import uuid
|
||||||
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.decorators import throttle_classes
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
from taiga.base.api.mixins import CreateModelMixin
|
from taiga.base.api.mixins import CreateModelMixin
|
||||||
from taiga.base.api.viewsets import GenericViewSet
|
from taiga.base.api.viewsets import GenericViewSet
|
||||||
from taiga.base.decorators import detail_route
|
from taiga.base.decorators import detail_route, list_route
|
||||||
|
from taiga.base import exceptions as exc
|
||||||
from taiga.projects.models import Project, Membership
|
from taiga.projects.models import Project, Membership
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
|
from taiga.projects.serializers import ProjectSerializer
|
||||||
|
|
||||||
from . import mixins
|
from . import mixins
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import service
|
from . import service
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
from . import tasks
|
||||||
|
from . import dump_service
|
||||||
|
from . import throttling
|
||||||
|
from .renderers import ExportRenderer
|
||||||
|
|
||||||
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
||||||
|
|
||||||
class Http400(APIException):
|
class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet):
|
||||||
status_code = 400
|
model = Project
|
||||||
|
permission_classes = (permissions.ImportExportPermission, )
|
||||||
|
|
||||||
|
def retrieve(self, request, pk, *args, **kwargs):
|
||||||
|
throttle = throttling.ImportDumpModeRateThrottle()
|
||||||
|
|
||||||
|
if not throttle.allow_request(request, self):
|
||||||
|
self.throttled(request, throttle.wait())
|
||||||
|
|
||||||
|
project = get_object_or_404(self.get_queryset(), pk=pk)
|
||||||
|
self.check_permissions(request, 'export_project', project)
|
||||||
|
|
||||||
|
if settings.CELERY_ENABLED:
|
||||||
|
task = tasks.dump_project.delay(request.user, project)
|
||||||
|
tasks.delete_project_dump.apply_async((project.pk, project.slug), countdown=settings.EXPORTS_TTL)
|
||||||
|
return Response({"export_id": task.id}, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
|
path = "exports/{}/{}-{}.json".format(project.pk, project.slug, uuid.uuid4().hex)
|
||||||
|
content = ContentFile(ExportRenderer().render(service.project_to_dict(project),
|
||||||
|
renderer_context={"indent": 4}).decode('utf-8'))
|
||||||
|
|
||||||
|
default_storage.save(path, content)
|
||||||
|
response_data = {
|
||||||
|
"url": default_storage.url(path)
|
||||||
|
}
|
||||||
|
return Response(response_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixin, GenericViewSet):
|
class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixin, GenericViewSet):
|
||||||
model = Project
|
model = Project
|
||||||
permission_classes = (permissions.ImportPermission, )
|
permission_classes = (permissions.ImportExportPermission, )
|
||||||
|
|
||||||
@method_decorator(atomic)
|
@method_decorator(atomic)
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
|
@ -52,7 +93,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
project_serialized = service.store_project(data)
|
project_serialized = service.store_project(data)
|
||||||
|
|
||||||
if project_serialized is None:
|
if project_serialized is None:
|
||||||
raise Http400(service.get_errors())
|
raise exc.BadRequest(service.get_errors())
|
||||||
|
|
||||||
if "points" in data:
|
if "points" in data:
|
||||||
service.store_choices(project_serialized.object, data,
|
service.store_choices(project_serialized.object, data,
|
||||||
|
@ -106,13 +147,47 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
errors = service.get_errors()
|
errors = service.get_errors()
|
||||||
if errors:
|
if errors:
|
||||||
raise Http400(errors)
|
raise exc.BadRequest(errors)
|
||||||
|
|
||||||
response_data = project_serialized.data
|
response_data = project_serialized.data
|
||||||
response_data['id'] = project_serialized.object.id
|
response_data['id'] = project_serialized.object.id
|
||||||
headers = self.get_success_headers(response_data)
|
headers = self.get_success_headers(response_data)
|
||||||
return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
|
@list_route(methods=["POST"])
|
||||||
|
@method_decorator(atomic)
|
||||||
|
def load_dump(self, request):
|
||||||
|
throttle = throttling.ImportDumpModeRateThrottle()
|
||||||
|
|
||||||
|
if not throttle.allow_request(request, self):
|
||||||
|
self.throttled(request, throttle.wait())
|
||||||
|
|
||||||
|
self.check_permissions(request, "load_dump", None)
|
||||||
|
|
||||||
|
dump = request.FILES.get('dump', None)
|
||||||
|
|
||||||
|
if not dump:
|
||||||
|
raise exc.WrongArguments(_("Needed dump file"))
|
||||||
|
|
||||||
|
reader = codecs.getreader("utf-8")
|
||||||
|
|
||||||
|
try:
|
||||||
|
dump = json.load(reader(dump))
|
||||||
|
except Exception:
|
||||||
|
raise exc.WrongArguments(_("Invalid dump format"))
|
||||||
|
|
||||||
|
if Project.objects.filter(slug=dump['slug']).exists():
|
||||||
|
del dump['slug']
|
||||||
|
|
||||||
|
if settings.CELERY_ENABLED:
|
||||||
|
task = tasks.load_project_dump.delay(request.user, dump)
|
||||||
|
return Response({"import_id": task.id}, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
|
project = dump_service.dict_to_project(dump, request.user.email)
|
||||||
|
response_data = ProjectSerializer(project).data
|
||||||
|
return Response(response_data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=['post'])
|
||||||
@method_decorator(atomic)
|
@method_decorator(atomic)
|
||||||
def issue(self, request, *args, **kwargs):
|
def issue(self, request, *args, **kwargs):
|
||||||
|
@ -126,7 +201,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
errors = service.get_errors()
|
errors = service.get_errors()
|
||||||
if errors:
|
if errors:
|
||||||
raise Http400(errors)
|
raise exc.BadRequest(errors)
|
||||||
|
|
||||||
headers = self.get_success_headers(issue.data)
|
headers = self.get_success_headers(issue.data)
|
||||||
return Response(issue.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(issue.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
@ -141,7 +216,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
errors = service.get_errors()
|
errors = service.get_errors()
|
||||||
if errors:
|
if errors:
|
||||||
raise Http400(errors)
|
raise exc.BadRequest(errors)
|
||||||
|
|
||||||
headers = self.get_success_headers(task.data)
|
headers = self.get_success_headers(task.data)
|
||||||
return Response(task.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(task.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
@ -156,7 +231,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
errors = service.get_errors()
|
errors = service.get_errors()
|
||||||
if errors:
|
if errors:
|
||||||
raise Http400(errors)
|
raise exc.BadRequest(errors)
|
||||||
|
|
||||||
headers = self.get_success_headers(us.data)
|
headers = self.get_success_headers(us.data)
|
||||||
return Response(us.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(us.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
@ -171,7 +246,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
errors = service.get_errors()
|
errors = service.get_errors()
|
||||||
if errors:
|
if errors:
|
||||||
raise Http400(errors)
|
raise exc.BadRequest(errors)
|
||||||
|
|
||||||
headers = self.get_success_headers(milestone.data)
|
headers = self.get_success_headers(milestone.data)
|
||||||
return Response(milestone.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(milestone.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
@ -186,7 +261,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
errors = service.get_errors()
|
errors = service.get_errors()
|
||||||
if errors:
|
if errors:
|
||||||
raise Http400(errors)
|
raise exc.BadRequest(errors)
|
||||||
|
|
||||||
headers = self.get_success_headers(wiki_page.data)
|
headers = self.get_success_headers(wiki_page.data)
|
||||||
return Response(wiki_page.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(wiki_page.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
@ -201,7 +276,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
errors = service.get_errors()
|
errors = service.get_errors()
|
||||||
if errors:
|
if errors:
|
||||||
raise Http400(errors)
|
raise exc.BadRequest(errors)
|
||||||
|
|
||||||
headers = self.get_success_headers(wiki_link.data)
|
headers = self.get_success_headers(wiki_link.data)
|
||||||
return Response(wiki_link.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(wiki_link.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
|
@ -72,6 +72,12 @@ def store_issues(project, data):
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def store_tags_colors(project, data):
|
||||||
|
project.tags_colors = data.get("tags_colors", [])
|
||||||
|
project.save()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def dict_to_project(data, owner=None):
|
def dict_to_project(data, owner=None):
|
||||||
if owner:
|
if owner:
|
||||||
data['owner'] = owner
|
data['owner'] = owner
|
||||||
|
@ -148,3 +154,7 @@ def dict_to_project(data, owner=None):
|
||||||
|
|
||||||
if service.get_errors(clear=False):
|
if service.get_errors(clear=False):
|
||||||
raise TaigaImportError('error importing issues')
|
raise TaigaImportError('error importing issues')
|
||||||
|
|
||||||
|
store_tags_colors(proj, data)
|
||||||
|
|
||||||
|
return proj
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
|
|
|
@ -19,6 +19,8 @@ from taiga.base.api.permissions import (TaigaResourcePermission,
|
||||||
IsProjectOwner, IsAuthenticated)
|
IsProjectOwner, IsAuthenticated)
|
||||||
|
|
||||||
|
|
||||||
class ImportPermission(TaigaResourcePermission):
|
class ImportExportPermission(TaigaResourcePermission):
|
||||||
import_project_perms = IsAuthenticated()
|
import_project_perms = IsAuthenticated()
|
||||||
import_item_perms = IsProjectOwner()
|
import_item_perms = IsProjectOwner()
|
||||||
|
export_project_perms = IsProjectOwner()
|
||||||
|
load_dump_perms = IsAuthenticated()
|
||||||
|
|
|
@ -46,8 +46,10 @@ class AttachedFileField(serializers.WritableField):
|
||||||
if not obj:
|
if not obj:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
data = base64.b64encode(obj.read()).decode('utf-8')
|
||||||
|
|
||||||
return OrderedDict([
|
return OrderedDict([
|
||||||
("data", base64.b64encode(obj.read()).decode('utf-8')),
|
("data", data),
|
||||||
("name", os.path.basename(obj.name)),
|
("name", os.path.basename(obj.name)),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -120,7 +122,7 @@ class ProjectRelatedField(serializers.RelatedField):
|
||||||
|
|
||||||
class HistoryUserField(JsonField):
|
class HistoryUserField(JsonField):
|
||||||
def to_native(self, obj):
|
def to_native(self, obj):
|
||||||
if obj is None:
|
if obj is None or obj == {}:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
user = users_models.User.objects.get(pk=obj['pk'])
|
user = users_models.User.objects.get(pk=obj['pk'])
|
||||||
|
@ -190,7 +192,7 @@ class HistoryExportSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = history_models.HistoryEntry
|
model = history_models.HistoryEntry
|
||||||
exclude = ("id", "comment_html")
|
exclude = ("id", "comment_html", "key")
|
||||||
|
|
||||||
|
|
||||||
class HistoryExportSerializerMixin(serializers.ModelSerializer):
|
class HistoryExportSerializerMixin(serializers.ModelSerializer):
|
||||||
|
|
|
@ -44,6 +44,7 @@ def add_errors(section, errors):
|
||||||
else:
|
else:
|
||||||
_errors_log[section] = [errors]
|
_errors_log[section] = [errors]
|
||||||
|
|
||||||
|
|
||||||
def project_to_dict(project):
|
def project_to_dict(project):
|
||||||
return serializers.ProjectExportSerializer(project).data
|
return serializers.ProjectExportSerializer(project).data
|
||||||
|
|
||||||
|
@ -84,7 +85,7 @@ def store_choice(project, data, field, serializer):
|
||||||
|
|
||||||
def store_choices(project, data, field, serializer):
|
def store_choices(project, data, field, serializer):
|
||||||
result = []
|
result = []
|
||||||
for choice_data in data[field]:
|
for choice_data in data.get(field, []):
|
||||||
result.append(store_choice(project, choice_data, field, serializer))
|
result.append(store_choice(project, choice_data, field, serializer))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -102,7 +103,7 @@ def store_role(project, role):
|
||||||
|
|
||||||
def store_roles(project, data):
|
def store_roles(project, data):
|
||||||
results = []
|
results = []
|
||||||
for role in data['roles']:
|
for role in data.get('roles', []):
|
||||||
results.append(store_role(project, role))
|
results.append(store_role(project, role))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
||||||
|
|
||||||
|
from taiga.celery import app
|
||||||
|
|
||||||
|
from .service import project_to_dict
|
||||||
|
from .dump_service import dict_to_project
|
||||||
|
from .renderers import ExportRenderer
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(bind=True)
|
||||||
|
def dump_project(self, user, project):
|
||||||
|
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
||||||
|
path = "exports/{}/{}-{}.json".format(project.pk, project.slug, self.request.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = ExportRenderer().render(project_to_dict(project), renderer_context={"indent": 4})
|
||||||
|
content = content.decode('utf-8')
|
||||||
|
content = ContentFile(content)
|
||||||
|
|
||||||
|
default_storage.save(path, content)
|
||||||
|
url = default_storage.url(path)
|
||||||
|
except Exception:
|
||||||
|
ctx = {
|
||||||
|
"user": user,
|
||||||
|
"error_subject": "Error generating project dump",
|
||||||
|
"error_message": "Error generating project dump",
|
||||||
|
"project": project
|
||||||
|
}
|
||||||
|
email = mbuilder.export_error(user.email, ctx)
|
||||||
|
email.send()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
deletion_date = timezone.now() + datetime.timedelta(seconds=settings.EXPORTS_TTL)
|
||||||
|
ctx = {
|
||||||
|
"url": url,
|
||||||
|
"project": project,
|
||||||
|
"user": user,
|
||||||
|
"deletion_date": deletion_date
|
||||||
|
}
|
||||||
|
email = mbuilder.dump_project(user.email, ctx)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def delete_project_dump(project_id, project_slug, task_id):
|
||||||
|
default_storage.delete("exports/{}/{}-{}.json".format(project_id, project_slug, task_id))
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def load_project_dump(user, dump):
|
||||||
|
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
||||||
|
|
||||||
|
try:
|
||||||
|
project = dict_to_project(dump, user.email)
|
||||||
|
except Exception:
|
||||||
|
ctx = {
|
||||||
|
"user": user,
|
||||||
|
"error_subject": "Error loading project dump",
|
||||||
|
"error_message": "Error loading project dump",
|
||||||
|
}
|
||||||
|
email = mbuilder.import_error(user.email, ctx)
|
||||||
|
email.send()
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx = {"user": user, "project": project}
|
||||||
|
email = mbuilder.load_dump(user.email, ctx)
|
||||||
|
email.send()
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
project=project.name|safe,
|
||||||
|
url=url,
|
||||||
|
deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
|
||||||
|
<h1>Project dump generated</h1>
|
||||||
|
<p>Hello {{ user }},</p>
|
||||||
|
<h3>Your dump from project {{ project }} has been correctly generated.</h3>
|
||||||
|
<p>You can download it here:</p>
|
||||||
|
<a class="button" href="{{ url }}" title="Download the dump file">Download the dump file</a>
|
||||||
|
<p>This file will be deleted on {{ deletion_date }}.</p>
|
||||||
|
<p><small>The Taiga Team</small></p>
|
||||||
|
{% endtrans %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
project=project.name|safe,
|
||||||
|
url=url,
|
||||||
|
deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
|
||||||
|
Hello {{ user }},
|
||||||
|
|
||||||
|
Your dump from project {{ project }} has been correctly generated. You can download it here:
|
||||||
|
|
||||||
|
{{ url }}
|
||||||
|
|
||||||
|
This file will be deleted on {{ deletion_date }}.
|
||||||
|
|
||||||
|
---
|
||||||
|
The Taiga Team
|
||||||
|
{% endtrans %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% trans project=project.name|safe %}[{{ project }}] Your project dump has been generated{% endtrans %}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
error_message=error_message,
|
||||||
|
support_email=sr("support.email"),
|
||||||
|
project=project.name|safe %}
|
||||||
|
<h1>{{ error_message }}</h1>
|
||||||
|
<p>Hello {{ user }},</p>
|
||||||
|
<p>Your project {{ project }} has not been exported correctly.</p>
|
||||||
|
<p>The Taiga system administrators have been informed.<br/> Please, try it again or contact with the support team at
|
||||||
|
<a href="mailto:{{ support_email }}" title="Support email" style="color: #699b05">{{ support_email }}</a></p>
|
||||||
|
<p><small>The Taiga Team</small></p>
|
||||||
|
{% endtrans %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
error_message=error_message,
|
||||||
|
support_email=sr("support.email"),
|
||||||
|
project=project.name|safe %}
|
||||||
|
Hello {{ user }},
|
||||||
|
|
||||||
|
{{ error_message }}
|
||||||
|
Your project {{ project }} has not been exported correctly.
|
||||||
|
|
||||||
|
The Taiga system administrators have been informed.
|
||||||
|
|
||||||
|
Please, try it again or contact with the support team at {{ support_email }}
|
||||||
|
|
||||||
|
---
|
||||||
|
The Taiga Team
|
||||||
|
{% endtrans %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% trans error_subject=error_subject, project=project.name|safe %}[{{ project }}] {{ error_subject }}{% endtrans %}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
error_message=error_message,
|
||||||
|
support_email=sr("support.email") %}
|
||||||
|
<h1>{{ error_message }}</h1>
|
||||||
|
<p>Hello {{ user }},</p>
|
||||||
|
<p>Your project has not been importer correctly.</p>
|
||||||
|
<p>The Taiga system administrators have been informed.<br/> Please, try it again or contact with the support team at
|
||||||
|
<a href="mailto:{{ support_email }}" title="Support email" style="color: #699b05">{{ support_email }}</a></p>
|
||||||
|
<p><small>The Taiga Team</small></p>
|
||||||
|
{% endtrans %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
error_message=error_message,
|
||||||
|
support_email=sr("support.email") %}
|
||||||
|
Hello {{ user }},
|
||||||
|
|
||||||
|
{{ error_message }}
|
||||||
|
|
||||||
|
Your project has not been importer correctly.
|
||||||
|
|
||||||
|
The Taiga system administrators have been informed.
|
||||||
|
|
||||||
|
Please, try it again or contact with the support team at {{ support_email }}
|
||||||
|
|
||||||
|
---
|
||||||
|
The Taiga Team
|
||||||
|
{% endtrans %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% trans error_subject=error_subject %}[Taiga] {{ error_subject }}{% endtrans %}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
url=resolve_front_url("project", project.slug),
|
||||||
|
project=project.name|safe %}
|
||||||
|
<h1>Project dump imported</h1>
|
||||||
|
<p>Hello {{ user }},</p>
|
||||||
|
<h3>Your project dump has been correctly imported.</h3>
|
||||||
|
<a class="button" href="{{ url }}" title="Go to the project {{ project }}">Go to {{ project }}</a>
|
||||||
|
<p><small>The Taiga Team</small></p>
|
||||||
|
{% endtrans %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
url=resolve_front_url("project", project.slug),
|
||||||
|
project=project.name|safe %}
|
||||||
|
Hello {{ user }},
|
||||||
|
|
||||||
|
Your project dump has been correctly imported.
|
||||||
|
|
||||||
|
You can see the project {{ project }} here:
|
||||||
|
|
||||||
|
{{ url }}
|
||||||
|
|
||||||
|
---
|
||||||
|
The Taiga Team
|
||||||
|
{% endtrans %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% trans project=project.name|safe %}[{{ project }}] Your project dump has been imported{% endtrans %}
|
|
@ -19,3 +19,6 @@ from taiga.base import throttling
|
||||||
|
|
||||||
class ImportModeRateThrottle(throttling.UserRateThrottle):
|
class ImportModeRateThrottle(throttling.UserRateThrottle):
|
||||||
scope = "import-mode"
|
scope = "import-mode"
|
||||||
|
|
||||||
|
class ImportDumpModeRateThrottle(throttling.UserRateThrottle):
|
||||||
|
scope = "import-dump-mode"
|
||||||
|
|
|
@ -46,6 +46,6 @@ class FeedbackViewSet(viewsets.ViewSet):
|
||||||
"HTTP_REFERER": request.META.get("HTTP_REFERER", None),
|
"HTTP_REFERER": request.META.get("HTTP_REFERER", None),
|
||||||
"HTTP_USER_AGENT": request.META.get("HTTP_USER_AGENT", None),
|
"HTTP_USER_AGENT": request.META.get("HTTP_USER_AGENT", None),
|
||||||
}
|
}
|
||||||
services.send_feedback(self.object, extra)
|
services.send_feedback(self.object, extra, reply_to=[request.user.email])
|
||||||
|
|
||||||
return response.Ok(serializer.data)
|
return response.Ok(serializer.data)
|
||||||
|
|
|
@ -16,14 +16,21 @@
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from djmail.template_mail import MagicMailBuilder
|
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
|
||||||
|
|
||||||
|
|
||||||
def send_feedback(feedback_entry, extra):
|
def send_feedback(feedback_entry, extra, reply_to=[]):
|
||||||
support_email = settings.FEEDBACK_EMAIL
|
support_email = settings.FEEDBACK_EMAIL
|
||||||
|
|
||||||
if support_email:
|
if support_email:
|
||||||
mbuilder = MagicMailBuilder()
|
reply_to.append(support_email)
|
||||||
email = mbuilder.feedback_notification(support_email, {"feedback_entry": feedback_entry,
|
|
||||||
"extra": extra})
|
ctx = {
|
||||||
|
"feedback_entry": feedback_entry,
|
||||||
|
"extra": extra
|
||||||
|
}
|
||||||
|
|
||||||
|
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
|
||||||
|
email = mbuilder.feedback_notification(support_email, ctx)
|
||||||
|
email.extra_headers["Reply-To"] = ", ".join(reply_to)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
|
@ -1,37 +1,29 @@
|
||||||
{% extends "emails/base.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<table border="0" width="100%" cellpadding="4" cellspacing="10" class="table-body" style="border-collapse: collapse;">
|
{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email %}
|
||||||
<tr>
|
<h1>Feedback</h1>
|
||||||
<td valign="top" style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
<p>Taiga has received feedback from {{ full_name }} <{{ email }}></p>
|
||||||
<strong>From:</strong>
|
{% endtrans %}
|
||||||
</td>
|
|
||||||
<td style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
{% trans comment=feedback_entry.comment|linebreaksbr %}
|
||||||
{{ feedback_entry.full_name }} [{{ feedback_entry.email }}]
|
<h3>Comment</h3>
|
||||||
</td>
|
<p>{{ comment }}</p>
|
||||||
</tr>
|
{% endtrans %}
|
||||||
<tr>
|
|
||||||
<td valign="top" style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
|
||||||
<strong>Comment:</strong>
|
|
||||||
</td>
|
|
||||||
<td style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
|
||||||
{{ feedback_entry.comment|linebreaks }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if extra %}
|
{% if extra %}
|
||||||
<tr>
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="templateBody">
|
||||||
<td valign="top" style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
<tr>
|
||||||
<strong>Extra:</strong>
|
<td valign="top" valign="top" class="bodyContent">
|
||||||
</td>
|
<h3>{{ _("Extra info") }}</h3>
|
||||||
<td style="border-top: 1px solid gray; border-bottom: 1px solid gray;">
|
<dl>
|
||||||
<dl>
|
{% for k, v in extra.items() %}
|
||||||
{% for k, v in extra.items() %}
|
<dt>{{ k }}</dt>
|
||||||
<dt>{{ k }}</dt>
|
<dd>{{ v }}</dd>
|
||||||
<dd>{{ v }}</dd>
|
{% endfor %}
|
||||||
{% endfor %}
|
</dl>
|
||||||
</dl>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr
|
</table>
|
||||||
{% endif %}>
|
{% endif %}
|
||||||
</table>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
---------
|
{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email, comment=feedback_entry.comment %}---------
|
||||||
- From: {{ feedback_entry.full_name }} [{{ feedback_entry.email }}]
|
- From: {{ full_name }} <{{ email }}>
|
||||||
---------
|
---------
|
||||||
- Comment:
|
- Comment:
|
||||||
{{ feedback_entry.comment }}
|
{{ comment }}
|
||||||
---------{% if extra %}
|
---------{% endtrans %}
|
||||||
- Extra:
|
{% if extra %}
|
||||||
|
{{ _("- Extra info:") }}
|
||||||
{% for k, v in extra.items() %}
|
{% for k, v in extra.items() %}
|
||||||
- {{ k }}: {{ v }}
|
- {{ k }}: {{ v }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
[Taiga] Feedback from {{ feedback_entry.full_name }} <{{ feedback_entry.email }}>
|
{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email %}
|
||||||
|
[Taiga] Feedback from {{ full_name }} <{{ email }}>
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
# SPANISH LANGUAGE PACKAGE FOR TAIGA IO.
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
# Copyright (C) 2014
|
||||||
# This file is distributed under the same license as the PACKAGE package.
|
# This file is distributed under the same license as the taiga io package.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
#
|
#
|
||||||
|
#. Spanish Translators: please, consider don't use informal expressions
|
||||||
|
#. Traductores al español, por favor, es necesario considerar el uso
|
||||||
|
#. de la 3era persona en lugar del "tú"
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2013-12-09 22:06+0100\n"
|
"POT-Creation-Date: 2013-12-09 22:06+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: 2014-12-19 19:48-0430\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: Hector Colina <hcolina@gmail.com>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
"Language: \n"
|
"Language: \n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -28,7 +31,7 @@ msgstr "No encontrado."
|
||||||
|
|
||||||
#: base/exceptions.py:36 base/exceptions.py:44
|
#: base/exceptions.py:36 base/exceptions.py:44
|
||||||
msgid "Wrong arguments."
|
msgid "Wrong arguments."
|
||||||
msgstr "Argumentos erroneos."
|
msgstr "Argumentos erróneos."
|
||||||
|
|
||||||
#: base/exceptions.py:59
|
#: base/exceptions.py:59
|
||||||
msgid "Precondition error"
|
msgid "Precondition error"
|
||||||
|
@ -48,23 +51,23 @@ msgstr "Permiso denegado"
|
||||||
|
|
||||||
#: base/auth/api.py:52
|
#: base/auth/api.py:52
|
||||||
msgid "Public register is disabled for this domain."
|
msgid "Public register is disabled for this domain."
|
||||||
msgstr "El registro publico está deshabilitado para este dominio."
|
msgstr "El registro público está deshabilitado para este dominio."
|
||||||
|
|
||||||
#: base/auth/api.py:91
|
#: base/auth/api.py:91
|
||||||
msgid "Invalid token"
|
msgid "Invalid token"
|
||||||
msgstr "Token invalido"
|
msgstr "Token inválido"
|
||||||
|
|
||||||
#: base/auth/api.py:100
|
#: base/auth/api.py:100
|
||||||
msgid "Incorrect password"
|
msgid "Incorrect password"
|
||||||
msgstr "Password incorrecto"
|
msgstr "Contraseña incorrecta"
|
||||||
|
|
||||||
#: base/auth/api.py:133
|
#: base/auth/api.py:133
|
||||||
msgid "invalid register type"
|
msgid "invalid register type"
|
||||||
msgstr "tipo de registro no valido"
|
msgstr "tipo de registro no válido"
|
||||||
|
|
||||||
#: base/auth/api.py:142 base/auth/api.py:145
|
#: base/auth/api.py:142 base/auth/api.py:145
|
||||||
msgid "Invalid username or password"
|
msgid "Invalid username or password"
|
||||||
msgstr "usuario o contraseña invalidos"
|
msgstr "usuario o contraseña inválidos"
|
||||||
|
|
||||||
#: base/domains/__init__.py:54
|
#: base/domains/__init__.py:54
|
||||||
msgid "domain not found"
|
msgid "domain not found"
|
||||||
|
@ -72,7 +75,7 @@ msgstr "dominio no encontrado"
|
||||||
|
|
||||||
#: base/domains/models.py:24
|
#: base/domains/models.py:24
|
||||||
msgid "The domain name cannot contain any spaces or tabs."
|
msgid "The domain name cannot contain any spaces or tabs."
|
||||||
msgstr "El nombre de dominio no puede tener espacios o tabs."
|
msgstr "El nombre de dominio no puede tener espacios o tabuladores."
|
||||||
|
|
||||||
#: base/domains/models.py:30
|
#: base/domains/models.py:30
|
||||||
msgid "domain name"
|
msgid "domain name"
|
||||||
|
@ -100,15 +103,15 @@ msgstr "Todos los eventos en mis proyectos"
|
||||||
|
|
||||||
#: base/notifications/models.py:13
|
#: base/notifications/models.py:13
|
||||||
msgid "Only events for objects i watch"
|
msgid "Only events for objects i watch"
|
||||||
msgstr "Solo eventos para objetos que observo"
|
msgstr "Sólo eventos para objetos a los cuales les hago seguimiento"
|
||||||
|
|
||||||
#: base/notifications/models.py:14
|
#: base/notifications/models.py:14
|
||||||
msgid "Only events for objects assigned to me"
|
msgid "Only events for objects assigned to me"
|
||||||
msgstr "Solo eventos para objetos asignados a mi"
|
msgstr "Sólo eventos para objetos que me han sido asignados"
|
||||||
|
|
||||||
#: base/notifications/models.py:15
|
#: base/notifications/models.py:15
|
||||||
msgid "Only events for objects owned by me"
|
msgid "Only events for objects owned by me"
|
||||||
msgstr "Solo eventos para mis objetos"
|
msgstr "Sólo eventos para mis objetos"
|
||||||
|
|
||||||
#: base/notifications/models.py:16
|
#: base/notifications/models.py:16
|
||||||
msgid "No events"
|
msgid "No events"
|
||||||
|
@ -144,11 +147,11 @@ msgstr "fechas importantes"
|
||||||
|
|
||||||
#: base/users/api.py:45 base/users/api.py:52
|
#: base/users/api.py:45 base/users/api.py:52
|
||||||
msgid "Invalid username or email"
|
msgid "Invalid username or email"
|
||||||
msgstr "usuario o email invalido"
|
msgstr "usuario o correos inválidos"
|
||||||
|
|
||||||
#: base/users/api.py:61
|
#: base/users/api.py:61
|
||||||
msgid "Mail sended successful!"
|
msgid "Mail sended successful!"
|
||||||
msgstr "¡Mail enviado correctamente!"
|
msgstr "¡Correo enviado correctamente!"
|
||||||
|
|
||||||
#: base/users/api.py:70
|
#: base/users/api.py:70
|
||||||
msgid "Token is invalid"
|
msgid "Token is invalid"
|
||||||
|
@ -160,7 +163,7 @@ msgstr "Argumentos incompletos"
|
||||||
|
|
||||||
#: base/users/api.py:90
|
#: base/users/api.py:90
|
||||||
msgid "Invalid password length"
|
msgid "Invalid password length"
|
||||||
msgstr "longitud del password no válida"
|
msgstr "longitud de la contraseña no válida"
|
||||||
|
|
||||||
#: base/users/models.py:13 projects/models.py:281 projects/models.py:331
|
#: base/users/models.py:13 projects/models.py:281 projects/models.py:331
|
||||||
#: projects/models.py:356 projects/models.py:379 projects/models.py:404
|
#: projects/models.py:356 projects/models.py:379 projects/models.py:404
|
||||||
|
@ -176,7 +179,7 @@ msgstr "descripción"
|
||||||
|
|
||||||
#: base/users/models.py:17
|
#: base/users/models.py:17
|
||||||
msgid "photo"
|
msgid "photo"
|
||||||
msgstr "foto"
|
msgstr "fotografía"
|
||||||
|
|
||||||
#: base/users/models.py:21
|
#: base/users/models.py:21
|
||||||
msgid "default timezone"
|
msgid "default timezone"
|
||||||
|
@ -216,11 +219,11 @@ msgstr "orden"
|
||||||
|
|
||||||
#: base/users/serializers.py:33
|
#: base/users/serializers.py:33
|
||||||
msgid "invalid token"
|
msgid "invalid token"
|
||||||
msgstr "token invalido"
|
msgstr "token inválido"
|
||||||
|
|
||||||
#: projects/api.py:89
|
#: projects/api.py:89
|
||||||
msgid "Email address is already taken."
|
msgid "Email address is already taken."
|
||||||
msgstr ""
|
msgstr "Dirección de correo ya utilizada"
|
||||||
|
|
||||||
#: projects/choices.py:7
|
#: projects/choices.py:7
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
|
@ -273,7 +276,7 @@ msgstr "Importante"
|
||||||
|
|
||||||
#: projects/choices.py:45
|
#: projects/choices.py:45
|
||||||
msgid "Critical"
|
msgid "Critical"
|
||||||
msgstr "Critica"
|
msgstr "Crítica"
|
||||||
|
|
||||||
#: projects/choices.py:54
|
#: projects/choices.py:54
|
||||||
msgid "Rejected"
|
msgid "Rejected"
|
||||||
|
@ -357,19 +360,19 @@ msgstr "miembros"
|
||||||
|
|
||||||
#: projects/models.py:105
|
#: projects/models.py:105
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "publico"
|
msgstr "público"
|
||||||
|
|
||||||
#: projects/models.py:107
|
#: projects/models.py:107
|
||||||
msgid "last us ref"
|
msgid "last us ref"
|
||||||
msgstr "ultima referencia de US"
|
msgstr "última referencia de US"
|
||||||
|
|
||||||
#: projects/models.py:109
|
#: projects/models.py:109
|
||||||
msgid "last task ref"
|
msgid "last task ref"
|
||||||
msgstr "ultima referencia de tarea"
|
msgstr "última referencia de tarea"
|
||||||
|
|
||||||
#: projects/models.py:111
|
#: projects/models.py:111
|
||||||
msgid "last issue ref"
|
msgid "last issue ref"
|
||||||
msgstr "ultima referencia de issue"
|
msgstr "última referencia de issue"
|
||||||
|
|
||||||
#: projects/models.py:113
|
#: projects/models.py:113
|
||||||
msgid "total of milestones"
|
msgid "total of milestones"
|
||||||
|
@ -418,7 +421,7 @@ msgstr "valor"
|
||||||
|
|
||||||
#: projects/documents/models.py:15
|
#: projects/documents/models.py:15
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "titulo"
|
msgstr "título"
|
||||||
|
|
||||||
#: projects/documents/models.py:30
|
#: projects/documents/models.py:30
|
||||||
msgid "attached_file"
|
msgid "attached_file"
|
||||||
|
@ -428,11 +431,11 @@ msgstr "fichero_adjunto"
|
||||||
#: projects/issues/api.py:88 projects/issues/api.py:91
|
#: projects/issues/api.py:88 projects/issues/api.py:91
|
||||||
#: projects/issues/api.py:94 projects/issues/api.py:97
|
#: projects/issues/api.py:94 projects/issues/api.py:97
|
||||||
msgid "You don't have permissions for add/modify this issue."
|
msgid "You don't have permissions for add/modify this issue."
|
||||||
msgstr "Tu no tienes permisos para crear/modificar esta peticion."
|
msgstr "No tienes permisos para crear/modificar esta petición."
|
||||||
|
|
||||||
#: projects/issues/api.py:131
|
#: projects/issues/api.py:131
|
||||||
msgid "You don't have permissions for add attachments to this issue"
|
msgid "You don't have permissions for add attachments to this issue"
|
||||||
msgstr "Tu no tienes permisos para añadir ficheros adjuntos a esta peticion"
|
msgstr "No tienes permisos para añadir ficheros adjuntos a esta petición"
|
||||||
|
|
||||||
#: projects/issues/models.py:20 projects/questions/models.py:20
|
#: projects/issues/models.py:20 projects/questions/models.py:20
|
||||||
#: projects/tasks/models.py:22 projects/userstories/models.py:42
|
#: projects/tasks/models.py:22 projects/userstories/models.py:42
|
||||||
|
@ -487,7 +490,7 @@ msgstr "Sin asignar"
|
||||||
|
|
||||||
#: projects/milestones/api.py:37
|
#: projects/milestones/api.py:37
|
||||||
msgid "You must not add a new milestone to this project."
|
msgid "You must not add a new milestone to this project."
|
||||||
msgstr "Tu no debes añadir un nuevo sprint a este proyecto."
|
msgstr "No debes añadir un nuevo sprint a este proyecto."
|
||||||
|
|
||||||
#: projects/milestones/models.py:28
|
#: projects/milestones/models.py:28
|
||||||
msgid "estimated start"
|
msgid "estimated start"
|
||||||
|
@ -511,12 +514,12 @@ msgstr "assignada_a"
|
||||||
|
|
||||||
#: projects/tasks/api.py:47
|
#: projects/tasks/api.py:47
|
||||||
msgid "You don't have permissions for add attachments to this task."
|
msgid "You don't have permissions for add attachments to this task."
|
||||||
msgstr "Tu no tienes permisos para añadir ficheros adjuntos a esta tarea."
|
msgstr "No tienes permisos para añadir ficheros adjuntos a esta tarea."
|
||||||
|
|
||||||
#: projects/tasks/api.py:74 projects/tasks/api.py:77 projects/tasks/api.py:80
|
#: projects/tasks/api.py:74 projects/tasks/api.py:77 projects/tasks/api.py:80
|
||||||
#: projects/tasks/api.py:83
|
#: projects/tasks/api.py:83
|
||||||
msgid "You don't have permissions for add/modify this task."
|
msgid "You don't have permissions for add/modify this task."
|
||||||
msgstr "Tu no tienes permisos para añadir/modificar esta tarea."
|
msgstr "No tienes permisos para añadir/modificar esta tarea."
|
||||||
|
|
||||||
#: projects/tasks/models.py:20 projects/userstories/models.py:20
|
#: projects/tasks/models.py:20 projects/userstories/models.py:20
|
||||||
msgid "user story"
|
msgid "user story"
|
||||||
|
@ -529,29 +532,29 @@ msgstr "es iocaina"
|
||||||
#: projects/userstories/api.py:55
|
#: projects/userstories/api.py:55
|
||||||
msgid "You don't have permissions for add attachments to this user story"
|
msgid "You don't have permissions for add attachments to this user story"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Tu no tienes permisos para añadir ficheros adjuntos a esta historia de "
|
"No tienes permisos para añadir ficheros adjuntos a esta historia de "
|
||||||
"usuario."
|
"usuario."
|
||||||
|
|
||||||
#: projects/userstories/api.py:75 projects/userstories/api.py:99
|
#: projects/userstories/api.py:75 projects/userstories/api.py:99
|
||||||
msgid "bulkStories parameter is mandatory"
|
msgid "bulkStories parameter is mandatory"
|
||||||
msgstr ""
|
msgstr "El parámetro bulkStories es obligatorio"
|
||||||
|
|
||||||
#: projects/userstories/api.py:79
|
#: projects/userstories/api.py:79
|
||||||
msgid "projectId parameter is mandatory"
|
msgid "projectId parameter is mandatory"
|
||||||
msgstr ""
|
msgstr "El parámetro projectID es obligatorio"
|
||||||
|
|
||||||
#: projects/userstories/api.py:84 projects/userstories/api.py:108
|
#: projects/userstories/api.py:84 projects/userstories/api.py:108
|
||||||
msgid "You don't have permisions to create user stories."
|
msgid "You don't have permisions to create user stories."
|
||||||
msgstr "Tu no tienes permisos para crear historias de usuario."
|
msgstr "No tienes permisos para crear historias de usuario."
|
||||||
|
|
||||||
#: projects/userstories/api.py:103
|
#: projects/userstories/api.py:103
|
||||||
msgid "projectId parameter ir mandatory"
|
msgid "projectId parameter ir mandatory"
|
||||||
msgstr ""
|
msgstr "El parámetro projectID es obligatorio"
|
||||||
|
|
||||||
#: projects/userstories/api.py:126 projects/userstories/api.py:129
|
#: projects/userstories/api.py:126 projects/userstories/api.py:129
|
||||||
#: projects/userstories/api.py:132
|
#: projects/userstories/api.py:132
|
||||||
msgid "You don't have permissions for add/modify this user story"
|
msgid "You don't have permissions for add/modify this user story"
|
||||||
msgstr "Tu no tienes permisos para crear o modificar esta historia de usuario."
|
msgstr "No tienes permisos para crear o modificar esta historia de usuario."
|
||||||
|
|
||||||
#: projects/userstories/models.py:23
|
#: projects/userstories/models.py:23
|
||||||
msgid "role"
|
msgid "role"
|
||||||
|
@ -576,15 +579,15 @@ msgstr "es requisito del equipo"
|
||||||
#: projects/wiki/api.py:39
|
#: projects/wiki/api.py:39
|
||||||
msgid "You don't have permissions for add attachments to this wiki page."
|
msgid "You don't have permissions for add attachments to this wiki page."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Tu no tienes permisos para añadir ficheros adjuntos a esta pagina de wiki."
|
"No tienes permisos para añadir ficheros adjuntos a esta página de wiki."
|
||||||
|
|
||||||
#: projects/wiki/api.py:65
|
#: projects/wiki/api.py:65
|
||||||
msgid "You don't haver permissions for add/modify this wiki page."
|
msgid "You don't haver permissions for add/modify this wiki page."
|
||||||
msgstr "Tu no tienes permisos para crear or modificar esta pagina de wiki."
|
msgstr "No tienes permisos para crear or modificar esta página de wiki."
|
||||||
|
|
||||||
#: settings/common.py:28
|
#: settings/common.py:28
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "Ingles"
|
msgstr "Inglés"
|
||||||
|
|
||||||
#: settings/common.py:29
|
#: settings/common.py:29
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
|
|
|
@ -32,7 +32,7 @@ class WikiLinkExtension(Extension):
|
||||||
return super().__init__(*args, **kwargs)
|
return super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md, md_globals):
|
||||||
WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[\w0-9_ -]+)?\]\]"
|
WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]"
|
||||||
md.inlinePatterns.add("wikilinks",
|
md.inlinePatterns.add("wikilinks",
|
||||||
WikiLinksPattern(md, WIKILINK_RE, self.project),
|
WikiLinksPattern(md, WIKILINK_RE, self.project),
|
||||||
"<not_strong")
|
"<not_strong")
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('attachments', '0002_add_size_and_name_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='attachment',
|
||||||
|
options={'ordering': ['project', 'created_date', 'id'], 'permissions': (('view_attachment', 'Can view attachment'),), 'verbose_name_plural': 'attachments', 'verbose_name': 'attachment'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -80,7 +80,7 @@ class Attachment(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "attachment"
|
verbose_name = "attachment"
|
||||||
verbose_name_plural = "attachments"
|
verbose_name_plural = "attachments"
|
||||||
ordering = ["project", "created_date"]
|
ordering = ["project", "created_date", "id"]
|
||||||
permissions = (
|
permissions = (
|
||||||
("view_attachment", "Can view attachment"),
|
("view_attachment", "Can view attachment"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff9900\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#ffcc00\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#999999\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]",
|
"task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff9900\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#ffcc00\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#999999\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]",
|
||||||
"is_backlog_activated": true,
|
"is_backlog_activated": true,
|
||||||
"modified_date": "2014-07-25T10:02:46.479Z",
|
"modified_date": "2014-07-25T10:02:46.479Z",
|
||||||
"us_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff8a84\", \"order\": 2, \"is_closed\": false, \"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\"}, {\"color\": \"#ff9900\", \"order\": 3, \"is_closed\": false, \"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#fcc000\", \"order\": 4, \"is_closed\": false, \"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 5, \"is_closed\": true, \"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\"}]",
|
"us_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff8a84\", \"order\": 2, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\"}, {\"color\": \"#ff9900\", \"order\": 3, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#fcc000\", \"order\": 4, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 5, \"is_closed\": true, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\"}, {\"color\": \"#5c3566\", \"order\": 6, \"is_closed\": true, \"is_archived\": true, \"wip_limit\": null, \"name\": \"Archived\", \"slug\": \"archived\"}]",
|
||||||
"is_wiki_activated": true,
|
"is_wiki_activated": true,
|
||||||
"roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]",
|
"roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]",
|
||||||
"points": "[{\"value\": null, \"order\": 1, \"name\": \"?\"}, {\"value\": 0.0, \"order\": 2, \"name\": \"0\"}, {\"value\": 0.5, \"order\": 3, \"name\": \"1/2\"}, {\"value\": 1.0, \"order\": 4, \"name\": \"1\"}, {\"value\": 2.0, \"order\": 5, \"name\": \"2\"}, {\"value\": 3.0, \"order\": 6, \"name\": \"3\"}, {\"value\": 5.0, \"order\": 7, \"name\": \"5\"}, {\"value\": 8.0, \"order\": 8, \"name\": \"8\"}, {\"value\": 10.0, \"order\": 9, \"name\": \"10\"}, {\"value\": 15.0, \"order\": 10, \"name\": \"15\"}, {\"value\": 20.0, \"order\": 11, \"name\": \"20\"}, {\"value\": 40.0, \"order\": 12, \"name\": \"40\"}]",
|
"points": "[{\"value\": null, \"order\": 1, \"name\": \"?\"}, {\"value\": 0.0, \"order\": 2, \"name\": \"0\"}, {\"value\": 0.5, \"order\": 3, \"name\": \"1/2\"}, {\"value\": 1.0, \"order\": 4, \"name\": \"1\"}, {\"value\": 2.0, \"order\": 5, \"name\": \"2\"}, {\"value\": 3.0, \"order\": 6, \"name\": \"3\"}, {\"value\": 5.0, \"order\": 7, \"name\": \"5\"}, {\"value\": 8.0, \"order\": 8, \"name\": \"8\"}, {\"value\": 10.0, \"order\": 9, \"name\": \"10\"}, {\"value\": 15.0, \"order\": 10, \"name\": \"15\"}, {\"value\": 20.0, \"order\": 11, \"name\": \"20\"}, {\"value\": 40.0, \"order\": 12, \"name\": \"40\"}]",
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
"task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]",
|
"task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]",
|
||||||
"is_backlog_activated": false,
|
"is_backlog_activated": false,
|
||||||
"modified_date": "2014-07-25T13:11:42.754Z",
|
"modified_date": "2014-07-25T13:11:42.754Z",
|
||||||
"us_statuses": "[{\"wip_limit\": null, \"order\": 1, \"is_closed\": false, \"color\": \"#999999\", \"name\": \"New\", \"slug\": \"new\"}, {\"wip_limit\": null, \"order\": 2, \"is_closed\": false, \"color\": \"#f57900\", \"name\": \"Ready\", \"slug\": \"ready\"}, {\"wip_limit\": null, \"order\": 3, \"is_closed\": false, \"color\": \"#729fcf\", \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"wip_limit\": null, \"order\": 4, \"is_closed\": false, \"color\": \"#4e9a06\", \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"wip_limit\": null, \"order\": 5, \"is_closed\": true, \"color\": \"#cc0000\", \"name\": \"Done\", \"slug\": \"done\"}]",
|
"us_statuses": "[{\"wip_limit\": null, \"order\": 1, \"is_closed\": false, \"is_archived\": false, \"color\": \"#999999\", \"name\": \"New\", \"slug\": \"new\"}, {\"wip_limit\": null, \"order\": 2, \"is_closed\": false, \"is_archived\": false, \"color\": \"#f57900\", \"name\": \"Ready\", \"slug\": \"ready\"}, {\"wip_limit\": null, \"order\": 3, \"is_closed\": false, \"is_archived\": false, \"color\": \"#729fcf\", \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"wip_limit\": null, \"order\": 4, \"is_closed\": false, \"is_archived\": false, \"color\": \"#4e9a06\", \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"wip_limit\": null, \"order\": 5, \"is_closed\": true, \"is_archived\": false, \"color\": \"#cc0000\", \"name\": \"Done\", \"slug\": \"done\"}, {\"wip_limit\": null, \"order\": 6, \"is_closed\": true, \"is_archived\": true, \"color\": \"#5c3566\", \"name\": \"Archived\", \"slug\": \"archived\"}]",
|
||||||
"is_wiki_activated": false,
|
"is_wiki_activated": false,
|
||||||
"roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]",
|
"roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]",
|
||||||
"points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 15.0, \"name\": \"15\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]",
|
"points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 15.0, \"name\": \"15\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]",
|
||||||
|
|
|
@ -173,7 +173,7 @@ def is_hidden_snapshot(obj:FrozenDiff) -> bool:
|
||||||
nfields = _not_important_fields[content_type]
|
nfields = _not_important_fields[content_type]
|
||||||
result = snapshot_fields - nfields
|
result = snapshot_fields - nfields
|
||||||
|
|
||||||
if len(result) == 0:
|
if snapshot_fields and len(result) == 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -9,141 +9,162 @@
|
||||||
"us_order"
|
"us_order"
|
||||||
] %}
|
] %}
|
||||||
|
|
||||||
<dl>
|
|
||||||
{% for field_name, values in changed_fields.items() %}
|
{% for field_name, values in changed_fields.items() %}
|
||||||
{% if field_name not in excluded_fields %}
|
{% if field_name not in excluded_fields %}
|
||||||
<dt style="background: #669933; padding: 5px 15px; color: #fff">
|
{# POINTS #}
|
||||||
<b>{{ verbose_name(object, field_name) }}</b>
|
|
||||||
</dt>
|
|
||||||
|
|
||||||
{# POINTS #}
|
|
||||||
{% if field_name == "points" %}
|
{% if field_name == "points" %}
|
||||||
|
|
||||||
{% for role, points in values.items() %}
|
{% for role, points in values.items() %}
|
||||||
<dd style="background: #b2cc99; padding: 5px 15px; color: #fff">
|
<tr>
|
||||||
<b>{{ role }}</b>
|
<td valign="middle" rowspan="2" class="update-row-name">
|
||||||
</dd>
|
<h3>{% trans role=role %}{{ role }} role points{% endtrans %}</h3>
|
||||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
</td>
|
||||||
<b>to:</b> <i>{{ points.1|linebreaksbr }}</i>
|
<td valign="top" class="update-row-from">
|
||||||
</dd>
|
<span>{{ _("from") }}</span><br>
|
||||||
<dd style="padding: 5px 15px; color: #bbb">
|
<strong>{{ points.1 }}</strong>
|
||||||
<b>from:</b> <i>{{ points.0|linebreaksbr }}</i>
|
</td>
|
||||||
</dd>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
<span>{{ _("to") }}</span><br>
|
||||||
|
<strong>{{ points.0 }}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{# ATTACHMENTS #}
|
{# ATTACHMENTS #}
|
||||||
{% elif field_name == "attachments" %}
|
{% elif field_name == "attachments" %}
|
||||||
|
|
||||||
{% if values.new %}
|
{% if values.new %}
|
||||||
<dd style="background: #b2cc99; padding: 5px 15px; color: #fff">
|
|
||||||
<b>{{ _("Added") }}</b>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
{% for att in values['new']%}
|
{% for att in values['new']%}
|
||||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
<tr>
|
||||||
<a href="{{ att.url }}" target="_blank" style="font-weight: bold; color: #444">
|
<td colspan="2">
|
||||||
{{ att.filename|linebreaksbr }}
|
<h3>{{ _("Added new attachment") }}</h3>
|
||||||
</a>
|
<p>
|
||||||
{% if att.description %}<i> {{ att.description|linebreaksbr }}</i>{% endif %}
|
<a href="{{ att.url }}" target="_blank">
|
||||||
</dd>
|
{{ att.filename }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% if att.description %}
|
||||||
|
<p>{{ att.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if values.changed %}
|
{% if values.changed %}
|
||||||
<dd style="background: #b2cc99; padding: 5px 15px; color: #fff">
|
|
||||||
<b>{{ _("Changed") }}</b>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
{% for att in values['changed'] %}
|
{% for att in values['changed'] %}
|
||||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
<tr>
|
||||||
<a href="{{ att.url }}" target="_blank" style="font-weight: bold; color: #444">
|
<td colspan="2">
|
||||||
{{ att.filename|linebreaksbr }}
|
<h3>{{ _("Updated attachment") }}</h3>
|
||||||
</a>
|
<p>
|
||||||
<ul>
|
<a href="{{ att.url }}" target="_blank">
|
||||||
{% if att.changes.is_deprecated %}
|
{{ att.filename|linebreaksbr }}
|
||||||
{% if att.changes.is_deprecated.1 %}
|
{% if att.changes.is_deprecated %}
|
||||||
<li>to <i>deprecated</i></li>
|
{% if att.changes.is_deprecated.1 %}
|
||||||
{% else %}
|
[<i>{{ _("deprecated") }}</i>]
|
||||||
<li>to <i>not deprecated</i></li>
|
{% else %}
|
||||||
{% endif %}
|
[<i>{{ _("not deprecated") }}</i>]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
{% if att.changes.description %}
|
{% if att.changes.description %}
|
||||||
<li>description to <i>{{ att.changes.description.1 }}</i></li>
|
<p>{{ att.changes.description.1 }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</td>
|
||||||
</dd>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if values.deleted %}
|
{% if values.deleted %}
|
||||||
<dd style="background: #b2cc99; padding: 5px 15px; color: #fff">
|
|
||||||
<b>{{ _("Deleted") }}</b>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
{% for att in values['deleted']%}
|
{% for att in values['deleted']%}
|
||||||
<dd style="padding: 5px 15px; color: #bbb">
|
<tr>
|
||||||
<i>{{ att.filename|linebreaksbr }}</i>
|
<td colspan="2">
|
||||||
</dd>
|
<h3>{{ _("Deleted attachment") }}</h3>
|
||||||
|
{% if att.changes.description %}
|
||||||
|
<p>{{ att.filename|linebreaksbr }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# TAGS AND WATCHERS #}
|
{# TAGS AND WATCHERS #}
|
||||||
{% elif field_name in ["tags", "watchers"] %}
|
{% elif field_name in ["tags", "watchers"] %}
|
||||||
|
<tr>
|
||||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
<td valign="middle" rowspan="2" class="update-row-name">
|
||||||
<b>to:</b> <i>{{ ', '.join(values.1)|linebreaksbr }}</i>
|
<h3>{{ field_name }}</h3>
|
||||||
</dd>
|
</td>
|
||||||
|
<td valign="top" class="update-row-from">
|
||||||
{% if values.0 %}
|
<span>{{ _("from") }}</span><br>
|
||||||
<dd style="padding: 5px 15px; color: #bbb">
|
<strong>{{ ', '.join(values.0) }}</strong>
|
||||||
<b>from:</b> <i>{{ ', '.join(values.0)|linebreaksbr }}</i>
|
</td>
|
||||||
</dd>
|
</tr>
|
||||||
{% endif %}
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
<span>{{ _("to") }}</span><br>
|
||||||
|
<strong>{{ ', '.join(values.1) }}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{# DESCRIPTIONS #}
|
{# DESCRIPTIONS #}
|
||||||
{% elif field_name in ["description_diff"] %}
|
{% elif field_name in ["description_diff"] %}
|
||||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
<tr>
|
||||||
<b>diff:</b> <i>{{ mdrender(project, values.1) }}</i>
|
<td colspan="2">
|
||||||
</dd>
|
<h3>{{ _("Description diff") }}</h3>
|
||||||
|
<p>{{ mdrender(project, values.1) }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{# CONTENT #}
|
{# CONTENT #}
|
||||||
{% elif field_name in ["content_diff"] %}
|
{% elif field_name in ["content_diff"] %}
|
||||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
<tr>
|
||||||
<b>diff:</b> <i>{{ mdrender(project, values.1) }}</i>
|
<td colspan="2">
|
||||||
</dd>
|
<h3>{{ _("Content diff") }}</h3>
|
||||||
|
<p>{{ mdrender(project, values.1) }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{# ASSIGNED TO #}
|
{# ASSIGNED TO #}
|
||||||
{% elif field_name == "assigned_to" %}
|
{% elif field_name == "assigned_to" %}
|
||||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
<tr>
|
||||||
{% if values.1 != None and values.1 != "" %}
|
<td valign="middle" rowspan="2" class="update-row-name">
|
||||||
<b>to:</b> <i>{{ values.1|linebreaksbr }}</i>
|
<h3>{{ field_name }}</h3>
|
||||||
{% else %}
|
</td>
|
||||||
<b>to:</b> <i>{{ _("Unassigned") }}</i>
|
<td valign="top" class="update-row-from">
|
||||||
{% endif %}
|
{% if values.0 != None and values.0 != "" %}
|
||||||
</dd>
|
<span>{{ _("from") }}</span><br>
|
||||||
|
<strong>{{ values.0 }}</strong>
|
||||||
<dd style="padding: 5px 15px; color: #bbb">
|
{% else %}
|
||||||
{% if values.0 != None and values.0 != "" %}
|
<span>{{ _("from") }}</span><br>
|
||||||
<b>from:</b> <i>{{ values.0|linebreaksbr }}</i>
|
<strong>{{ _("Unassigned") }}</strong>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<b>from:</b> <i>{{ _("Unassigned") }}</i>
|
</td>
|
||||||
{% endif %}
|
</tr>
|
||||||
</dd>
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
{% if values.1 != None and values.1 != "" %}
|
||||||
|
<span>{{ _("to") }}</span><br>
|
||||||
|
<strong>{{ values.1 }}</strong>
|
||||||
|
{% else %}
|
||||||
|
<span>{{ _("to") }}</span><br>
|
||||||
|
<strong>{{ _("Unassigned") }}</strong>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{# * #}
|
{# * #}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if values.1 != None and values.1 != "" %}
|
|
||||||
<dd style="background: #eee; padding: 5px 15px; color: #444">
|
|
||||||
<b>to:</b> <i>{{ values.1|linebreaksbr }}</i>
|
|
||||||
</dd>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if values.0 != None and values.0 != "" %}
|
|
||||||
<dd style="padding: 5px 15px; color: #bbb">
|
|
||||||
<b>from:</b> <i>{{ values.0|linebreaksbr }}</i>
|
|
||||||
</dd>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td valign="middle" rowspan="2" class="update-row-name">
|
||||||
|
<h3>{{ field_name }}</h3>
|
||||||
|
</td>
|
||||||
|
<td valign="top" class="update-row-from">
|
||||||
|
<span>{{ _("from") }}</span><br>
|
||||||
|
<strong>{{ values.1|linebreaksbr }}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
<span>{{ _("to") }}</span><br>
|
||||||
|
<strong>{{ values.0|linebreaksbr }}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
|
||||||
|
|
|
@ -14,43 +14,43 @@
|
||||||
{# POINTS #}
|
{# POINTS #}
|
||||||
{% if field_name == "points" %}
|
{% if field_name == "points" %}
|
||||||
{% for role, points in values.items() %}
|
{% for role, points in values.items() %}
|
||||||
* {{ role }} to: {{ points.1|linebreaksbr }} from: {{ points.0|linebreaksbr }}
|
* {{ role }} {{ _("to:") }} {{ points.1 }} {{ _("from:") }} {{ points.0 }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{# ATTACHMENTS #}
|
{# ATTACHMENTS #}
|
||||||
{% elif field_name == "attachments" %}
|
{% elif field_name == "attachments" %}
|
||||||
{% if values.new %}
|
{% if values.new %}
|
||||||
* {{ _("Added") }}:
|
* {{ _("Added") }}:
|
||||||
{% for att in values['new']%}
|
{% for att in values['new']%}
|
||||||
- {{ att.filename|linebreaksbr }}
|
- {{ att.filename }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if values.changed %}
|
{% if values.changed %}
|
||||||
* {{ _("Changed") }}
|
* {{ _("Changed") }}
|
||||||
{% for att in values['changed'] %}
|
{% for att in values['changed'] %}
|
||||||
- {{ att.filename|linebreaksbr }}
|
- {{ att.filename }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if values.deleted %}
|
{% if values.deleted %}
|
||||||
* {{ _("Deleted") }}
|
* {{ _("Deleted") }}
|
||||||
{% for att in values['deleted']%}
|
{% for att in values['deleted']%}
|
||||||
- {{ att.filename|linebreaksbr }}
|
- {{ att.filename }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# TAGS AND WATCHERS #}
|
{# TAGS AND WATCHERS #}
|
||||||
{% elif field_name in ["tags", "watchers"] %}
|
{% elif field_name in ["tags", "watchers"] %}
|
||||||
* to: {{ ', '.join(values.1)|linebreaksbr }}
|
* {{ _("to:") }} {{ ', '.join(values.1) }}
|
||||||
{% if values.0 %}
|
{% if values.0 %}
|
||||||
* from: {{ ', '.join(values.0)|linebreaksbr }}
|
* {{ _("from:") }} {{ ', '.join(values.0) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# * #}
|
{# * #}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if values.1 != None and values.1 != "" %}
|
{% if values.1 != None and values.1 != "" %}
|
||||||
* to: {{ values.1|linebreaksbr }}
|
* {{ _("to:") }} {{ values.1|linebreaksbr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if values.0 != None and values.0 != "" %}
|
{% if values.0 != None and values.0 != "" %}
|
||||||
* from: {{ values.0|linebreaksbr }}
|
* {{ _("from:") }} {{ values.0|linebreaksbr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
from django.db import connection
|
||||||
|
from taiga.projects.userstories.models import *
|
||||||
|
from taiga.projects.tasks.models import *
|
||||||
|
from taiga.projects.issues.models import *
|
||||||
|
from taiga.projects.models import *
|
||||||
|
|
||||||
|
def _fix_tags_model(tags_model):
|
||||||
|
table_name = tags_model._meta.db_table
|
||||||
|
query = "select id from (select id, unnest(tags) tag from %s) x where tag LIKE '%%,%%'"%(table_name)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(query)
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
id = row[0]
|
||||||
|
instance = tags_model.objects.get(id=id)
|
||||||
|
instance.tags = [tag.replace(",", "") for tag in instance.tags]
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
|
def fix_tags(apps, schema_editor):
|
||||||
|
print("Fixing user issue tags")
|
||||||
|
_fix_tags_model(Issue)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('issues', '0002_issue_external_reference'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(fix_tags),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('issues', '0003_auto_20141210_1108'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='issue',
|
||||||
|
options={'ordering': ['project', '-id'], 'permissions': (('view_issue', 'Can view issue'),), 'verbose_name_plural': 'issues', 'verbose_name': 'issue'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -69,7 +69,7 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "issue"
|
verbose_name = "issue"
|
||||||
verbose_name_plural = "issues"
|
verbose_name_plural = "issues"
|
||||||
ordering = ["project", "-created_date"]
|
ordering = ["project", "-id"]
|
||||||
permissions = (
|
permissions = (
|
||||||
("view_issue", "Can view issue"),
|
("view_issue", "Can view issue"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin, PgArrayField
|
from taiga.base.serializers import Serializer, TagsField, NeighborsSerializerMixin, PgArrayField
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.projects.validators import ProjectExistsValidator
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
|
@ -25,7 +25,7 @@ from . import models
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
|
class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
|
||||||
tags = PickleField(required=False)
|
tags = TagsField(required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
is_closed = serializers.Field(source="is_closed")
|
is_closed = serializers.Field(source="is_closed")
|
||||||
comment = serializers.SerializerMethodField("get_comment")
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
|
|
|
@ -40,19 +40,19 @@ def update_many(objects, fields=[], using="default"):
|
||||||
|
|
||||||
|
|
||||||
def update_slug(apps, schema_editor):
|
def update_slug(apps, schema_editor):
|
||||||
update_qs = UserStoryStatus.objects.all()
|
update_qs = UserStoryStatus.objects.all().only("name")
|
||||||
for us_status in update_qs:
|
for us_status in update_qs:
|
||||||
us_status.slug = slugify(unidecode(us_status.name))
|
us_status.slug = slugify(unidecode(us_status.name))
|
||||||
|
|
||||||
update_many(update_qs, fields=["slug"])
|
update_many(update_qs, fields=["slug"])
|
||||||
|
|
||||||
update_qs = TaskStatus.objects.all()
|
update_qs = TaskStatus.objects.all().only("name")
|
||||||
for task_status in update_qs:
|
for task_status in update_qs:
|
||||||
task_status.slug = slugify(unidecode(task_status.name))
|
task_status.slug = slugify(unidecode(task_status.name))
|
||||||
|
|
||||||
update_many(update_qs, fields=["slug"])
|
update_many(update_qs, fields=["slug"])
|
||||||
|
|
||||||
update_qs = IssueStatus.objects.all()
|
update_qs = IssueStatus.objects.all().only("name")
|
||||||
for issue_status in update_qs:
|
for issue_status in update_qs:
|
||||||
issue_status.slug = slugify(unidecode(issue_status.name))
|
issue_status.slug = slugify(unidecode(issue_status.name))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
from django.db import connection
|
||||||
|
from taiga.projects.userstories.models import *
|
||||||
|
from taiga.projects.tasks.models import *
|
||||||
|
from taiga.projects.issues.models import *
|
||||||
|
from taiga.projects.models import *
|
||||||
|
|
||||||
|
def _fix_tags_model(tags_model):
|
||||||
|
table_name = tags_model._meta.db_table
|
||||||
|
query = "select id from (select id, unnest(tags) tag from %s) x where tag LIKE '%%,%%'"%(table_name)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(query)
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
id = row[0]
|
||||||
|
instance = tags_model.objects.get(id=id)
|
||||||
|
instance.tags = [tag.replace(",", "") for tag in instance.tags]
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
|
def fix_tags(apps, schema_editor):
|
||||||
|
print("Fixing project tags")
|
||||||
|
_fix_tags_model(Project)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('projects', '0012_auto_20141210_1009'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(fix_tags),
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('projects', '0013_auto_20141210_1040'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userstorystatus',
|
||||||
|
name='is_archived',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='is archived'),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
def fix_project_template_us_status_archived(apps, schema_editor):
|
||||||
|
ProjectTemplate = apps.get_model("projects", "ProjectTemplate")
|
||||||
|
for pt in ProjectTemplate.objects.all():
|
||||||
|
for us_status in pt.us_statuses:
|
||||||
|
us_status["is_archived"] = False
|
||||||
|
|
||||||
|
pt.us_statuses.append({
|
||||||
|
"color": "#5c3566",
|
||||||
|
"order": 6,
|
||||||
|
"is_closed": True,
|
||||||
|
"is_archived": True,
|
||||||
|
"wip_limit": None,
|
||||||
|
"name": "Archived",
|
||||||
|
"slug": "archived"})
|
||||||
|
|
||||||
|
pt.save()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('projects', '0014_userstorystatus_is_archived'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(fix_project_template_us_status_archived),
|
||||||
|
]
|
|
@ -40,14 +40,19 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
|
||||||
serializer_class = serializers.MilestoneSerializer
|
serializer_class = serializers.MilestoneSerializer
|
||||||
permission_classes = (permissions.MilestonePermission,)
|
permission_classes = (permissions.MilestonePermission,)
|
||||||
filter_backends = (filters.CanViewMilestonesFilterBackend,)
|
filter_backends = (filters.CanViewMilestonesFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project", "closed")
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = models.Milestone.objects.all()
|
qs = models.Milestone.objects.all()
|
||||||
qs = qs.prefetch_related("user_stories",
|
qs = qs.prefetch_related("user_stories",
|
||||||
"user_stories__role_points",
|
"user_stories__role_points",
|
||||||
"user_stories__role_points__points",
|
"user_stories__role_points__points",
|
||||||
"user_stories__role_points__role")
|
"user_stories__role_points__role",
|
||||||
|
"user_stories__generated_from_issue",
|
||||||
|
"user_stories__project",
|
||||||
|
"watchers",
|
||||||
|
"user_stories__watchers")
|
||||||
|
qs = qs.select_related("project")
|
||||||
qs = qs.order_by("-estimated_start")
|
qs = qs.order_by("-estimated_start")
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
|
@ -91,46 +91,62 @@ class Milestone(WatchedModelMixin, models.Model):
|
||||||
@property
|
@property
|
||||||
def total_points(self):
|
def total_points(self):
|
||||||
return self._get_user_stories_points(
|
return self._get_user_stories_points(
|
||||||
[us for us in self.user_stories.all().prefetch_related('role_points', 'role_points__points')]
|
[us for us in self.user_stories.all()]
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def closed_points(self):
|
def closed_points(self):
|
||||||
return self._get_user_stories_points(
|
return self._get_user_stories_points(
|
||||||
[us for us in self.user_stories.all().prefetch_related('role_points', 'role_points__points') if us.is_closed]
|
[us for us in self.user_stories.all() if us.is_closed]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_points_increment(self, client_requirement, team_requirement):
|
def _get_increment_points(self):
|
||||||
|
if hasattr(self, "_increments"):
|
||||||
|
return self._increments
|
||||||
|
|
||||||
|
self._increments = {
|
||||||
|
"client_increment": {},
|
||||||
|
"team_increment": {},
|
||||||
|
"shared_increment": {},
|
||||||
|
}
|
||||||
user_stories = UserStory.objects.none()
|
user_stories = UserStory.objects.none()
|
||||||
if self.estimated_start and self.estimated_finish:
|
if self.estimated_start and self.estimated_finish:
|
||||||
user_stories = UserStory.objects.filter(
|
user_stories = filter(
|
||||||
created_date__gte=self.estimated_start,
|
lambda x: x.created_date.date() >= self.estimated_start and x.created_date.date() < self.estimated_finish,
|
||||||
created_date__lt=self.estimated_finish,
|
self.project.user_stories.all()
|
||||||
project_id=self.project_id,
|
)
|
||||||
client_requirement=client_requirement,
|
self._increments['client_increment'] = self._get_user_stories_points(
|
||||||
team_requirement=team_requirement
|
[us for us in user_stories if us.client_requirement is True and us.team_requirement is False]
|
||||||
).prefetch_related('role_points', 'role_points__points')
|
)
|
||||||
return self._get_user_stories_points(user_stories)
|
self._increments['team_increment'] = self._get_user_stories_points(
|
||||||
|
[us for us in user_stories if us.client_requirement is False and us.team_requirement is True]
|
||||||
|
)
|
||||||
|
self._increments['shared_increment'] = self._get_user_stories_points(
|
||||||
|
[us for us in user_stories if us.client_requirement is True and us.team_requirement is True]
|
||||||
|
)
|
||||||
|
return self._increments
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client_increment_points(self):
|
def client_increment_points(self):
|
||||||
client_increment = self._get_points_increment(True, False)
|
self._get_increment_points()
|
||||||
|
client_increment = self._get_increment_points()["client_increment"]
|
||||||
shared_increment = {
|
shared_increment = {
|
||||||
key: value/2 for key, value in self.shared_increment_points.items()
|
key: value/2 for key, value in self._get_increment_points()["shared_increment"].items()
|
||||||
}
|
}
|
||||||
return dict_sum(client_increment, shared_increment)
|
return dict_sum(client_increment, shared_increment)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def team_increment_points(self):
|
def team_increment_points(self):
|
||||||
team_increment = self._get_points_increment(False, True)
|
team_increment = self._get_increment_points()["team_increment"]
|
||||||
shared_increment = {
|
shared_increment = {
|
||||||
key: value/2 for key, value in self.shared_increment_points.items()
|
key: value/2 for key, value in self._get_increment_points()["shared_increment"].items()
|
||||||
}
|
}
|
||||||
return dict_sum(team_increment, shared_increment)
|
return dict_sum(team_increment, shared_increment)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shared_increment_points(self):
|
def shared_increment_points(self):
|
||||||
return self._get_points_increment(True, True)
|
return self._get_increment_points()["shared_increment"]
|
||||||
|
|
||||||
def closed_points_by_date(self, date):
|
def closed_points_by_date(self, date):
|
||||||
return self._get_user_stories_points([
|
return self._get_user_stories_points([
|
||||||
|
|
|
@ -254,23 +254,20 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
|
||||||
return dict_sum(*flat_role_dicts)
|
return dict_sum(*flat_role_dicts)
|
||||||
|
|
||||||
def _get_points_increment(self, client_requirement, team_requirement):
|
def _get_points_increment(self, client_requirement, team_requirement):
|
||||||
userstory_model = apps.get_model("userstories", "UserStory")
|
|
||||||
user_stories = userstory_model.objects.none()
|
|
||||||
last_milestones = self.milestones.order_by('-estimated_finish')
|
last_milestones = self.milestones.order_by('-estimated_finish')
|
||||||
last_milestone = last_milestones[0] if last_milestones else None
|
last_milestone = last_milestones[0] if last_milestones else None
|
||||||
if last_milestone:
|
if last_milestone:
|
||||||
user_stories = userstory_model.objects.filter(
|
user_stories = self.user_stories.filter(
|
||||||
created_date__gte=last_milestone.estimated_finish,
|
created_date__gte=last_milestone.estimated_finish,
|
||||||
project_id=self.id,
|
|
||||||
client_requirement=client_requirement,
|
client_requirement=client_requirement,
|
||||||
team_requirement=team_requirement
|
team_requirement=team_requirement
|
||||||
).prefetch_related('role_points', 'role_points__points')
|
)
|
||||||
else:
|
else:
|
||||||
user_stories = userstory_model.objects.filter(
|
user_stories = self.user_stories.filter(
|
||||||
project_id=self.id,
|
|
||||||
client_requirement=client_requirement,
|
client_requirement=client_requirement,
|
||||||
team_requirement=team_requirement
|
team_requirement=team_requirement
|
||||||
).prefetch_related('role_points', 'role_points__points')
|
)
|
||||||
|
user_stories = user_stories.prefetch_related('role_points', 'role_points__points')
|
||||||
return self._get_user_stories_points(user_stories)
|
return self._get_user_stories_points(user_stories)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -291,15 +288,26 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def closed_points(self):
|
def closed_points(self):
|
||||||
return self._get_user_stories_points(self.user_stories.filter(is_closed=True).prefetch_related('role_points', 'role_points__points'))
|
return self.calculated_points["closed"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def defined_points(self):
|
def defined_points(self):
|
||||||
return self._get_user_stories_points(self.user_stories.all().prefetch_related('role_points', 'role_points__points'))
|
return self.calculated_points["defined"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def assigned_points(self):
|
def assigned_points(self):
|
||||||
return self._get_user_stories_points(self.user_stories.filter(milestone__isnull=False).prefetch_related('role_points', 'role_points__points'))
|
return self.calculated_points["assigned"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def calculated_points(self):
|
||||||
|
user_stories = self.user_stories.all().prefetch_related('role_points', 'role_points__points')
|
||||||
|
closed_user_stories = user_stories.filter(is_closed=True)
|
||||||
|
assigned_user_stories = user_stories.filter(milestone__isnull=False)
|
||||||
|
return {
|
||||||
|
"defined": self._get_user_stories_points(user_stories),
|
||||||
|
"closed": self._get_user_stories_points(closed_user_stories),
|
||||||
|
"assigned": self._get_user_stories_points(assigned_user_stories),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProjectModulesConfig(models.Model):
|
class ProjectModulesConfig(models.Model):
|
||||||
|
@ -323,6 +331,8 @@ class UserStoryStatus(models.Model):
|
||||||
verbose_name=_("order"))
|
verbose_name=_("order"))
|
||||||
is_closed = models.BooleanField(default=False, null=False, blank=True,
|
is_closed = models.BooleanField(default=False, null=False, blank=True,
|
||||||
verbose_name=_("is closed"))
|
verbose_name=_("is closed"))
|
||||||
|
is_archived = models.BooleanField(default=False, null=False, blank=True,
|
||||||
|
verbose_name=_("is archived"))
|
||||||
color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
|
color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
|
||||||
verbose_name=_("color"))
|
verbose_name=_("color"))
|
||||||
wip_limit = models.IntegerField(null=True, blank=True, default=None,
|
wip_limit = models.IntegerField(null=True, blank=True, default=None,
|
||||||
|
@ -690,6 +700,7 @@ class ProjectTemplate(models.Model):
|
||||||
name=us_status["name"],
|
name=us_status["name"],
|
||||||
slug=us_status["slug"],
|
slug=us_status["slug"],
|
||||||
is_closed=us_status["is_closed"],
|
is_closed=us_status["is_closed"],
|
||||||
|
is_archived=us_status["is_archived"],
|
||||||
color=us_status["color"],
|
color=us_status["color"],
|
||||||
wip_limit=us_status["wip_limit"],
|
wip_limit=us_status["wip_limit"],
|
||||||
order=us_status["order"],
|
order=us_status["order"],
|
||||||
|
|
|
@ -198,8 +198,8 @@ def _make_template_mail(name:str):
|
||||||
instance for specified name, and return an instance
|
instance for specified name, and return an instance
|
||||||
of it.
|
of it.
|
||||||
"""
|
"""
|
||||||
cls = type("TemplateMail",
|
cls = type("InlineCSSTemplateMail",
|
||||||
(template_mail.TemplateMail,),
|
(template_mail.InlineCSSTemplateMail,),
|
||||||
{"name": name})
|
{"name": name})
|
||||||
|
|
||||||
return cls()
|
return cls()
|
||||||
|
@ -250,7 +250,8 @@ def send_sync_notifications(notification_id):
|
||||||
history_entries = tuple(notification.history_entries.all().order_by("created_at"))
|
history_entries = tuple(notification.history_entries.all().order_by("created_at"))
|
||||||
obj, _ = get_last_snapshot_for_key(notification.key)
|
obj, _ = get_last_snapshot_for_key(notification.key)
|
||||||
|
|
||||||
context = {"snapshot": obj.snapshot,
|
context = {
|
||||||
|
"snapshot": obj.snapshot,
|
||||||
"project": notification.project,
|
"project": notification.project,
|
||||||
"changer": notification.owner,
|
"changer": notification.owner,
|
||||||
"history_entries": history_entries}
|
"history_entries": history_entries}
|
||||||
|
@ -260,6 +261,7 @@ def send_sync_notifications(notification_id):
|
||||||
email = _make_template_mail(template_name)
|
email = _make_template_mail(template_name)
|
||||||
|
|
||||||
for user in notification.notify_users.distinct():
|
for user in notification.notify_users.distinct():
|
||||||
|
context["user"] = user
|
||||||
email.send(user.email, context)
|
email.send(user.email, context)
|
||||||
|
|
||||||
notification.delete()
|
notification.delete()
|
||||||
|
|
|
@ -1,30 +1,15 @@
|
||||||
{% extends "emails/base.jinja" %}
|
{% extends "emails/updates-body-html.jinja" %}
|
||||||
|
|
||||||
{% set final_url = resolve_front_url("issue", project.slug, snapshot.ref) %}
|
{% block head %}
|
||||||
{% set final_url_name = "Taiga - View issue #{0}".format(snapshot.ref) %}
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
changer=changer.get_full_name()|safe,
|
||||||
{% block body %}
|
project=project.name|safe,
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
ref=snapshot.ref,
|
||||||
<tr>
|
subject=snapshot.subject|safe,
|
||||||
<td>
|
url=resolve_front_url("issue", project.slug, snapshot.ref) %}
|
||||||
<h1>Project: {{ project.name }}</h1>
|
<h1>Issue updated</h1>
|
||||||
<h2>Issue #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
<p>Hello {{ user }}, <br> {{ changer }} has updated an issue on {{ project }}</p>
|
||||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
<p>Issue #{{ ref }} {{ subject }}</p>
|
||||||
{% for entry in history_entries%}
|
<a class="button" href="{{ url }}" title="See Issue #{{ ref }}: {{ subject }} in Taiga">See issue</a>
|
||||||
{% if entry.comment %}
|
{% endtrans %}
|
||||||
<p>Comment <b>{{ mdrender(project, entry.comment) }}</b></p>
|
|
||||||
{% endif %}
|
|
||||||
{% set changed_fields = entry.values_diff %}
|
|
||||||
{% if changed_fields %}
|
|
||||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
||||||
{% block footer %}
|
|
||||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
|
||||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
{% set final_url = resolve_front_url("issue", project.slug, snapshot.ref) %}
|
{% extends "emails/updates-body-text.jinja" %}
|
||||||
{% set final_url_name = "Taiga - View issue #{0}".format(snapshot.ref) %}
|
{% block head %}
|
||||||
|
{% trans user=user.get_full_name(),
|
||||||
- Project: {{ project.name }}
|
changer=changer.get_full_name()|safe,
|
||||||
- Issue #{{ snapshot.ref }}: {{ snapshot.subject }}
|
project=project.name|safe,
|
||||||
- Updated by {{ changer.get_full_name() }}
|
ref=snapshot.ref,
|
||||||
{% for entry in history_entries%}
|
subject=snapshot.subject|safe,
|
||||||
{% if entry.comment %}
|
url=resolve_front_url("issue", project.slug, snapshot.ref) %}
|
||||||
Comment: {{ entry.comment|linebreaksbr }}
|
Issue updated
|
||||||
{% endif %}
|
Hello {{ user }}, {{ changer }} has updated an issue on {{ project }}
|
||||||
{% set changed_fields = entry.values_diff %}
|
See issue #{{ ref }} {{ subject }} at {{ url }}
|
||||||
{% if changed_fields %}
|
{% endtrans %}
|
||||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
{% endblock %}
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
|
||||||
|
|
|
@ -1 +1,5 @@
|
||||||
[{{ project.name|safe }}] Updated the issue #{{ snapshot.ref|safe }} "{{ snapshot.subject|safe }}"
|
{% trans project=project.name|safe,
|
||||||
|
ref=snapshot.ref,
|
||||||
|
subject=snapshot.subject|safe %}
|
||||||
|
[{{ project }}] Updated the issue #{{ ref }} "{{ subject }}"
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
{% extends "emails/base.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% set final_url = resolve_front_url("issue", project.slug, snapshot.ref) %}
|
|
||||||
{% set final_url_name = "Taiga - View issue #{0}".format(snapshot.ref) %}
|
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
{% trans user=user.get_full_name()|safe,
|
||||||
<tr>
|
changer=changer.get_full_name()|safe,
|
||||||
<td>
|
project=project.name|safe,
|
||||||
<h1>Project: {{ project.name }}</h1>
|
ref=snapshot.ref,
|
||||||
<h2>Issue #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
subject=snapshot.subject|safe,
|
||||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
url=resolve_front_url("issue", project.slug, snapshot.ref) %}
|
||||||
</td>
|
<h1>New issue created</h1>
|
||||||
</tr>
|
<p>Hello {{ user }},<br />{{ changer }} has created a new issue on {{ project }}</p>
|
||||||
</table>
|
<p>Issue #{{ ref }} {{ subject }}</p>
|
||||||
{% endblock %}
|
<a class="button" href="{{ url }}" title="See Issue #{{ ref }} {{ subject }}">See issue</a>
|
||||||
{% block footer %}
|
<p><small>The Taiga Team</small></p>
|
||||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
{% endtrans %}
|
||||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
{% set final_url = resolve_front_url("issue", project.slug, snapshot.ref) %}
|
{% trans user=user.get_full_name()|safe,
|
||||||
{% set final_url_name = "Taiga - View issue #{0}".format(snapshot.ref) %}
|
changer=changer.get_full_name()|safe,
|
||||||
|
project=project.name|safe,
|
||||||
|
ref=snapshot.ref,
|
||||||
|
subject=snapshot.subject|safe,
|
||||||
|
url=resolve_front_url("issue", project.slug, snapshot.ref) %}
|
||||||
|
New issue created
|
||||||
|
Hello {{ user }}, {{ changer }} has created a new issue on {{ project }}
|
||||||
|
See issue #{{ ref }} {{ subject }} at {{ url }}
|
||||||
|
|
||||||
- Project: {{ project.name }}
|
---
|
||||||
- US #{{ snapshot.ref }}: {{ snapshot.subject }}
|
The Taiga Team
|
||||||
- Created by {{ changer.get_full_name() }}
|
{% endtrans %}
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
|
||||||
|
|
|
@ -1 +1,5 @@
|
||||||
[{{ project.name|safe }}] Created the issue #{{ snapshot.ref|safe }} "{{ snapshot.subject|safe }}"
|
{% trans project=project.name|safe,
|
||||||
|
ref=snapshot.ref,
|
||||||
|
subject=snapshot.subject|safe %}
|
||||||
|
[{{ project }}] Created the issue #{{ ref }} "{{ subject }}"
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
{% extends "emails/base.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
{% trans user=user.get_full_name()|safe,
|
||||||
<tr>
|
changer=changer.get_full_name()|safe,
|
||||||
<td>
|
project=project.name|safe,
|
||||||
<h1>{{ project.name }}</h1>
|
ref=snapshot.ref,
|
||||||
<h2>Issue #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
subject=snapshot.subject|safe %}
|
||||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
<h1>Issue deleted</h1>
|
||||||
</td>
|
<p>Hello {{ user }},<br />{{ changer }} has deleted an issue on {{ project }}</p>
|
||||||
</tr>
|
<p>Issue #{{ ref }} {{ subject }}</p>
|
||||||
</table>
|
<p><small>The Taiga Team</small></p>
|
||||||
|
{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
- Project: {{ project.name }}
|
{% trans user=user.get_full_name()|safe,
|
||||||
- Issue #{{ snapshot.ref }}: {{ snapshot.subject }}
|
changer=changer.get_full_name()|safe,
|
||||||
- Deleted by {{ changer.get_full_name() }}
|
project=project.name|safe,
|
||||||
|
ref=snapshot.ref,
|
||||||
|
subject=snapshot.subject|safe %}
|
||||||
|
Issue deleted
|
||||||
|
Hello {{ user }}, {{ changer }} has deleted an issue on {{ project }}
|
||||||
|
Issue #{{ ref }} {{ subject }}
|
||||||
|
|
||||||
|
---
|
||||||
|
The Taiga Team
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1 +1,5 @@
|
||||||
[{{ project.name|safe }}] Deleted the issue #{{ snapshot.ref|safe }} "{{ snapshot.subject|safe }}"
|
{% trans project=project.name|safe,
|
||||||
|
ref=snapshot.ref,
|
||||||
|
subject=snapshot.subject|safe %}
|
||||||
|
[{{ project }}] Deleted the issue #{{ ref }} "{{ subject }}"
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1,30 +1,14 @@
|
||||||
{% extends "emails/base.jinja" %}
|
{% extends "emails/updates-body-html.jinja" %}
|
||||||
|
|
||||||
{% set final_url = resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
{% block head %}
|
||||||
{% set final_url_name = "Taiga - View milestone #{0}".format(snapshot.slug) %}
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
changer=changer.get_full_name()|safe,
|
||||||
{% block body %}
|
project=project.name|safe,
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
name=snapshot.name|safe,
|
||||||
<tr>
|
url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
||||||
<td>
|
<h1>Sprint updated</h1>
|
||||||
<h1>Project: {{ project.name }}</h1>
|
<p>Hello {{ user }}, <br> {{ changer }} has updated an sprint on {{ project }}</p>
|
||||||
<h2>Milestone #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
<p>Sprint {{ name }}</p>
|
||||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
<a class="button" href="{{ url }}" title="See Sprint: {{ name }} in Taiga">See sprint</a>
|
||||||
{% for entry in history_entries%}
|
{% endtrans %}
|
||||||
{% if entry.comment %}
|
|
||||||
<p>Comment <b>{{ entry.comment|linebreaksbr }}</b></p>
|
|
||||||
{% endif %}
|
|
||||||
{% set changed_fields = entry.values_diff %}
|
|
||||||
{% if changed_fields %}
|
|
||||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
||||||
{% block footer %}
|
|
||||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
|
||||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
{% set final_url = resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
{% extends "emails/updates-body-text.jinja" %}
|
||||||
{% set final_url_name = "Taiga - View milestone #{0}".format(snapshot.slug) %}
|
{% block head %}
|
||||||
|
{% trans user=user.get_full_name()|safe,
|
||||||
- Project: {{ project.name }}
|
changer=changer.get_full_name()|safe,
|
||||||
- Milestone #{{ snapshot.slug }}: {{ snapshot.name }}
|
project=project.name|safe,
|
||||||
- Updated by {{ changer.get_full_name() }}
|
name=snapshot.name|safe,
|
||||||
{% for entry in history_entries%}
|
url=resolve_front_url("task", project.slug, snapshot.slug) %}
|
||||||
{% if entry.comment %}
|
Sprint updated
|
||||||
Comment: {{ entry.comment|linebreaksbr }}
|
Hello {{ user }}, {{ changer }} has updated a sprint on {{ project }}
|
||||||
{% endif %}
|
See sprint {{ subject }} at {{ url }}
|
||||||
{% set changed_fields = entry.values_diff %}
|
{% endtrans %}
|
||||||
{% if changed_fields %}
|
{% endblock %}
|
||||||
{% include "emails/includes/fields_diff-text.jinja" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
[{{ project.name|safe }}] Updated the milestone #{{ snapshot.slug|safe }} "{{ snapshot.name|safe }}"
|
{% trans project=project.name|safe,
|
||||||
|
milestone=snapshot.name|safe %}
|
||||||
|
[{{ project }}] Updated the sprint "{{ milestone }}"
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
{% extends "emails/base.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% set final_url = resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
|
||||||
{% set final_url_name = "Taiga - View milestone #{0}".format(snapshot.slug) %}
|
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
{% trans user=user.get_full_name()|safe,
|
||||||
<tr>
|
changer=changer.get_full_name()|safe,
|
||||||
<td>
|
project=project.name|safe,
|
||||||
<h1>Project: {{ project.name }}</h1>
|
name=snapshot.name|safe,
|
||||||
<h2>Milestone #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
||||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
<h1>New sprint created</h1>
|
||||||
</td>
|
<p>Hello {{ user }},<br />{{ changer }} has created a new sprint on {{ project }}</p>
|
||||||
</tr>
|
<p>Sprint {{ name }}</p>
|
||||||
</table>
|
<a class="button" href="{{ url }}" title="See Sprint {{ name }}">See sprint</a>
|
||||||
{% endblock %}
|
<p><small>The Taiga Team</small></p>
|
||||||
{% block footer %}
|
{% endtrans %}
|
||||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
|
||||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
{% set final_url = resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
{% trans user=user.get_full_name()|safe,
|
||||||
{% set final_url_name = "Taiga - View milestone #{0}".format(snapshot.slug) %}
|
changer=changer.get_full_name()|safe,
|
||||||
|
project=project.name|safe,
|
||||||
|
name=snapshot.name|safe,
|
||||||
|
url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}
|
||||||
|
New sprint created
|
||||||
|
Hello {{ user }}, {{ changer }} has created a new sprint on {{ project }}
|
||||||
|
See sprint {{ subject }} at {{ url }}
|
||||||
|
|
||||||
- Project: {{ project.name }}
|
---
|
||||||
- Milestone #{{ snapshot.slug }}: {{ snapshot.name }}
|
The Taiga Team
|
||||||
- Created by {{ changer.get_full_name() }}
|
{% endtrans %}
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
[{{ project.name|safe }}] Created the milestone #{{ snapshot.slug|safe }} "{{ snapshot.name|safe }}"
|
{% trans project=project.name|safe,
|
||||||
|
milestone=snapshot.name|safe %}
|
||||||
|
[{{ project }}] Created the sprint "{{ milestone }}"
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
{% extends "emails/base.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
{% trans user=user.get_full_name()|safe,
|
||||||
<tr>
|
changer=changer.get_full_name()|safe,
|
||||||
<td>
|
project=project.name|safe,
|
||||||
<h1>{{ project.name }}</h1>
|
ref=snapshot.ref,
|
||||||
<h2>Milestone #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
name=snapshot.name|safe %}
|
||||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
<h1>Sprint deleted</h1>
|
||||||
</td>
|
<p>Hello {{ user }},<br />{{ changer }} has deleted an sprint on {{ project }}</p>
|
||||||
</tr>
|
<p>Sprint {{ name }}</p>
|
||||||
</table>
|
<p><small>The Taiga Team</small></p>
|
||||||
|
{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
- Project: {{ project.name }}
|
{% trans user=user.get_full_name()|safe,
|
||||||
- Milestone #{{ snapshot.slug }}: {{ snapshot.name }}
|
changer=changer.get_full_name()|safe,
|
||||||
- Deleted by {{ changer.get_full_name() }}
|
project=project.name|safe,
|
||||||
|
name=snapshot.name|safe %}
|
||||||
|
Sprint deleted
|
||||||
|
Hello {{ user }}, {{ changer }} has deleted an sprint on {{ project }}
|
||||||
|
Sprint {{ name }}
|
||||||
|
|
||||||
|
---
|
||||||
|
The Taiga Team
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
[{{ project.name|safe }}] Deleted the milestone #{{ snapshot.slug|safe }} "{{ snapshot.name|safe }}"
|
{% trans project=project.name|safe,
|
||||||
|
milestone=snapshot.name|safe %}
|
||||||
|
[{{ project }}] Deleted the Sprint "{{ milestone }}"
|
||||||
|
{% endtrans %}
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
{% extends "emails/base.jinja" %}
|
|
||||||
|
|
||||||
{% set final_url = resolve_front_url("project-admin", snapshot.slug) %}
|
|
||||||
{% set final_url_name = "Taiga - View Project #{0}".format(snapshot.slug) %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<h2>Project #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
|
||||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
|
||||||
{% for entry in history_entries%}
|
|
||||||
{% if entry.comment %}
|
|
||||||
<p>Comment <b>{{ entry.comment|linebreaksbr }}</b></p>
|
|
||||||
{% endif %}
|
|
||||||
{% set changed_fields = entry.values_diff %}
|
|
||||||
{% if changed_fields %}
|
|
||||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
||||||
{% block footer %}
|
|
||||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
|
||||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% set final_url = resolve_front_url("project-admin", snapshot.slug) %}
|
|
||||||
{% set final_url_name = "Taiga - View Project #{0}".format(snapshot.slug) %}
|
|
||||||
|
|
||||||
- Project #{{ snapshot.slug }}: {{ snapshot.name }}
|
|
||||||
- Updated by {{ changer.get_full_name() }}
|
|
||||||
{% for entry in history_entries%}
|
|
||||||
{% if entry.comment %}
|
|
||||||
Comment: {{ entry.comment|linebreaksbr }}
|
|
||||||
{% endif %}
|
|
||||||
{% set changed_fields = entry.values_diff %}
|
|
||||||
{% for field_name, values in changed_fields.items() %}
|
|
||||||
* {{ verbose_name(object, field_name) }}</b>: from '{{ values.0 }}' to '{{ values.1 }}'.
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
|
|
@ -1 +0,0 @@
|
||||||
[{{ snapshot.name|safe }}] Updated the project #{{ snapshot.slug|safe }}
|
|
|
@ -1,19 +0,0 @@
|
||||||
{% set final_url = resolve_front_url("project-admin", snapshot.slug) %}
|
|
||||||
{% set final_url_name = "Taiga - View Project #{0}".format(snapshot.slug) %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<h1>Project: {{ project.name }}</h1>
|
|
||||||
<h2>Project #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
|
||||||
<p>Created by <b>{{ changer.get_full_name() }}</b>.</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
||||||
{% block footer %}
|
|
||||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
|
||||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{% set final_url = resolve_front_url("project-admin", snapshot.slug) %}
|
|
||||||
{% set final_url_name = "Taiga - View Project #{0}".format(snapshot.slug) %}
|
|
||||||
|
|
||||||
- Project #{{ snapshot.slug }}: {{ snapshot.name }}
|
|
||||||
- Created by {{ changer.get_full_name() }}
|
|
||||||
|
|
||||||
** More info at {{ final_url_name }} ({{ final_url }}) **
|
|
|
@ -1 +0,0 @@
|
||||||
[{{ snapshot.name|safe }}] Created the project #{{ snapshot.slug|safe }}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{% extends "emails/base.jinja" %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<h2>Project #{{ snapshot.slug }}: {{ snapshot.name }}</h2>
|
|
||||||
<p>Deleted by <b>{{ changer.get_full_name() }}</b></p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
|
@ -1,2 +0,0 @@
|
||||||
- Project #{{ snapshot.slug }}: {{ snapshot.name }}
|
|
||||||
- Deleted by {{ changer.get_full_name() }}
|
|
|
@ -1 +0,0 @@
|
||||||
[{{ snapshot.name|safe }}] Deleted the project #{{ snapshot.slug|safe }}
|
|
|
@ -1,30 +1,15 @@
|
||||||
{% extends "emails/base.jinja" %}
|
{% extends "emails/updates-body-html.jinja" %}
|
||||||
|
|
||||||
{% set final_url = resolve_front_url("task", project.slug, snapshot.ref) %}
|
{% block head %}
|
||||||
{% set final_url_name = "Taiga - View task #{0}".format(snapshot.ref) %}
|
{% trans user=user.get_full_name()|safe,
|
||||||
|
changer=changer.get_full_name()|safe,
|
||||||
{% block body %}
|
project=project.name|safe,
|
||||||
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="table-body">
|
ref=snapshot.ref,
|
||||||
<tr>
|
subject=snapshot.subject|safe,
|
||||||
<td>
|
url=resolve_front_url("task", project.slug, snapshot.ref) %}
|
||||||
<h1>Project: {{ project.name }}</h1>
|
<h1>Task updated</h1>
|
||||||
<h2>Task #{{ snapshot.ref }}: {{ snapshot.subject }}</h2>
|
<p>Hello {{ user }}, <br> {{ changer }} has updated a task on {{ project }}</p>
|
||||||
<p>Updated by <b>{{ changer.get_full_name() }}</b>.</p>
|
<p>Task #{{ ref }} {{ subject }}</p>
|
||||||
{% for entry in history_entries%}
|
<a class="button" href="{{ url }}" title="See Task #{{ ref }}: {{ subject }} in Taiga">See task</a>
|
||||||
{% if entry.comment %}
|
{% endtrans %}
|
||||||
<p>Comment <b>{{ mdrender(project, entry.comment) }}</b></p>
|
|
||||||
{% endif %}
|
|
||||||
{% set changed_fields = entry.values_diff %}
|
|
||||||
{% if changed_fields %}
|
|
||||||
{% include "emails/includes/fields_diff-html.jinja" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
||||||
{% block footer %}
|
|
||||||
<p style="padding: 10px; border-top: 1px solid #eee;">
|
|
||||||
More info at: <a href="{{ final_url }}" style="color: #666;">{{ final_url_name }}</a>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue