Merge branch 'master' into stable

Conflicts:
	taiga/projects/serializers.py
remotes/origin/enhancement/email-actions 1.5.0
Jesús Espino 2015-01-29 11:50:03 +01:00
commit be710a4545
187 changed files with 4508 additions and 1159 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ media
.coverage .coverage
.cache .cache
.\#* .\#*
.project

View File

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

View File

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

View File

@ -4,7 +4,7 @@
[![Travis Badge](https://img.shields.io/travis/taigaio/taiga-back.svg?style=flat)](https://travis-ci.org/taigaio/taiga-back "Travis Badge") [![Travis Badge](https://img.shields.io/travis/taigaio/taiga-back.svg?style=flat)](https://travis-ci.org/taigaio/taiga-back "Travis Badge")
[![Coveralls](http://img.shields.io/coveralls/taigaio/taiga-back.svg?style=flat)](https://travis-ci.org/taigaio/taiga-back "Coveralls") [![Coveralls](http://img.shields.io/coveralls/taigaio/taiga-back.svg?style=flat)](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 ##

View File

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

View File

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

29
settings/sr.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

19
taiga/contrib_routers.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,19 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
{% trans project=project.name|safe %}[{{ project }}] Your project dump has been generated{% endtrans %}

View File

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

View File

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

View File

@ -0,0 +1 @@
{% trans error_subject=error_subject, project=project.name|safe %}[{{ project }}] {{ error_subject }}{% endtrans %}

View File

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

View File

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

View File

@ -0,0 +1 @@
{% trans error_subject=error_subject %}[Taiga] {{ error_subject }}{% endtrans %}

View File

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

View File

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

View File

@ -0,0 +1 @@
{% trans project=project.name|safe %}[{{ project }}] Your project dump has been imported{% endtrans %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
[{{ snapshot.name|safe }}] Updated the project #{{ snapshot.slug|safe }}

View File

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

View File

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

View File

@ -1 +0,0 @@
[{{ snapshot.name|safe }}] Created the project #{{ snapshot.slug|safe }}

View File

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

View File

@ -1,2 +0,0 @@
- Project #{{ snapshot.slug }}: {{ snapshot.name }}
- Deleted by {{ changer.get_full_name() }}

View File

@ -1 +0,0 @@
[{{ snapshot.name|safe }}] Deleted the project #{{ snapshot.slug|safe }}

View File

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