Merge remote-tracking branch 'upstream/master'
commit
edfb98de72
|
@ -1,4 +1,5 @@
|
|||
.*.sw*
|
||||
.#*
|
||||
*.log
|
||||
taiga/search
|
||||
settings/local.py
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
# Changelog #
|
||||
|
||||
|
||||
## 1.8.0 ??? (unreleased)
|
||||
## 1.8.0 Saracenia Purpurea (unreleased)
|
||||
|
||||
### Features
|
||||
- Improve timeline resource.
|
||||
- Add sitemap of taiga-front (the web client).
|
||||
- Search by reference (thanks to [@artlepool](https://github.com/artlepool))
|
||||
- Add call 'by_username' to the API resource User
|
||||
- i18n.
|
||||
- Add deutsch (de) translation.
|
||||
- Add nederlands (nl) translation.
|
||||
|
||||
### Misc
|
||||
- Lots of small and not so small bugfixes.
|
||||
|
|
|
@ -18,3 +18,5 @@ echo "-> Load initial roles"
|
|||
python manage.py loaddata initial_role --traceback
|
||||
echo "-> Generate sample data"
|
||||
python manage.py sample_data --traceback
|
||||
echo "-> Rebuilding timeline"
|
||||
python manage.py rebuild_timeline --purge
|
||||
|
|
|
@ -78,7 +78,7 @@ LANGUAGES = [
|
|||
#("cs", "Čeština"), # Czech
|
||||
#("cy", "Cymraeg"), # Welsh
|
||||
#("da", "Dansk"), # Danish
|
||||
#("de", "Deutsch"), # German
|
||||
("de", "Deutsch"), # German
|
||||
#("el", "Ελληνικά"), # Greek
|
||||
("en", "English (US)"), # English
|
||||
#("en-au", "English (Australia)"), # Australian English
|
||||
|
@ -122,7 +122,7 @@ LANGUAGES = [
|
|||
#("my", "မြန်မာ"), # Burmese
|
||||
#("nb", "Norsk (bokmål)"), # Norwegian Bokmal
|
||||
#("ne", "नेपाली"), # Nepali
|
||||
#("nl", "Nederlands"), # Dutch
|
||||
("nl", "Nederlands"), # Dutch
|
||||
#("nn", "Norsk (nynorsk)"), # Norwegian Nynorsk
|
||||
#("os", "Ирон æвзаг"), # Ossetic
|
||||
#("pa", "ਪੰਜਾਬੀ"), # Punjabi
|
||||
|
|
|
@ -25,8 +25,8 @@ COORS_ALLOWED_HEADERS = ["content-type", "x-requested-with",
|
|||
"x-session-id"]
|
||||
COORS_ALLOWED_CREDENTIALS = True
|
||||
COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", "x-paginated-by",
|
||||
"x-paginated-by", "x-pagination-current", "x-site-host",
|
||||
"x-site-register"]
|
||||
"x-pagination-current", "x-pagination-next", "x-pagination-prev",
|
||||
"x-site-host", "x-site-register"]
|
||||
|
||||
|
||||
class CoorsMiddleware(object):
|
||||
|
|
|
@ -379,7 +379,7 @@
|
|||
}
|
||||
</style>
|
||||
</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>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||
<tr>
|
||||
|
|
|
@ -340,7 +340,7 @@
|
|||
}
|
||||
</style>
|
||||
</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>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||
<tr>
|
||||
|
|
|
@ -379,7 +379,7 @@
|
|||
}
|
||||
</style>
|
||||
</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>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
|
||||
<tr>
|
||||
|
|
|
@ -241,7 +241,9 @@ class HistoryExportSerializerMixin(serializers.ModelSerializer):
|
|||
history = serializers.SerializerMethodField("get_history")
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ from django.template.defaultfilters import slugify
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
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.projects.references import sequences as seq
|
||||
from taiga.projects.references import models as refs
|
||||
|
@ -229,9 +229,13 @@ def store_task(project, data):
|
|||
for task_attachment in data.get("attachments", []):
|
||||
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)
|
||||
|
||||
if not history_entries:
|
||||
take_snapshot(serialized.object, user=serialized.object.owner)
|
||||
|
||||
custom_attributes_values = data.get("custom_attributes_values", None)
|
||||
if custom_attributes_values:
|
||||
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", []):
|
||||
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)
|
||||
|
||||
if not history_entries:
|
||||
take_snapshot(serialized.object, user=serialized.object.owner)
|
||||
|
||||
return serialized
|
||||
|
||||
add_errors("wiki_pages", serialized.errors)
|
||||
|
@ -381,9 +389,13 @@ def store_user_story(project, data):
|
|||
for role_point in data.get("role_points", []):
|
||||
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)
|
||||
|
||||
if not history_entries:
|
||||
take_snapshot(serialized.object, user=serialized.object.owner)
|
||||
|
||||
custom_attributes_values = data.get("custom_attributes_values", None)
|
||||
if custom_attributes_values:
|
||||
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", []):
|
||||
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)
|
||||
|
||||
if not history_entries:
|
||||
take_snapshot(serialized.object, user=serialized.object.owner)
|
||||
|
||||
custom_attributes_values = data.get("custom_attributes_values", None)
|
||||
if custom_attributes_values:
|
||||
custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name')
|
||||
|
|
|
@ -9,7 +9,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: taiga-back\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"
|
||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||
"Language-Team: Catalan (http://www.transifex.com/projects/p/taiga-back/"
|
||||
|
@ -2554,7 +2554,7 @@ msgstr ""
|
|||
msgid "Stakeholder"
|
||||
msgstr ""
|
||||
|
||||
#: taiga/projects/userstories/api.py:173
|
||||
#: taiga/projects/userstories/api.py:174
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"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 ""
|
||||
"Project-Id-Version: taiga-back\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"
|
||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||
"Language-Team: Taiga Dev Team <support@taiga.io>\n"
|
||||
|
@ -2503,7 +2503,7 @@ msgstr ""
|
|||
msgid "Stakeholder"
|
||||
msgstr ""
|
||||
|
||||
#: taiga/projects/userstories/api.py:173
|
||||
#: taiga/projects/userstories/api.py:174
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||
|
|
|
@ -12,9 +12,9 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: taiga-back\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-06-09 09:47+0200\n"
|
||||
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||
"POT-Creation-Date: 2015-06-15 12:34+0200\n"
|
||||
"PO-Revision-Date: 2015-06-17 07:24+0000\n"
|
||||
"Last-Translator: David Barragán <bameda@gmail.com>\n"
|
||||
"Language-Team: Spanish (http://www.transifex.com/projects/p/taiga-back/"
|
||||
"language/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -477,6 +477,9 @@ msgid ""
|
|||
"%(comment)s</p>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"<h3>comentario:</h3>\n"
|
||||
"<p>%(comment)s</p>"
|
||||
|
||||
#: taiga/base/templates/emails/updates-body-text.jinja:6
|
||||
#, python-format
|
||||
|
@ -851,7 +854,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\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
|
||||
#, python-format
|
||||
|
@ -2477,7 +2480,7 @@ msgstr "La versión debe ser un número entero"
|
|||
|
||||
#: taiga/projects/occ/mixins.py:58
|
||||
msgid "The version parameter is not valid"
|
||||
msgstr ""
|
||||
msgstr "La versión no es válida"
|
||||
|
||||
#: taiga/projects/occ/mixins.py:74
|
||||
msgid "The version doesn't match with the current one"
|
||||
|
@ -2961,7 +2964,7 @@ msgstr "Product Owner"
|
|||
msgid "Stakeholder"
|
||||
msgstr "Stakeholder"
|
||||
|
||||
#: taiga/projects/userstories/api.py:173
|
||||
#: taiga/projects/userstories/api.py:174
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||
|
@ -3165,7 +3168,7 @@ msgstr "idioma por defecto"
|
|||
|
||||
#: taiga/users/models.py:122
|
||||
msgid "default theme"
|
||||
msgstr ""
|
||||
msgstr "tema por defecto"
|
||||
|
||||
#: taiga/users/models.py:124
|
||||
msgid "default timezone"
|
||||
|
|
|
@ -9,7 +9,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: taiga-back\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"
|
||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||
"Language-Team: Finnish (http://www.transifex.com/projects/p/taiga-back/"
|
||||
|
@ -2954,7 +2954,7 @@ msgstr "Tuoteomistaja"
|
|||
msgid "Stakeholder"
|
||||
msgstr "Sidosryhmä"
|
||||
|
||||
#: taiga/projects/userstories/api.py:173
|
||||
#: taiga/projects/userstories/api.py:174
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||
|
|
|
@ -8,15 +8,16 @@
|
|||
# Florent B. <me@kxrz.me>, 2015
|
||||
# Louis-Michel Couture <louim_1@hotmail.com>, 2015
|
||||
# Matthieu Durocher <matthieu@technocyclope.com>, 2015
|
||||
# Nlko <nospam1@thomasson.fr>, 2015
|
||||
# Stéphane Mor <stephanemor@gmail.com>, 2015
|
||||
# William Godin <williamgodin@gmail.com>, 2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: taiga-back\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-06-09 09:47+0200\n"
|
||||
"PO-Revision-Date: 2015-06-09 07:47+0000\n"
|
||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||
"POT-Creation-Date: 2015-06-15 12:34+0200\n"
|
||||
"PO-Revision-Date: 2015-06-12 21:30+0000\n"
|
||||
"Last-Translator: Nlko <nospam1@thomasson.fr>\n"
|
||||
"Language-Team: French (http://www.transifex.com/projects/p/taiga-back/"
|
||||
"language/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -469,6 +470,9 @@ msgid ""
|
|||
"%(comment)s</p>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"<h3>commentaire:</h3>\n"
|
||||
"<p>%(comment)s</p>"
|
||||
|
||||
#: taiga/base/templates/emails/updates-body-text.jinja:6
|
||||
#, python-format
|
||||
|
@ -894,6 +898,9 @@ msgid ""
|
|||
"\n"
|
||||
"{message}"
|
||||
msgstr ""
|
||||
"Commentaire provenant de GitHub:\n"
|
||||
"\n"
|
||||
"{message}"
|
||||
|
||||
#: taiga/hooks/gitlab/event_hooks.py:87
|
||||
msgid "Status changed from GitLab commit"
|
||||
|
@ -1800,6 +1807,8 @@ msgid ""
|
|||
"\n"
|
||||
"[%(project)s] Created the sprint \"%(milestone)s\"\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"[%(project)s] Sprint \"%(milestone)s\" créé\n"
|
||||
|
||||
#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4
|
||||
#, python-format
|
||||
|
@ -1831,6 +1840,8 @@ msgid ""
|
|||
"\n"
|
||||
"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"[%(project)s] Sprint \"%(milestone)s\" Éffacé\n"
|
||||
|
||||
#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4
|
||||
#, python-format
|
||||
|
@ -1860,6 +1871,8 @@ msgid ""
|
|||
"\n"
|
||||
"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n"
|
||||
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
|
||||
#, python-format
|
||||
|
@ -1893,6 +1906,8 @@ msgid ""
|
|||
"\n"
|
||||
"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n"
|
||||
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
|
||||
#, python-format
|
||||
|
@ -1924,6 +1939,8 @@ msgid ""
|
|||
"\n"
|
||||
"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n"
|
||||
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
|
||||
#, python-format
|
||||
|
@ -1953,6 +1970,8 @@ msgid ""
|
|||
"\n"
|
||||
"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"[%(project)s] US #%(ref)s \"%(subject)s\" mise à jour\n"
|
||||
|
||||
#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4
|
||||
#, python-format
|
||||
|
@ -2594,7 +2613,7 @@ msgstr "Product Owner"
|
|||
msgid "Stakeholder"
|
||||
msgstr "Participant"
|
||||
|
||||
#: taiga/projects/userstories/api.py:173
|
||||
#: taiga/projects/userstories/api.py:174
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"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 ""
|
||||
"Project-Id-Version: taiga-back\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"
|
||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||
"Language-Team: Chinese Traditional (http://www.transifex.com/projects/p/"
|
||||
|
@ -2946,7 +2946,7 @@ msgstr "產品所有人"
|
|||
msgid "Stakeholder"
|
||||
msgstr "利害關係人"
|
||||
|
||||
#: taiga/projects/userstories/api.py:173
|
||||
#: taiga/projects/userstories/api.py:174
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - "
|
||||
|
|
|
@ -98,6 +98,7 @@ class BasicUserStoryStatusSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = models.UserStoryStatus
|
||||
i18n_fields = ("name",)
|
||||
fields = ("name", "color")
|
||||
|
||||
|
||||
|
@ -128,6 +129,7 @@ class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = models.TaskStatus
|
||||
i18n_fields = ("name",)
|
||||
fields = ("name", "color")
|
||||
|
||||
|
||||
|
@ -170,6 +172,7 @@ class BasicIssueStatusSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = models.IssueStatus
|
||||
i18n_fields = ("name",)
|
||||
fields = ("name", "color")
|
||||
|
||||
|
||||
|
@ -273,6 +276,7 @@ class ProjectMembershipSerializer(serializers.ModelSerializer):
|
|||
full_name = serializers.CharField(source='user.get_full_name', required=False)
|
||||
username = serializers.CharField(source='user.username', 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")
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -26,7 +26,7 @@ from taiga.projects.validators import ProjectExistsValidator
|
|||
from taiga.projects.validators import UserStoryStatusExistsValidator
|
||||
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
||||
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 . import models
|
||||
|
@ -53,7 +53,7 @@ class UserStorySerializer(WatchersValidator, serializers.ModelSerializer):
|
|||
origin_issue = serializers.SerializerMethodField("get_origin_issue")
|
||||
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_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)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -32,3 +32,5 @@ class TimelineAppConfig(AppConfig):
|
|||
sender=apps.get_model("projects", "Membership"))
|
||||
signals.post_delete.connect(handlers.delete_membership_push_to_timeline,
|
||||
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,13 +46,16 @@ class BulkCreator(object):
|
|||
self.timeline_objects = []
|
||||
self.created = None
|
||||
|
||||
def createElement(self, element):
|
||||
def create_element(self, element):
|
||||
self.timeline_objects.append(element)
|
||||
if len(self.timeline_objects) > 1000:
|
||||
Timeline.objects.bulk_create(self.timeline_objects, batch_size=1000)
|
||||
del self.timeline_objects
|
||||
self.timeline_objects = []
|
||||
gc.collect()
|
||||
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()
|
||||
|
||||
|
@ -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)
|
||||
impl = _timeline_impl_map.get(event_type_key, None)
|
||||
|
||||
bulk_creator.createElement(Timeline(
|
||||
bulk_creator.create_element(Timeline(
|
||||
content_object=obj,
|
||||
namespace=namespace,
|
||||
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):
|
||||
if initial_date or final_date:
|
||||
def generate_timeline(initial_date, final_date, project_id):
|
||||
if initial_date or final_date or project_id:
|
||||
timelines = Timeline.objects.all()
|
||||
if initial_date:
|
||||
timelines = timelines.filter(created__gte=initial_date)
|
||||
if final_date:
|
||||
timelines = timelines.filter(created__lt=final_date)
|
||||
if project_id:
|
||||
timelines = timelines.filter(project__id=project_id)
|
||||
|
||||
timelines.delete()
|
||||
|
||||
|
@ -97,6 +102,22 @@ def generate_timeline(initial_date, final_date):
|
|||
projects = projects.filter(created_date__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():
|
||||
bulk_creator.created = project.created_date
|
||||
print("Project:", bulk_creator.created)
|
||||
|
@ -115,6 +136,8 @@ def generate_timeline(initial_date, final_date):
|
|||
except ObjectDoesNotExist as e:
|
||||
print("Ignoring")
|
||||
|
||||
bulk_creator.flush()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Regenerate project timeline'
|
||||
|
@ -136,6 +159,12 @@ class Command(BaseCommand):
|
|||
dest='final_date',
|
||||
default=None,
|
||||
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:
|
||||
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')
|
||||
namespace = models.CharField(max_length=250, default="default", 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_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
|
||||
created = models.DateTimeField(default=timezone.now)
|
||||
|
|
|
@ -43,6 +43,7 @@ class TimelineDataJsonField(serializers.WritableField):
|
|||
"photo": get_photo_or_gravatar_url(user),
|
||||
"big_photo": get_big_photo_or_gravatar_url(user),
|
||||
"username": user.username,
|
||||
"date_joined": user.date_joined,
|
||||
}
|
||||
except User.DoesNotExist:
|
||||
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)
|
||||
impl = _timeline_impl_map.get(event_type_key, None)
|
||||
|
||||
project = None
|
||||
if hasattr(instance, "project"):
|
||||
project = instance.project
|
||||
|
||||
Timeline.objects.create(
|
||||
content_object=obj,
|
||||
namespace=namespace,
|
||||
event_type=event_type_key,
|
||||
project=instance.project,
|
||||
project=project,
|
||||
data=impl(instance, extra_data=extra_data),
|
||||
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):
|
||||
# Filtering public projects
|
||||
tl_filter = Q(project__is_private=False)
|
||||
# Filtering entities from public projects or entities without project
|
||||
tl_filter = Q(project__is_private=False) | Q(project=None)
|
||||
|
||||
# Filtering private project with some public parts
|
||||
content_types = {
|
||||
|
@ -115,6 +119,10 @@ def filter_timeline_for_user(timeline, user):
|
|||
project__anon_permissions__contains=[content_type_key],
|
||||
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
|
||||
if not user.is_anonymous():
|
||||
membership_model = apps.get_model('projects', 'Membership')
|
||||
|
|
|
@ -34,10 +34,11 @@ def _push_to_timeline(*args, **kwargs):
|
|||
|
||||
|
||||
def _push_to_timelines(project, user, obj, event_type, extra_data={}):
|
||||
if project is not None:
|
||||
# Project timeline
|
||||
_push_to_timeline(project, obj, event_type,
|
||||
namespace=build_project_namespace(project),
|
||||
extra_data=extra_data)
|
||||
_push_to_timeline(project, obj, event_type,
|
||||
namespace=build_project_namespace(project),
|
||||
extra_data=extra_data)
|
||||
|
||||
# User timeline
|
||||
_push_to_timeline(user, obj, event_type,
|
||||
|
@ -56,18 +57,18 @@ def _push_to_timelines(project, user, obj, event_type, extra_data={}):
|
|||
if watchers:
|
||||
related_people |= watchers
|
||||
|
||||
# Team
|
||||
team_members_ids = project.memberships.filter(user__isnull=False).values_list("id", flat=True)
|
||||
team = User.objects.filter(id__in=team_members_ids)
|
||||
related_people |= team
|
||||
if project is not None:
|
||||
# Team
|
||||
team_members_ids = project.memberships.filter(user__isnull=False).values_list("id", flat=True)
|
||||
team = User.objects.filter(id__in=team_members_ids)
|
||||
related_people |= team
|
||||
related_people = related_people.distinct()
|
||||
|
||||
related_people = related_people.distinct()
|
||||
_push_to_timeline(related_people, obj, event_type,
|
||||
namespace=build_user_namespace(user),
|
||||
extra_data=extra_data)
|
||||
|
||||
_push_to_timeline(related_people, obj, event_type,
|
||||
namespace=build_user_namespace(user),
|
||||
extra_data=extra_data)
|
||||
|
||||
#Related people: team members
|
||||
#Related people: team members
|
||||
|
||||
|
||||
def on_new_history_entry(sender, instance, created, **kwargs):
|
||||
|
@ -98,12 +99,18 @@ def on_new_history_entry(sender, instance, created, **kwargs):
|
|||
"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)
|
||||
|
||||
|
||||
def create_membership_push_to_timeline(sender, instance, **kwargs):
|
||||
# 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")
|
||||
|
||||
#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):
|
||||
if instance.user:
|
||||
_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)
|
||||
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()
|
||||
remove_avatar_perms = IsAuthenticated()
|
||||
starred_perms = AllowAny()
|
||||
change_email_perms = IsTheSameUser()
|
||||
change_email_perms = AllowAny()
|
||||
contacts_perms = AllowAny()
|
||||
|
||||
|
||||
|
|
|
@ -272,9 +272,10 @@ def test_user_action_password_recovery(client, data):
|
|||
def test_user_action_change_email(client, data):
|
||||
url = reverse('users-change-email')
|
||||
|
||||
data.registered_user.email_token = "test-token"
|
||||
data.registered_user.new_email = "new@email.com"
|
||||
data.registered_user.save()
|
||||
def after_each_request():
|
||||
data.registered_user.email_token = "test-token"
|
||||
data.registered_user.new_email = "new@email.com"
|
||||
data.registered_user.save()
|
||||
|
||||
users = [
|
||||
None,
|
||||
|
@ -283,5 +284,6 @@ def test_user_action_change_email(client, data):
|
|||
]
|
||||
|
||||
patch_data = json.dumps({"email_token": "test-token"})
|
||||
results = helper_test_http_method(client, 'post', url, patch_data, users)
|
||||
assert results == [401, 204, 400]
|
||||
after_each_request()
|
||||
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")
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -59,9 +59,9 @@ def test_get_timeline():
|
|||
service._add_to_object_timeline(user1, task4, "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=user2.id).count() == 1
|
||||
assert Timeline.objects.filter(object_id=user3.id).count() == 0
|
||||
assert Timeline.objects.filter(object_id=user1.id).count() == 5
|
||||
assert Timeline.objects.filter(object_id=user2.id).count() == 2
|
||||
assert Timeline.objects.filter(object_id=user3.id).count() == 1
|
||||
|
||||
|
||||
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._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)
|
||||
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._add_to_object_timeline(user1, task1, "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)
|
||||
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._add_to_object_timeline(user1, task1, "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)
|
||||
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._add_to_object_timeline(user1, task1, "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)
|
||||
assert timeline.count() == 1
|
||||
assert timeline.count() == 3
|
||||
|
||||
|
||||
def test_create_project_timeline():
|
||||
|
|
|
@ -93,6 +93,18 @@ def test_validate_requested_email_change(client):
|
|||
assert user.new_email is None
|
||||
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):
|
||||
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||
|
|
Loading…
Reference in New Issue