Merge remote-tracking branch 'upstream/master'
commit
edfb98de72
|
@ -1,4 +1,5 @@
|
||||||
.*.sw*
|
.*.sw*
|
||||||
|
.#*
|
||||||
*.log
|
*.log
|
||||||
taiga/search
|
taiga/search
|
||||||
settings/local.py
|
settings/local.py
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
# Changelog #
|
# Changelog #
|
||||||
|
|
||||||
|
|
||||||
## 1.8.0 ??? (unreleased)
|
## 1.8.0 Saracenia Purpurea (unreleased)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Improve timeline resource.
|
- Improve timeline resource.
|
||||||
- Add sitemap of taiga-front (the web client).
|
- Add sitemap of taiga-front (the web client).
|
||||||
- Search by reference (thanks to [@artlepool](https://github.com/artlepool))
|
- Search by reference (thanks to [@artlepool](https://github.com/artlepool))
|
||||||
- Add call 'by_username' to the API resource User
|
- Add call 'by_username' to the API resource User
|
||||||
|
- i18n.
|
||||||
|
- Add deutsch (de) translation.
|
||||||
|
- Add nederlands (nl) translation.
|
||||||
|
|
||||||
### Misc
|
### Misc
|
||||||
- Lots of small and not so small bugfixes.
|
- Lots of small and not so small bugfixes.
|
||||||
|
|
|
@ -18,3 +18,5 @@ echo "-> Load initial roles"
|
||||||
python manage.py loaddata initial_role --traceback
|
python manage.py loaddata initial_role --traceback
|
||||||
echo "-> Generate sample data"
|
echo "-> Generate sample data"
|
||||||
python manage.py sample_data --traceback
|
python manage.py sample_data --traceback
|
||||||
|
echo "-> Rebuilding timeline"
|
||||||
|
python manage.py rebuild_timeline --purge
|
||||||
|
|
|
@ -78,7 +78,7 @@ LANGUAGES = [
|
||||||
#("cs", "Čeština"), # Czech
|
#("cs", "Čeština"), # Czech
|
||||||
#("cy", "Cymraeg"), # Welsh
|
#("cy", "Cymraeg"), # Welsh
|
||||||
#("da", "Dansk"), # Danish
|
#("da", "Dansk"), # Danish
|
||||||
#("de", "Deutsch"), # German
|
("de", "Deutsch"), # German
|
||||||
#("el", "Ελληνικά"), # Greek
|
#("el", "Ελληνικά"), # Greek
|
||||||
("en", "English (US)"), # English
|
("en", "English (US)"), # English
|
||||||
#("en-au", "English (Australia)"), # Australian English
|
#("en-au", "English (Australia)"), # Australian English
|
||||||
|
@ -122,7 +122,7 @@ LANGUAGES = [
|
||||||
#("my", "မြန်မာ"), # Burmese
|
#("my", "မြန်မာ"), # Burmese
|
||||||
#("nb", "Norsk (bokmål)"), # Norwegian Bokmal
|
#("nb", "Norsk (bokmål)"), # Norwegian Bokmal
|
||||||
#("ne", "नेपाली"), # Nepali
|
#("ne", "नेपाली"), # Nepali
|
||||||
#("nl", "Nederlands"), # Dutch
|
("nl", "Nederlands"), # Dutch
|
||||||
#("nn", "Norsk (nynorsk)"), # Norwegian Nynorsk
|
#("nn", "Norsk (nynorsk)"), # Norwegian Nynorsk
|
||||||
#("os", "Ирон æвзаг"), # Ossetic
|
#("os", "Ирон æвзаг"), # Ossetic
|
||||||
#("pa", "ਪੰਜਾਬੀ"), # Punjabi
|
#("pa", "ਪੰਜਾਬੀ"), # Punjabi
|
||||||
|
|
|
@ -25,8 +25,8 @@ COORS_ALLOWED_HEADERS = ["content-type", "x-requested-with",
|
||||||
"x-session-id"]
|
"x-session-id"]
|
||||||
COORS_ALLOWED_CREDENTIALS = True
|
COORS_ALLOWED_CREDENTIALS = True
|
||||||
COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", "x-paginated-by",
|
COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", "x-paginated-by",
|
||||||
"x-paginated-by", "x-pagination-current", "x-site-host",
|
"x-pagination-current", "x-pagination-next", "x-pagination-prev",
|
||||||
"x-site-register"]
|
"x-site-host", "x-site-register"]
|
||||||
|
|
||||||
|
|
||||||
class CoorsMiddleware(object):
|
class CoorsMiddleware(object):
|
||||||
|
|
|
@ -379,7 +379,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
|
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0" alink="#699b05" link="#699b05" bgcolor="#FFFFFF" text="#444444">
|
||||||
<center>
|
<center>
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -340,7 +340,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
|
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0" alink="#699b05" link="#699b05" bgcolor="#FFFFFF" text="#444444">
|
||||||
<center>
|
<center>
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -379,7 +379,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
|
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0" alink="#699b05" link="#699b05" bgcolor="#FFFFFF" text="#444444">
|
||||||
<center>
|
<center>
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -241,7 +241,9 @@ class HistoryExportSerializerMixin(serializers.ModelSerializer):
|
||||||
history = serializers.SerializerMethodField("get_history")
|
history = serializers.SerializerMethodField("get_history")
|
||||||
|
|
||||||
def get_history(self, obj):
|
def get_history(self, obj):
|
||||||
history_qs = history_service.get_history_queryset_by_model_instance(obj)
|
history_qs = history_service.get_history_queryset_by_model_instance(obj,
|
||||||
|
types=(history_models.HistoryType.change, history_models.HistoryType.create,))
|
||||||
|
|
||||||
return HistoryExportSerializer(history_qs, many=True).data
|
return HistoryExportSerializer(history_qs, many=True).data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ from django.template.defaultfilters import slugify
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from taiga.projects.history.services import make_key_from_model_object
|
from taiga.projects.history.services import make_key_from_model_object, take_snapshot
|
||||||
from taiga.timeline.service import build_project_namespace
|
from taiga.timeline.service import build_project_namespace
|
||||||
from taiga.projects.references import sequences as seq
|
from taiga.projects.references import sequences as seq
|
||||||
from taiga.projects.references import models as refs
|
from taiga.projects.references import models as refs
|
||||||
|
@ -229,9 +229,13 @@ def store_task(project, data):
|
||||||
for task_attachment in data.get("attachments", []):
|
for task_attachment in data.get("attachments", []):
|
||||||
store_attachment(project, serialized.object, task_attachment)
|
store_attachment(project, serialized.object, task_attachment)
|
||||||
|
|
||||||
for history in data.get("history", []):
|
history_entries = data.get("history", [])
|
||||||
|
for history in history_entries:
|
||||||
store_history(project, serialized.object, history)
|
store_history(project, serialized.object, history)
|
||||||
|
|
||||||
|
if not history_entries:
|
||||||
|
take_snapshot(serialized.object, user=serialized.object.owner)
|
||||||
|
|
||||||
custom_attributes_values = data.get("custom_attributes_values", None)
|
custom_attributes_values = data.get("custom_attributes_values", None)
|
||||||
if custom_attributes_values:
|
if custom_attributes_values:
|
||||||
custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name')
|
custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name')
|
||||||
|
@ -319,9 +323,13 @@ def store_wiki_page(project, wiki_page):
|
||||||
for attachment in wiki_page.get("attachments", []):
|
for attachment in wiki_page.get("attachments", []):
|
||||||
store_attachment(project, serialized.object, attachment)
|
store_attachment(project, serialized.object, attachment)
|
||||||
|
|
||||||
for history in wiki_page.get("history", []):
|
history_entries = wiki_page.get("history", [])
|
||||||
|
for history in history_entries:
|
||||||
store_history(project, serialized.object, history)
|
store_history(project, serialized.object, history)
|
||||||
|
|
||||||
|
if not history_entries:
|
||||||
|
take_snapshot(serialized.object, user=serialized.object.owner)
|
||||||
|
|
||||||
return serialized
|
return serialized
|
||||||
|
|
||||||
add_errors("wiki_pages", serialized.errors)
|
add_errors("wiki_pages", serialized.errors)
|
||||||
|
@ -381,9 +389,13 @@ def store_user_story(project, data):
|
||||||
for role_point in data.get("role_points", []):
|
for role_point in data.get("role_points", []):
|
||||||
store_role_point(project, serialized.object, role_point)
|
store_role_point(project, serialized.object, role_point)
|
||||||
|
|
||||||
for history in data.get("history", []):
|
history_entries = data.get("history", [])
|
||||||
|
for history in history_entries:
|
||||||
store_history(project, serialized.object, history)
|
store_history(project, serialized.object, history)
|
||||||
|
|
||||||
|
if not history_entries:
|
||||||
|
take_snapshot(serialized.object, user=serialized.object.owner)
|
||||||
|
|
||||||
custom_attributes_values = data.get("custom_attributes_values", None)
|
custom_attributes_values = data.get("custom_attributes_values", None)
|
||||||
if custom_attributes_values:
|
if custom_attributes_values:
|
||||||
custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name')
|
custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name')
|
||||||
|
@ -434,9 +446,13 @@ def store_issue(project, data):
|
||||||
for attachment in data.get("attachments", []):
|
for attachment in data.get("attachments", []):
|
||||||
store_attachment(project, serialized.object, attachment)
|
store_attachment(project, serialized.object, attachment)
|
||||||
|
|
||||||
for history in data.get("history", []):
|
history_entries = data.get("history", [])
|
||||||
|
for history in history_entries:
|
||||||
store_history(project, serialized.object, history)
|
store_history(project, serialized.object, history)
|
||||||
|
|
||||||
|
if not history_entries:
|
||||||
|
take_snapshot(serialized.object, user=serialized.object.owner)
|
||||||
|
|
||||||
custom_attributes_values = data.get("custom_attributes_values", None)
|
custom_attributes_values = data.get("custom_attributes_values", None)
|
||||||
if custom_attributes_values:
|
if custom_attributes_values:
|
||||||
custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name')
|
custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name')
|
||||||
|
|
|
@ -9,7 +9,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-06-09 09:47+0200\n"
|
"POT-Creation-Date: 2015-06-15 12:34+0200\n"
|
||||||
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
||||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||||
"Language-Team: Catalan (http://www.transifex.com/projects/p/taiga-back/"
|
"Language-Team: Catalan (http://www.transifex.com/projects/p/taiga-back/"
|
||||||
|
@ -2554,7 +2554,7 @@ msgstr ""
|
||||||
msgid "Stakeholder"
|
msgid "Stakeholder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/userstories/api.py:173
|
#: taiga/projects/userstories/api.py:174
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-06-09 09:47+0200\n"
|
"POT-Creation-Date: 2015-06-15 12:34+0200\n"
|
||||||
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
|
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
|
||||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||||
"Language-Team: Taiga Dev Team <support@taiga.io>\n"
|
"Language-Team: Taiga Dev Team <support@taiga.io>\n"
|
||||||
|
@ -2503,7 +2503,7 @@ msgstr ""
|
||||||
msgid "Stakeholder"
|
msgid "Stakeholder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/userstories/api.py:173
|
#: taiga/projects/userstories/api.py:174
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||||
|
|
|
@ -12,9 +12,9 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-06-09 09:47+0200\n"
|
"POT-Creation-Date: 2015-06-15 12:34+0200\n"
|
||||||
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
"PO-Revision-Date: 2015-06-17 07:24+0000\n"
|
||||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
"Last-Translator: David Barragán <bameda@gmail.com>\n"
|
||||||
"Language-Team: Spanish (http://www.transifex.com/projects/p/taiga-back/"
|
"Language-Team: Spanish (http://www.transifex.com/projects/p/taiga-back/"
|
||||||
"language/es/)\n"
|
"language/es/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -477,6 +477,9 @@ msgid ""
|
||||||
"%(comment)s</p>\n"
|
"%(comment)s</p>\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"<h3>comentario:</h3>\n"
|
||||||
|
"<p>%(comment)s</p>"
|
||||||
|
|
||||||
#: taiga/base/templates/emails/updates-body-text.jinja:6
|
#: taiga/base/templates/emails/updates-body-text.jinja:6
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -851,7 +854,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>Feedback</h1>\n"
|
"<h1>Feedback</h1>\n"
|
||||||
"<p>Taiga ha recivido feedback de %(full_name)s <%(email)s></p>"
|
"<p>Taiga ha recibido feedback de %(full_name)s <%(email)s></p>"
|
||||||
|
|
||||||
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9
|
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -2477,7 +2480,7 @@ msgstr "La versión debe ser un número entero"
|
||||||
|
|
||||||
#: taiga/projects/occ/mixins.py:58
|
#: taiga/projects/occ/mixins.py:58
|
||||||
msgid "The version parameter is not valid"
|
msgid "The version parameter is not valid"
|
||||||
msgstr ""
|
msgstr "La versión no es válida"
|
||||||
|
|
||||||
#: taiga/projects/occ/mixins.py:74
|
#: taiga/projects/occ/mixins.py:74
|
||||||
msgid "The version doesn't match with the current one"
|
msgid "The version doesn't match with the current one"
|
||||||
|
@ -2961,7 +2964,7 @@ msgstr "Product Owner"
|
||||||
msgid "Stakeholder"
|
msgid "Stakeholder"
|
||||||
msgstr "Stakeholder"
|
msgstr "Stakeholder"
|
||||||
|
|
||||||
#: taiga/projects/userstories/api.py:173
|
#: taiga/projects/userstories/api.py:174
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||||
|
@ -3165,7 +3168,7 @@ msgstr "idioma por defecto"
|
||||||
|
|
||||||
#: taiga/users/models.py:122
|
#: taiga/users/models.py:122
|
||||||
msgid "default theme"
|
msgid "default theme"
|
||||||
msgstr ""
|
msgstr "tema por defecto"
|
||||||
|
|
||||||
#: taiga/users/models.py:124
|
#: taiga/users/models.py:124
|
||||||
msgid "default timezone"
|
msgid "default timezone"
|
||||||
|
|
|
@ -9,7 +9,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-06-09 09:47+0200\n"
|
"POT-Creation-Date: 2015-06-15 12:34+0200\n"
|
||||||
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
||||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||||
"Language-Team: Finnish (http://www.transifex.com/projects/p/taiga-back/"
|
"Language-Team: Finnish (http://www.transifex.com/projects/p/taiga-back/"
|
||||||
|
@ -2954,7 +2954,7 @@ msgstr "Tuoteomistaja"
|
||||||
msgid "Stakeholder"
|
msgid "Stakeholder"
|
||||||
msgstr "Sidosryhmä"
|
msgstr "Sidosryhmä"
|
||||||
|
|
||||||
#: taiga/projects/userstories/api.py:173
|
#: taiga/projects/userstories/api.py:174
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||||
|
|
|
@ -8,15 +8,16 @@
|
||||||
# Florent B. <me@kxrz.me>, 2015
|
# Florent B. <me@kxrz.me>, 2015
|
||||||
# Louis-Michel Couture <louim_1@hotmail.com>, 2015
|
# Louis-Michel Couture <louim_1@hotmail.com>, 2015
|
||||||
# Matthieu Durocher <matthieu@technocyclope.com>, 2015
|
# Matthieu Durocher <matthieu@technocyclope.com>, 2015
|
||||||
|
# Nlko <nospam1@thomasson.fr>, 2015
|
||||||
# Stéphane Mor <stephanemor@gmail.com>, 2015
|
# Stéphane Mor <stephanemor@gmail.com>, 2015
|
||||||
# William Godin <williamgodin@gmail.com>, 2015
|
# William Godin <williamgodin@gmail.com>, 2015
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-06-09 09:47+0200\n"
|
"POT-Creation-Date: 2015-06-15 12:34+0200\n"
|
||||||
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
"PO-Revision-Date: 2015-06-12 21:30+0000\n"
|
||||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
"Last-Translator: Nlko <nospam1@thomasson.fr>\n"
|
||||||
"Language-Team: French (http://www.transifex.com/projects/p/taiga-back/"
|
"Language-Team: French (http://www.transifex.com/projects/p/taiga-back/"
|
||||||
"language/fr/)\n"
|
"language/fr/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -469,6 +470,9 @@ msgid ""
|
||||||
"%(comment)s</p>\n"
|
"%(comment)s</p>\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"<h3>commentaire:</h3>\n"
|
||||||
|
"<p>%(comment)s</p>"
|
||||||
|
|
||||||
#: taiga/base/templates/emails/updates-body-text.jinja:6
|
#: taiga/base/templates/emails/updates-body-text.jinja:6
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -894,6 +898,9 @@ msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"{message}"
|
"{message}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Commentaire provenant de GitHub:\n"
|
||||||
|
"\n"
|
||||||
|
"{message}"
|
||||||
|
|
||||||
#: taiga/hooks/gitlab/event_hooks.py:87
|
#: taiga/hooks/gitlab/event_hooks.py:87
|
||||||
msgid "Status changed from GitLab commit"
|
msgid "Status changed from GitLab commit"
|
||||||
|
@ -1800,6 +1807,8 @@ msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"[%(project)s] Created the sprint \"%(milestone)s\"\n"
|
"[%(project)s] Created the sprint \"%(milestone)s\"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"[%(project)s] Sprint \"%(milestone)s\" créé\n"
|
||||||
|
|
||||||
#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4
|
#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -1831,6 +1840,8 @@ msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n"
|
"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"[%(project)s] Sprint \"%(milestone)s\" Éffacé\n"
|
||||||
|
|
||||||
#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4
|
#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -1860,6 +1871,8 @@ msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n"
|
"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"[%(project)s] Tâche #%(ref)s \"%(subject)s\" mise à jour\n"
|
||||||
|
|
||||||
#: taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja:4
|
#: taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja:4
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -1893,6 +1906,8 @@ msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n"
|
"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"[%(project)s] Tâche #%(ref)s \"%(subject)s\" créée\n"
|
||||||
|
|
||||||
#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja:4
|
#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja:4
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -1924,6 +1939,8 @@ msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n"
|
"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"[%(project)s] Tâche #%(ref)s \"%(subject)s\" supprimée\n"
|
||||||
|
|
||||||
#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja:4
|
#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja:4
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -1953,6 +1970,8 @@ msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n"
|
"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"[%(project)s] US #%(ref)s \"%(subject)s\" mise à jour\n"
|
||||||
|
|
||||||
#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4
|
#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -2594,7 +2613,7 @@ msgstr "Product Owner"
|
||||||
msgid "Stakeholder"
|
msgid "Stakeholder"
|
||||||
msgstr "Participant"
|
msgstr "Participant"
|
||||||
|
|
||||||
#: taiga/projects/userstories/api.py:173
|
#: taiga/projects/userstories/api.py:174
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,7 +11,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-06-09 09:47+0200\n"
|
"POT-Creation-Date: 2015-06-15 12:34+0200\n"
|
||||||
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
||||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||||
"Language-Team: Chinese Traditional (http://www.transifex.com/projects/p/"
|
"Language-Team: Chinese Traditional (http://www.transifex.com/projects/p/"
|
||||||
|
@ -2946,7 +2946,7 @@ msgstr "產品所有人"
|
||||||
msgid "Stakeholder"
|
msgid "Stakeholder"
|
||||||
msgstr "利害關係人"
|
msgstr "利害關係人"
|
||||||
|
|
||||||
#: taiga/projects/userstories/api.py:173
|
#: taiga/projects/userstories/api.py:174
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||||
|
|
|
@ -98,6 +98,7 @@ class BasicUserStoryStatusSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.UserStoryStatus
|
model = models.UserStoryStatus
|
||||||
|
i18n_fields = ("name",)
|
||||||
fields = ("name", "color")
|
fields = ("name", "color")
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,6 +129,7 @@ class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.TaskStatus
|
model = models.TaskStatus
|
||||||
|
i18n_fields = ("name",)
|
||||||
fields = ("name", "color")
|
fields = ("name", "color")
|
||||||
|
|
||||||
|
|
||||||
|
@ -170,6 +172,7 @@ class BasicIssueStatusSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.IssueStatus
|
model = models.IssueStatus
|
||||||
|
i18n_fields = ("name",)
|
||||||
fields = ("name", "color")
|
fields = ("name", "color")
|
||||||
|
|
||||||
|
|
||||||
|
@ -273,6 +276,7 @@ class ProjectMembershipSerializer(serializers.ModelSerializer):
|
||||||
full_name = serializers.CharField(source='user.get_full_name', required=False)
|
full_name = serializers.CharField(source='user.get_full_name', required=False)
|
||||||
username = serializers.CharField(source='user.username', required=False)
|
username = serializers.CharField(source='user.username', required=False)
|
||||||
color = serializers.CharField(source='user.color', required=False)
|
color = serializers.CharField(source='user.color', required=False)
|
||||||
|
is_active = serializers.BooleanField(source='user.is_active', required=False)
|
||||||
photo = serializers.SerializerMethodField("get_photo")
|
photo = serializers.SerializerMethodField("get_photo")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -26,7 +26,7 @@ from taiga.projects.validators import ProjectExistsValidator
|
||||||
from taiga.projects.validators import UserStoryStatusExistsValidator
|
from taiga.projects.validators import UserStoryStatusExistsValidator
|
||||||
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.serializers import UserStoryStatusSerializer
|
from taiga.projects.serializers import BasicUserStoryStatusSerializer
|
||||||
from taiga.users.serializers import BasicInfoSerializer as UserBasicInfoSerializer
|
from taiga.users.serializers import BasicInfoSerializer as UserBasicInfoSerializer
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -53,7 +53,7 @@ class UserStorySerializer(WatchersValidator, serializers.ModelSerializer):
|
||||||
origin_issue = serializers.SerializerMethodField("get_origin_issue")
|
origin_issue = serializers.SerializerMethodField("get_origin_issue")
|
||||||
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
|
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
|
||||||
description_html = serializers.SerializerMethodField("get_description_html")
|
description_html = serializers.SerializerMethodField("get_description_html")
|
||||||
status_extra_info = UserStoryStatusSerializer(source="status", required=False, read_only=True)
|
status_extra_info = BasicUserStoryStatusSerializer(source="status", required=False, read_only=True)
|
||||||
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
|
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -32,3 +32,5 @@ class TimelineAppConfig(AppConfig):
|
||||||
sender=apps.get_model("projects", "Membership"))
|
sender=apps.get_model("projects", "Membership"))
|
||||||
signals.post_delete.connect(handlers.delete_membership_push_to_timeline,
|
signals.post_delete.connect(handlers.delete_membership_push_to_timeline,
|
||||||
sender=apps.get_model("projects", "Membership"))
|
sender=apps.get_model("projects", "Membership"))
|
||||||
|
signals.post_save.connect(handlers.create_user_push_to_timeline,
|
||||||
|
sender=apps.get_model("users", "User"))
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from taiga.timeline.models import Timeline
|
||||||
|
from taiga.projects.models import Project
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Regenerate unnecessary new memberships entry lines'
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
debug_enabled = settings.DEBUG
|
||||||
|
if debug_enabled:
|
||||||
|
print("Please, execute this script only with DEBUG mode disabled (DEBUG=False)")
|
||||||
|
return
|
||||||
|
|
||||||
|
removing_timeline_ids = []
|
||||||
|
for t in Timeline.objects.filter(event_type="projects.membership.create").order_by("created"):
|
||||||
|
print(t.created)
|
||||||
|
if t.project.owner.id == t.data["user"].get("id", None):
|
||||||
|
removing_timeline_ids.append(t.id)
|
||||||
|
|
||||||
|
Timeline.objects.filter(id__in=removing_timeline_ids).delete()
|
|
@ -46,9 +46,12 @@ class BulkCreator(object):
|
||||||
self.timeline_objects = []
|
self.timeline_objects = []
|
||||||
self.created = None
|
self.created = None
|
||||||
|
|
||||||
def createElement(self, element):
|
def create_element(self, element):
|
||||||
self.timeline_objects.append(element)
|
self.timeline_objects.append(element)
|
||||||
if len(self.timeline_objects) > 1000:
|
if len(self.timeline_objects) > 1000:
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
Timeline.objects.bulk_create(self.timeline_objects, batch_size=1000)
|
Timeline.objects.bulk_create(self.timeline_objects, batch_size=1000)
|
||||||
del self.timeline_objects
|
del self.timeline_objects
|
||||||
self.timeline_objects = []
|
self.timeline_objects = []
|
||||||
|
@ -63,7 +66,7 @@ def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, n
|
||||||
event_type_key = _get_impl_key_from_model(instance.__class__, event_type)
|
event_type_key = _get_impl_key_from_model(instance.__class__, event_type)
|
||||||
impl = _timeline_impl_map.get(event_type_key, None)
|
impl = _timeline_impl_map.get(event_type_key, None)
|
||||||
|
|
||||||
bulk_creator.createElement(Timeline(
|
bulk_creator.create_element(Timeline(
|
||||||
content_object=obj,
|
content_object=obj,
|
||||||
namespace=namespace,
|
namespace=namespace,
|
||||||
event_type=event_type_key,
|
event_type=event_type_key,
|
||||||
|
@ -74,13 +77,15 @@ def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, n
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def generate_timeline(initial_date, final_date):
|
def generate_timeline(initial_date, final_date, project_id):
|
||||||
if initial_date or final_date:
|
if initial_date or final_date or project_id:
|
||||||
timelines = Timeline.objects.all()
|
timelines = Timeline.objects.all()
|
||||||
if initial_date:
|
if initial_date:
|
||||||
timelines = timelines.filter(created__gte=initial_date)
|
timelines = timelines.filter(created__gte=initial_date)
|
||||||
if final_date:
|
if final_date:
|
||||||
timelines = timelines.filter(created__lt=final_date)
|
timelines = timelines.filter(created__lt=final_date)
|
||||||
|
if project_id:
|
||||||
|
timelines = timelines.filter(project__id=project_id)
|
||||||
|
|
||||||
timelines.delete()
|
timelines.delete()
|
||||||
|
|
||||||
|
@ -97,6 +102,22 @@ def generate_timeline(initial_date, final_date):
|
||||||
projects = projects.filter(created_date__lt=final_date)
|
projects = projects.filter(created_date__lt=final_date)
|
||||||
history_entries = history_entries.filter(created_at__lt=final_date)
|
history_entries = history_entries.filter(created_at__lt=final_date)
|
||||||
|
|
||||||
|
if project_id:
|
||||||
|
project = Project.objects.get(id=project_id)
|
||||||
|
us_keys = ['userstories.userstory:%s'%(id) for id in project.user_stories.values_list("id", flat=True)]
|
||||||
|
tasks_keys = ['tasks.task:%s'%(id) for id in project.tasks.values_list("id", flat=True)]
|
||||||
|
issue_keys = ['issues.issue:%s'%(id) for id in project.issues.values_list("id", flat=True)]
|
||||||
|
wiki_keys = ['wiki.wikipage:%s'%(id) for id in project.wiki_pages.values_list("id", flat=True)]
|
||||||
|
keys = us_keys + tasks_keys + issue_keys + wiki_keys
|
||||||
|
|
||||||
|
projects = projects.filter(id=project_id)
|
||||||
|
history_entries = history_entries.filter(key__in=keys)
|
||||||
|
|
||||||
|
#Memberships
|
||||||
|
for membership in project.memberships.exclude(user=None).exclude(user=project.owner):
|
||||||
|
bulk_creator.created = membership.created_at
|
||||||
|
_push_to_timelines(project, membership.user, membership, "create")
|
||||||
|
|
||||||
for project in projects.iterator():
|
for project in projects.iterator():
|
||||||
bulk_creator.created = project.created_date
|
bulk_creator.created = project.created_date
|
||||||
print("Project:", bulk_creator.created)
|
print("Project:", bulk_creator.created)
|
||||||
|
@ -115,6 +136,8 @@ def generate_timeline(initial_date, final_date):
|
||||||
except ObjectDoesNotExist as e:
|
except ObjectDoesNotExist as e:
|
||||||
print("Ignoring")
|
print("Ignoring")
|
||||||
|
|
||||||
|
bulk_creator.flush()
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Regenerate project timeline'
|
help = 'Regenerate project timeline'
|
||||||
|
@ -136,6 +159,12 @@ class Command(BaseCommand):
|
||||||
dest='final_date',
|
dest='final_date',
|
||||||
default=None,
|
default=None,
|
||||||
help='Final date for timeline generation'),
|
help='Final date for timeline generation'),
|
||||||
|
) + (
|
||||||
|
make_option('--project',
|
||||||
|
action='store',
|
||||||
|
dest='project',
|
||||||
|
default=None,
|
||||||
|
help='Selected project id for timeline generation'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -148,4 +177,4 @@ class Command(BaseCommand):
|
||||||
if options["purge"] == True:
|
if options["purge"] == True:
|
||||||
Timeline.objects.all().delete()
|
Timeline.objects.all().delete()
|
||||||
|
|
||||||
generate_timeline(options["initial_date"], options["final_date"])
|
generate_timeline(options["initial_date"], options["final_date"], options["project"])
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
# Examples:
|
||||||
|
# python manage.py rebuild_timeline_for_user_creation --settings=settings.local_timeline
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.db import reset_queries
|
||||||
|
|
||||||
|
from taiga.timeline.service import (_get_impl_key_from_model,
|
||||||
|
_timeline_impl_map, extract_user_info)
|
||||||
|
from taiga.timeline.models import Timeline
|
||||||
|
from taiga.timeline.signals import _push_to_timelines
|
||||||
|
from taiga.users.models import User
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import gc
|
||||||
|
|
||||||
|
class BulkCreator(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.timeline_objects = []
|
||||||
|
self.created = None
|
||||||
|
|
||||||
|
def create_element(self, element):
|
||||||
|
self.timeline_objects.append(element)
|
||||||
|
if len(self.timeline_objects) > 1000:
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
Timeline.objects.bulk_create(self.timeline_objects, batch_size=1000)
|
||||||
|
del self.timeline_objects
|
||||||
|
self.timeline_objects = []
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
bulk_creator = BulkCreator()
|
||||||
|
|
||||||
|
|
||||||
|
def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, namespace:str="default", extra_data:dict={}):
|
||||||
|
assert isinstance(obj, Model), "obj must be a instance of Model"
|
||||||
|
assert isinstance(instance, Model), "instance must be a instance of Model"
|
||||||
|
event_type_key = _get_impl_key_from_model(instance.__class__, event_type)
|
||||||
|
impl = _timeline_impl_map.get(event_type_key, None)
|
||||||
|
|
||||||
|
bulk_creator.create_element(Timeline(
|
||||||
|
content_object=obj,
|
||||||
|
namespace=namespace,
|
||||||
|
event_type=event_type_key,
|
||||||
|
project=None,
|
||||||
|
data=impl(instance, extra_data=extra_data),
|
||||||
|
data_content_type = ContentType.objects.get_for_model(instance.__class__),
|
||||||
|
created = bulk_creator.created,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_timeline():
|
||||||
|
with patch('taiga.timeline.service._add_to_object_timeline', new=custom_add_to_object_timeline):
|
||||||
|
# Users api wasn't a HistoryResourceMixin so we can't interate on the HistoryEntries in this case
|
||||||
|
users = User.objects.order_by("date_joined")
|
||||||
|
for user in users.iterator():
|
||||||
|
bulk_creator.created = user.date_joined
|
||||||
|
print("User:", user.date_joined)
|
||||||
|
extra_data = {
|
||||||
|
"values_diff": {},
|
||||||
|
"user": extract_user_info(user),
|
||||||
|
}
|
||||||
|
_push_to_timelines(None, user, user, "create", extra_data=extra_data)
|
||||||
|
del extra_data
|
||||||
|
|
||||||
|
bulk_creator.flush()
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Regenerate project timeline'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
debug_enabled = settings.DEBUG
|
||||||
|
if debug_enabled:
|
||||||
|
print("Please, execute this script only with DEBUG mode disabled (DEBUG=False)")
|
||||||
|
return
|
||||||
|
|
||||||
|
generate_timeline()
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('timeline', '0003_auto_20150410_0829'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timeline',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(null=True, to='projects.Project'),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
]
|
|
@ -31,7 +31,7 @@ class Timeline(models.Model):
|
||||||
content_object = GenericForeignKey('content_type', 'object_id')
|
content_object = GenericForeignKey('content_type', 'object_id')
|
||||||
namespace = models.CharField(max_length=250, default="default", db_index=True)
|
namespace = models.CharField(max_length=250, default="default", db_index=True)
|
||||||
event_type = models.CharField(max_length=250, db_index=True)
|
event_type = models.CharField(max_length=250, db_index=True)
|
||||||
project = models.ForeignKey(Project)
|
project = models.ForeignKey(Project, null=True)
|
||||||
data = JsonField()
|
data = JsonField()
|
||||||
data_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
|
data_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
|
||||||
created = models.DateTimeField(default=timezone.now)
|
created = models.DateTimeField(default=timezone.now)
|
||||||
|
|
|
@ -43,6 +43,7 @@ class TimelineDataJsonField(serializers.WritableField):
|
||||||
"photo": get_photo_or_gravatar_url(user),
|
"photo": get_photo_or_gravatar_url(user),
|
||||||
"big_photo": get_big_photo_or_gravatar_url(user),
|
"big_photo": get_big_photo_or_gravatar_url(user),
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
|
"date_joined": user.date_joined,
|
||||||
}
|
}
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -57,11 +57,15 @@ def _add_to_object_timeline(obj:object, instance:object, event_type:str, namespa
|
||||||
event_type_key = _get_impl_key_from_model(instance.__class__, event_type)
|
event_type_key = _get_impl_key_from_model(instance.__class__, event_type)
|
||||||
impl = _timeline_impl_map.get(event_type_key, None)
|
impl = _timeline_impl_map.get(event_type_key, None)
|
||||||
|
|
||||||
|
project = None
|
||||||
|
if hasattr(instance, "project"):
|
||||||
|
project = instance.project
|
||||||
|
|
||||||
Timeline.objects.create(
|
Timeline.objects.create(
|
||||||
content_object=obj,
|
content_object=obj,
|
||||||
namespace=namespace,
|
namespace=namespace,
|
||||||
event_type=event_type_key,
|
event_type=event_type_key,
|
||||||
project=instance.project,
|
project=project,
|
||||||
data=impl(instance, extra_data=extra_data),
|
data=impl(instance, extra_data=extra_data),
|
||||||
data_content_type = ContentType.objects.get_for_model(instance.__class__),
|
data_content_type = ContentType.objects.get_for_model(instance.__class__),
|
||||||
)
|
)
|
||||||
|
@ -96,8 +100,8 @@ def get_timeline(obj, namespace=None):
|
||||||
|
|
||||||
|
|
||||||
def filter_timeline_for_user(timeline, user):
|
def filter_timeline_for_user(timeline, user):
|
||||||
# Filtering public projects
|
# Filtering entities from public projects or entities without project
|
||||||
tl_filter = Q(project__is_private=False)
|
tl_filter = Q(project__is_private=False) | Q(project=None)
|
||||||
|
|
||||||
# Filtering private project with some public parts
|
# Filtering private project with some public parts
|
||||||
content_types = {
|
content_types = {
|
||||||
|
@ -115,6 +119,10 @@ def filter_timeline_for_user(timeline, user):
|
||||||
project__anon_permissions__contains=[content_type_key],
|
project__anon_permissions__contains=[content_type_key],
|
||||||
data_content_type=content_type)
|
data_content_type=content_type)
|
||||||
|
|
||||||
|
# There is no specific permission for seeing new memberships
|
||||||
|
membership_content_type = ContentType.objects.get(app_label="projects", model="membership")
|
||||||
|
tl_filter |= Q(project__is_private=True, data_content_type=membership_content_type)
|
||||||
|
|
||||||
# Filtering private projects where user is member
|
# Filtering private projects where user is member
|
||||||
if not user.is_anonymous():
|
if not user.is_anonymous():
|
||||||
membership_model = apps.get_model('projects', 'Membership')
|
membership_model = apps.get_model('projects', 'Membership')
|
||||||
|
|
|
@ -34,6 +34,7 @@ def _push_to_timeline(*args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def _push_to_timelines(project, user, obj, event_type, extra_data={}):
|
def _push_to_timelines(project, user, obj, event_type, extra_data={}):
|
||||||
|
if project is not None:
|
||||||
# Project timeline
|
# Project timeline
|
||||||
_push_to_timeline(project, obj, event_type,
|
_push_to_timeline(project, obj, event_type,
|
||||||
namespace=build_project_namespace(project),
|
namespace=build_project_namespace(project),
|
||||||
|
@ -56,11 +57,11 @@ def _push_to_timelines(project, user, obj, event_type, extra_data={}):
|
||||||
if watchers:
|
if watchers:
|
||||||
related_people |= watchers
|
related_people |= watchers
|
||||||
|
|
||||||
|
if project is not None:
|
||||||
# Team
|
# Team
|
||||||
team_members_ids = project.memberships.filter(user__isnull=False).values_list("id", flat=True)
|
team_members_ids = project.memberships.filter(user__isnull=False).values_list("id", flat=True)
|
||||||
team = User.objects.filter(id__in=team_members_ids)
|
team = User.objects.filter(id__in=team_members_ids)
|
||||||
related_people |= team
|
related_people |= team
|
||||||
|
|
||||||
related_people = related_people.distinct()
|
related_people = related_people.distinct()
|
||||||
|
|
||||||
_push_to_timeline(related_people, obj, event_type,
|
_push_to_timeline(related_people, obj, event_type,
|
||||||
|
@ -98,12 +99,18 @@ def on_new_history_entry(sender, instance, created, **kwargs):
|
||||||
"comment_html": instance.comment_html,
|
"comment_html": instance.comment_html,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Detect deleted comment
|
||||||
|
if instance.delete_comment_date:
|
||||||
|
extra_data["comment_deleted"] = True
|
||||||
|
|
||||||
_push_to_timelines(project, user, obj, event_type, extra_data=extra_data)
|
_push_to_timelines(project, user, obj, event_type, extra_data=extra_data)
|
||||||
|
|
||||||
|
|
||||||
def create_membership_push_to_timeline(sender, instance, **kwargs):
|
def create_membership_push_to_timeline(sender, instance, **kwargs):
|
||||||
# Creating new membership with associated user
|
# Creating new membership with associated user
|
||||||
if not instance.pk and instance.user:
|
# If the user is the project owner we don't do anything because that info will
|
||||||
|
# be shown in created project timeline entry
|
||||||
|
if not instance.pk and instance.user and instance.user != instance.project.owner:
|
||||||
_push_to_timelines(instance.project, instance.user, instance, "create")
|
_push_to_timelines(instance.project, instance.user, instance, "create")
|
||||||
|
|
||||||
#Updating existing membership
|
#Updating existing membership
|
||||||
|
@ -120,3 +127,10 @@ def create_membership_push_to_timeline(sender, instance, **kwargs):
|
||||||
def delete_membership_push_to_timeline(sender, instance, **kwargs):
|
def delete_membership_push_to_timeline(sender, instance, **kwargs):
|
||||||
if instance.user:
|
if instance.user:
|
||||||
_push_to_timelines(instance.project, instance.user, instance, "delete")
|
_push_to_timelines(instance.project, instance.user, instance, "delete")
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_push_to_timeline(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
project = None
|
||||||
|
user = instance
|
||||||
|
_push_to_timelines(project, user, user, "create")
|
||||||
|
|
|
@ -105,3 +105,11 @@ def membership_timeline(instance, extra_data={}):
|
||||||
}
|
}
|
||||||
result.update(extra_data)
|
result.update(extra_data)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@register_timeline_implementation("users.user", "create")
|
||||||
|
def user_timeline(instance, extra_data={}):
|
||||||
|
result = {
|
||||||
|
"user": service.extract_user_info(instance),
|
||||||
|
}
|
||||||
|
result.update(extra_data)
|
||||||
|
return result
|
||||||
|
|
|
@ -44,7 +44,7 @@ class UserPermission(TaigaResourcePermission):
|
||||||
me_perms = IsAuthenticated()
|
me_perms = IsAuthenticated()
|
||||||
remove_avatar_perms = IsAuthenticated()
|
remove_avatar_perms = IsAuthenticated()
|
||||||
starred_perms = AllowAny()
|
starred_perms = AllowAny()
|
||||||
change_email_perms = IsTheSameUser()
|
change_email_perms = AllowAny()
|
||||||
contacts_perms = AllowAny()
|
contacts_perms = AllowAny()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -272,6 +272,7 @@ def test_user_action_password_recovery(client, data):
|
||||||
def test_user_action_change_email(client, data):
|
def test_user_action_change_email(client, data):
|
||||||
url = reverse('users-change-email')
|
url = reverse('users-change-email')
|
||||||
|
|
||||||
|
def after_each_request():
|
||||||
data.registered_user.email_token = "test-token"
|
data.registered_user.email_token = "test-token"
|
||||||
data.registered_user.new_email = "new@email.com"
|
data.registered_user.new_email = "new@email.com"
|
||||||
data.registered_user.save()
|
data.registered_user.save()
|
||||||
|
@ -283,5 +284,6 @@ def test_user_action_change_email(client, data):
|
||||||
]
|
]
|
||||||
|
|
||||||
patch_data = json.dumps({"email_token": "test-token"})
|
patch_data = json.dumps({"email_token": "test-token"})
|
||||||
results = helper_test_http_method(client, 'post', url, patch_data, users)
|
after_each_request()
|
||||||
assert results == [401, 204, 400]
|
results = helper_test_http_method(client, 'post', url, patch_data, users, after_each_request=after_each_request)
|
||||||
|
assert results == [204, 204, 204]
|
||||||
|
|
|
@ -36,7 +36,7 @@ def test_add_to_object_timeline():
|
||||||
|
|
||||||
service._add_to_object_timeline(user1, task, "test")
|
service._add_to_object_timeline(user1, task, "test")
|
||||||
|
|
||||||
assert Timeline.objects.filter(object_id=user1.id).count() == 1
|
assert Timeline.objects.filter(object_id=user1.id).count() == 2
|
||||||
assert Timeline.objects.order_by("-id")[0].data == id(task)
|
assert Timeline.objects.order_by("-id")[0].data == id(task)
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,9 +59,9 @@ def test_get_timeline():
|
||||||
service._add_to_object_timeline(user1, task4, "test")
|
service._add_to_object_timeline(user1, task4, "test")
|
||||||
service._add_to_object_timeline(user2, task1, "test")
|
service._add_to_object_timeline(user2, task1, "test")
|
||||||
|
|
||||||
assert Timeline.objects.filter(object_id=user1.id).count() == 4
|
assert Timeline.objects.filter(object_id=user1.id).count() == 5
|
||||||
assert Timeline.objects.filter(object_id=user2.id).count() == 1
|
assert Timeline.objects.filter(object_id=user2.id).count() == 2
|
||||||
assert Timeline.objects.filter(object_id=user3.id).count() == 0
|
assert Timeline.objects.filter(object_id=user3.id).count() == 1
|
||||||
|
|
||||||
|
|
||||||
def test_filter_timeline_no_privileges():
|
def test_filter_timeline_no_privileges():
|
||||||
|
@ -72,7 +72,7 @@ def test_filter_timeline_no_privileges():
|
||||||
|
|
||||||
service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
|
service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
|
||||||
service._add_to_object_timeline(user1, task1, "test")
|
service._add_to_object_timeline(user1, task1, "test")
|
||||||
timeline = Timeline.objects.all()
|
timeline = Timeline.objects.exclude(event_type="users.user.create")
|
||||||
timeline = service.filter_timeline_for_user(timeline, user2)
|
timeline = service.filter_timeline_for_user(timeline, user2)
|
||||||
assert timeline.count() == 0
|
assert timeline.count() == 0
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ def test_filter_timeline_public_project():
|
||||||
service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
|
service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
|
||||||
service._add_to_object_timeline(user1, task1, "test")
|
service._add_to_object_timeline(user1, task1, "test")
|
||||||
service._add_to_object_timeline(user1, task2, "test")
|
service._add_to_object_timeline(user1, task2, "test")
|
||||||
timeline = Timeline.objects.all()
|
timeline = Timeline.objects.exclude(event_type="users.user.create")
|
||||||
timeline = service.filter_timeline_for_user(timeline, user2)
|
timeline = service.filter_timeline_for_user(timeline, user2)
|
||||||
assert timeline.count() == 1
|
assert timeline.count() == 1
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ def test_filter_timeline_private_project_anon_permissions():
|
||||||
service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
|
service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
|
||||||
service._add_to_object_timeline(user1, task1, "test")
|
service._add_to_object_timeline(user1, task1, "test")
|
||||||
service._add_to_object_timeline(user1, task2, "test")
|
service._add_to_object_timeline(user1, task2, "test")
|
||||||
timeline = Timeline.objects.all()
|
timeline = Timeline.objects.exclude(event_type="users.user.create")
|
||||||
timeline = service.filter_timeline_for_user(timeline, user2)
|
timeline = service.filter_timeline_for_user(timeline, user2)
|
||||||
assert timeline.count() == 1
|
assert timeline.count() == 1
|
||||||
|
|
||||||
|
@ -123,9 +123,9 @@ def test_filter_timeline_private_project_member_permissions():
|
||||||
service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
|
service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x)))
|
||||||
service._add_to_object_timeline(user1, task1, "test")
|
service._add_to_object_timeline(user1, task1, "test")
|
||||||
service._add_to_object_timeline(user1, task2, "test")
|
service._add_to_object_timeline(user1, task2, "test")
|
||||||
timeline = Timeline.objects.all()
|
timeline = Timeline.objects.exclude(event_type="users.user.create")
|
||||||
timeline = service.filter_timeline_for_user(timeline, user2)
|
timeline = service.filter_timeline_for_user(timeline, user2)
|
||||||
assert timeline.count() == 1
|
assert timeline.count() == 3
|
||||||
|
|
||||||
|
|
||||||
def test_create_project_timeline():
|
def test_create_project_timeline():
|
||||||
|
|
|
@ -93,6 +93,18 @@ def test_validate_requested_email_change(client):
|
||||||
assert user.new_email is None
|
assert user.new_email is None
|
||||||
assert user.email == "new@email.com"
|
assert user.email == "new@email.com"
|
||||||
|
|
||||||
|
def test_validate_requested_email_change_for_anonymous_user(client):
|
||||||
|
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||||
|
url = reverse('users-change-email')
|
||||||
|
data = {"email_token": "change_email_token"}
|
||||||
|
|
||||||
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
user = models.User.objects.get(pk=user.id)
|
||||||
|
assert user.email_token is None
|
||||||
|
assert user.new_email is None
|
||||||
|
assert user.email == "new@email.com"
|
||||||
|
|
||||||
def test_validate_requested_email_change_without_token(client):
|
def test_validate_requested_email_change_without_token(client):
|
||||||
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||||
|
|
Loading…
Reference in New Issue