Compare commits

...

31 Commits

Author SHA1 Message Date
hecfernandez 120442206b fix tags exclude filter behaviour (#1246)
* fix tags exclude filter behaviour
2019-02-13 14:20:04 +01:00
Álex Hermida d6d3f8c6a8 Update change log 2019-02-01 21:54:16 +01:00
Miguel Gonzalez 3cebad87eb fix: Sanitize email header removing linefeed and carriage return chars 2019-01-29 13:45:23 +01:00
Álex Hermida f45f5ae08a Activate Ukrainian language 2019-01-29 13:15:19 +01:00
Álex Hermida 0434e8b78b Fix calculate_milestone_is_closed and add tests 2019-01-29 10:30:59 +01:00
Héctor Fernández Cascallar 0c0e09819a refactor concatenation method for keep compatibility 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 2b38fefa13 refactor exclude filter mode implementation 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 7c5ba16d24 refactor prepare filter methods in tags filter 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar b0d065167c refactor tags exclude filter implementation 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 4bb12d73d9 fix membership query for avoid project-role without users 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar e736846562 code refactor 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 2bdd652ea7 remove pytest development mark 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 483d3ffd5f add tests for task filters 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 2d77f8974b add exclude mode for user stories filter 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 77fa09a953 add exclude mode for issues filters 2019-01-22 11:29:20 +01:00
Álex Hermida df9830bb4f Update change log 2019-01-14 13:48:44 +01:00
Álex Hermida 849ce97a1c Update messages catalog 2019-01-14 13:25:24 +01:00
Álex Hermida c260a4dd22 Upgrade calculate milestone is closed 2019-01-11 13:24:48 +01:00
Álex Hermida 5f301450df Modify create task factory 2019-01-03 22:13:49 +01:00
Álex Hermida abf2b11220 Refactor and fix tests 2019-01-03 22:13:49 +01:00
Álex Hermida a4256c3f09 Add issue milestones signals 2019-01-03 22:13:49 +01:00
Álex Hermida 18e97be27c Add move issues to sprint test & endpoint 2019-01-03 22:13:49 +01:00
Álex Hermida a17ed83755 Move tasks to another sprint 2019-01-03 22:13:49 +01:00
Álex Hermida fe4cddac30 Test move and close oprevious sprint 2019-01-03 22:13:49 +01:00
Álex Hermida 0cb423c929 Change approach move us's from sprint to another 2019-01-03 22:13:49 +01:00
Álex Hermida 39e9de71cf Add milestones bulk items 2019-01-03 22:13:49 +01:00
Álex Hermida 4c74e6182f Add test test_api_update_milestone_in_bulk_userstories 2019-01-03 22:13:49 +01:00
Álex Hermida dae83618a1 Update change log 2018-12-10 19:36:53 +01:00
Miguel Gonzalez 97b69cdb61 Add extra requirements for oauthlib
See https://oauthlib.readthedocs.io/en/latest/faq.html?highlight=signedtoken#oauth-2-serviceapplicationclient-and-oauth-1-with-rsa-sha1-signatures-say-could-not-import-jwt-what-should-i-do
2018-12-10 10:49:58 +01:00
Álex Hermida a5386cb79c Update change log 2018-12-03 21:41:03 +01:00
Álex Hermida 6a0a55f982 Update messages catalog 2018-12-03 21:40:15 +01:00
28 changed files with 5632 additions and 328 deletions

View File

@ -2,6 +2,35 @@
## Unreleased
## 4.1.0 (2019-02-04)
### Misc
- Fix Close sprints
### Features:
- Negative filters
- Activate the Ukrainian language
## 4.0.4 (2019-01-15)
### Misc
- Minor bug fixes.
## 4.0.3 (2018-12-11)
### Misc
- Add extra requirements for oauthlib
## 4.0.2 (2018-12-04)
### Misc
- Update messages catalog.
## 4.0.1 (2018-11-28)
### Misc

View File

@ -31,6 +31,7 @@ pytz = "*"
raven = "==6.1.0"
redis = "==2.10.5"
requests = "==2.20.0"
requests_oauthlib = "*"
serpy = "==0.1.1"
webcolors = "==1.7"
CairoSVG = "==2.0.3"
@ -39,6 +40,7 @@ Markdown = "==3.0.1"
Pillow = "==4.1.1"
Unidecode = "==0.4.20"
Pygments = "==2.2.0"
oauthlib = {extras = ["signedtoken"],version = "*"}
[dev-packages]
coverage = "*"

31
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "888d57920f90a3bcb3ad23268eef74f7c2951caa473cab150774fe460ac797a5"
"sha256": "2505838cdc4b5390130990ae63647a4447c5f146ab37ca8bf29f627091112cf7"
},
"pipfile-spec": 6,
"requires": {
@ -40,9 +40,9 @@
},
"billiard": {
"hashes": [
"sha256:ed65448da5877b5558f19d2f7f11f8355ea76b3e63e1c0a6059f47cfae5f1c84"
"sha256:42d9a227401ac4fba892918bba0a0c409def5435c4b483267ebfe821afaaba0e"
],
"version": "==3.5.0.4"
"version": "==3.5.0.5"
},
"bleach": {
"hashes": [
@ -75,10 +75,10 @@
},
"certifi": {
"hashes": [
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.10.15"
"version": "==2018.11.29"
},
"cffi": {
"hashes": [
@ -498,6 +498,13 @@
"index": "pypi",
"version": "==2.2.0"
},
"pyjwt": {
"hashes": [
"sha256:00414bfef802aaecd8cc0d5258b6cb87bd8f553c2986c2c5f29b19dd5633aeb7",
"sha256:ddec8409c57e9d371c6006e388f91daf3b0b43bdf9fcbf99451fb7cf5ce0a86d"
],
"version": "==1.7.0"
},
"python-dateutil": {
"hashes": [
"sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
@ -634,10 +641,10 @@
},
"certifi": {
"hashes": [
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.10.15"
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
@ -707,10 +714,10 @@
},
"faker": {
"hashes": [
"sha256:2621643b80a10b91999925cfd20f64d2b36f20bf22136bbdc749bb57d6ffe124",
"sha256:5ed822d31bd2d6edf10944d176d30dc9c886afdd381eefb7ba8b7aad86171646"
"sha256:c61a41d0dab8865b850bd00454fb11e90f3fd2a092d8bc90120d1e1c01cff906",
"sha256:f909ff9133ce0625ca388b6838190630ad7a593f87eaf058d872338a76241d5d"
],
"version": "==0.9.2"
"version": "==1.0.0"
},
"idna": {
"hashes": [

View File

@ -2,12 +2,12 @@
amqp==2.3.2
asana==0.6.7
asn1crypto==0.24.0
billiard==3.5.0.4
billiard==3.5.0.5
bleach==2.1.4
cairocffi==0.9.0
cairosvg==2.0.3
celery==4.0.2
certifi==2018.10.15
certifi==2018.11.29
cffi==1.11.5
chardet==3.0.4
contextlib2==0.5.5
@ -44,6 +44,7 @@ psd-tools==1.4
psycopg2-binary==2.7.5
pycparser==2.19
pygments==2.2.0
pyjwt==1.7.0
python-dateutil==2.7.5
python-magic==0.4.13
pytz==2018.7

View File

@ -148,7 +148,7 @@ LANGUAGES = [
("tr", "Türkçe"), # Turkish
#("tt", "татар теле"), # Tatar
#("udm", "удмурт кыл"), # Udmurt
#("uk", "Українська"), # Ukrainian
("uk", "Українська"), # Ukrainian
#("ur", "اردو‏"), # Urdu
#("vi", "Tiếng Việt"), # Vietnamese
("zh-hans", "中文(简体)"), # Simplified Chinese

View File

@ -375,14 +375,18 @@ class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdmi
class BaseRelatedFieldsFilter(FilterBackend):
filter_name = None
param_name = None
exclude_param_name = None
def __init__(self, filter_name=None, param_name=None):
def __init__(self, filter_name=None, param_name=None, exclude_param_name=None):
if filter_name:
self.filter_name = filter_name
if param_name:
self.param_name = param_name
if exclude_param_name:
self.exclude_param_name
def _prepare_filter_data(self, query_param_value):
def _transform_value(value):
try:
@ -396,48 +400,57 @@ class BaseRelatedFieldsFilter(FilterBackend):
values = map(_transform_value, values)
return list(values)
def _get_queryparams(self, params):
param_name = self.param_name or self.filter_name
def _get_queryparams(self, params, mode=''):
param_name = self.exclude_param_name if mode == 'exclude' else self.param_name or self.filter_name
raw_value = params.get(param_name, None)
if raw_value:
value = self._prepare_filter_data(raw_value)
if None in value:
qs_in_kwargs = {"{}__in".format(self.filter_name): [v for v in value if v is not None]}
qs_isnull_kwargs = {"{}__isnull".format(self.filter_name): True}
return Q(**qs_in_kwargs) | Q(**qs_isnull_kwargs)
else:
return {"{}__in".format(self.filter_name): value}
return Q(**{"{}__in".format(self.filter_name): value})
return None
def _prepare_filter_query(self, query):
return query
def _prepare_exclude_query(self, query):
return ~Q(query)
def filter_queryset(self, request, queryset, view):
query = self._get_queryparams(request.QUERY_PARAMS)
if query:
if isinstance(query, dict):
queryset = queryset.filter(**query)
else:
queryset = queryset.filter(query)
operations = {
"filter": self._prepare_filter_query,
"exclude": self._prepare_exclude_query,
}
for mode, prepare_method in operations.items():
query = self._get_queryparams(request.QUERY_PARAMS, mode=mode)
if query:
queryset = queryset.filter(prepare_method(query))
return super().filter_queryset(request, queryset, view)
class OwnersFilter(BaseRelatedFieldsFilter):
filter_name = 'owner'
exclude_param_name = 'exclude_owner'
class AssignedToFilter(BaseRelatedFieldsFilter):
filter_name = 'assigned_to'
exclude_param_name = 'exclude_assigned_to'
class AssignedUsersFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
filter_name = 'assigned_users'
exclude_param_name = 'exclude_assigned_users'
def _get_queryparams(self, params):
param_name = self.param_name or self.filter_name
def _get_queryparams(self, params, mode=''):
param_name = self.exclude_param_name if mode == 'exclude' else self.param_name or self.filter_name
raw_value = params.get(param_name, None)
if raw_value:
value = self._prepare_filter_data(raw_value)
UserStoryModel = apps.get_model("userstories", "UserStory")
@ -461,38 +474,65 @@ class AssignedUsersFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
class StatusesFilter(BaseRelatedFieldsFilter):
filter_name = 'status'
exclude_param_name = 'exclude_status'
class IssueTypesFilter(BaseRelatedFieldsFilter):
filter_name = 'type'
param_name = 'type'
exclude_param_name = 'exclude_type'
class PrioritiesFilter(BaseRelatedFieldsFilter):
filter_name = 'priority'
exclude_param_name = 'exclude_priority'
class SeveritiesFilter(BaseRelatedFieldsFilter):
filter_name = 'severity'
exclude_param_name = 'exclude_severity'
class TagsFilter(FilterBackend):
filter_name = 'tags'
exclude_param_name = 'exclude_tags'
def __init__(self, filter_name=None):
def __init__(self, filter_name=None, exclude_param_name=None):
if filter_name:
self.filter_name = filter_name
def _get_tags_queryparams(self, params):
tags = params.get(self.filter_name, None)
if exclude_param_name:
self.exclude_param_name = exclude_param_name
def _get_tags_queryparams(self, params, mode=''):
param_name = self.exclude_param_name if mode == "exclude" else self.filter_name
tags = params.get(param_name, None)
if tags:
return tags.split(",")
return None
def _prepare_filter_query(self, query):
return Q(tags__contains=query)
def _prepare_exclude_query(self, tags):
queries = [Q(tags__contains=[tag]) for tag in tags]
query = queries.pop()
for item in queries:
query |= item
return ~Q(query)
def filter_queryset(self, request, queryset, view):
query_tags = self._get_tags_queryparams(request.QUERY_PARAMS)
if query_tags:
queryset = queryset.filter(tags__contains=query_tags)
operations = {
"filter": self._prepare_filter_query,
"exclude": self._prepare_exclude_query,
}
for mode, prepare_method in operations.items():
query = self._get_tags_queryparams(request.QUERY_PARAMS, mode=mode)
if query:
queryset = queryset.filter(prepare_method(query))
return super().filter_queryset(request, queryset, view)
@ -631,18 +671,22 @@ class QFilter(FilterBackend):
class RoleFilter(BaseRelatedFieldsFilter):
filter_name = "role_id"
param_name = "role"
exclude_param_name = "exclude_role"
def filter_queryset(self, request, queryset, view):
Membership = apps.get_model('projects', 'Membership')
query = self._get_queryparams(request.QUERY_PARAMS)
if query:
if isinstance(query, dict):
memberships = Membership.objects.filter(**query).values_list("user_id", flat=True)
queryset = queryset.filter(assigned_to__in=memberships)
else:
memberships = Membership.objects.filter(query).values_list("user_id", flat=True)
if memberships:
queryset = queryset.filter(assigned_to__in=memberships)
operations = {
"filter": self._prepare_filter_query,
"exclude": self._prepare_exclude_query,
}
for mode, qs_method in operations.items():
query = self._get_queryparams(request.QUERY_PARAMS, mode=mode)
if query:
memberships = Membership.objects.filter(query).exclude(user__isnull=True).values_list("user_id", flat=True)
if memberships:
queryset = queryset.filter(qs_method(Q(assigned_to__in=memberships)))
return FilterBackend.filter_queryset(self, request, queryset, view)
@ -650,20 +694,24 @@ class RoleFilter(BaseRelatedFieldsFilter):
class UserStoriesRoleFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
filter_name = "role_id"
param_name = "role"
exclude_param_name = 'exclude_role'
def filter_queryset(self, request, queryset, view):
Membership = apps.get_model('projects', 'Membership')
query = self._get_queryparams(request.QUERY_PARAMS)
if query:
if isinstance(query, dict):
memberships = Membership.objects.filter(**query).values_list("user_id", flat=True)
else:
memberships = Membership.objects.filter(query).values_list("user_id", flat=True)
if memberships:
user_story_model = apps.get_model("userstories", "UserStory")
queryset = queryset.filter(
self.get_assigned_users_filter(user_story_model, memberships)
)
operations = {
"filter": self._prepare_filter_query,
"exclude": self._prepare_exclude_query,
}
for mode, qs_method in operations.items():
query = self._get_queryparams(request.QUERY_PARAMS, mode=mode)
if query:
memberships = Membership.objects.filter(query).exclude(user__isnull=True).values_list("user_id", flat=True)
if memberships:
user_story_model = apps.get_model("userstories", "UserStory")
queryset = queryset.filter(
qs_method(Q(self.get_assigned_users_filter(user_story_model, memberships)))
)
return FilterBackend.filter_queryset(self, request, queryset, view)

View File

@ -4,14 +4,16 @@
#
# Translators:
# Translators:
# Amirhoshang Hoseinpour Dehkordi <amir.hoseinpour@gmail.com>, 2018
# Vahid Dayyani <vahid.dayyani@ymail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"PO-Revision-Date: 2018-12-02 10:54+0000\n"
"Last-Translator: Amirhoshang Hoseinpour Dehkordi <amir.hoseinpour@gmail."
"com>\n"
"Language-Team: Persian (Iran) (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/fa_IR/)\n"
"MIME-Version: 1.0\n"
@ -26,7 +28,7 @@ msgstr "ثبت نام عمومی غیرفعال است."
#: taiga/auth/api.py:93
msgid "You must accept our terms of service and privacy policy"
msgstr ""
msgstr "شما باید موارد سرویس و سیاست های امنیت ما را قبول کنید."
#: taiga/auth/api.py:102
msgid "invalid register type"
@ -73,7 +75,7 @@ msgstr "نام کاربری نامعتبر"
#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "ضروری است. ۲۵۵ کاراکتر یا کمتر. حروف و اعداد و . و - و ـ مجاز است."
msgstr "255 کاراکتر یا کمتر ضروری است. حروف و اعداد و . و - و ـ مجاز است."
#: taiga/base/api/fields.py:294
msgid "This field is required."
@ -86,7 +88,7 @@ msgstr "مقدار نامعتبر."
#: taiga/base/api/fields.py:484
#, python-format
msgid "'%s' value must be either True or False."
msgstr "'%s' می‌بایست True (صحیح) یا False (غلط) باشد."
msgstr "'%s' می‌بایست صحیح یا غلط باشد."
#: taiga/base/api/fields.py:549
msgid ""
@ -517,91 +519,91 @@ msgstr ""
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
msgstr "خطای دسترسی به هاست"
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
msgstr "خطای آدرس آی پی"
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
msgstr "گزارش کاربر ساخته شد."
#: taiga/events/events.py:109
msgid "User story changed"
msgstr ""
msgstr "گزارش کاربر تغییر کرد."
#: taiga/events/events.py:112
msgid "User story deleted"
msgstr ""
msgstr "گزارش کاربر حذف شد."
#: taiga/events/events.py:114
msgid "US #{} - {}"
msgstr ""
msgstr "US #{} - {}"
#: taiga/events/events.py:117
msgid "Task created"
msgstr ""
msgstr "وظیفه ساخته شد."
#: taiga/events/events.py:120
msgid "Task changed"
msgstr ""
msgstr "وظیفه تغییر کرد."
#: taiga/events/events.py:123
msgid "Task deleted"
msgstr ""
msgstr "وظیفه حذف شد."
#: taiga/events/events.py:125
msgid "Task #{} - {}"
msgstr ""
msgstr "وظیفه #{} - {}"
#: taiga/events/events.py:128
msgid "Issue created"
msgstr ""
msgstr "موضوع ساخته شد."
#: taiga/events/events.py:131
msgid "Issue changed"
msgstr ""
msgstr "موضوع تغییر کرد."
#: taiga/events/events.py:134
msgid "Issue deleted"
msgstr ""
msgstr "موضوع حذف شد."
#: taiga/events/events.py:136
msgid "Issue: #{} - {}"
msgstr ""
msgstr "موضوع: #{} - {}"
#: taiga/events/events.py:139
msgid "Wiki Page created"
msgstr ""
msgstr "صفحه ویکی ساخته شد."
#: taiga/events/events.py:142
msgid "Wiki Page changed"
msgstr ""
msgstr "صفحه ویکی تغییر داده شد."
#: taiga/events/events.py:145
msgid "Wiki Page deleted"
msgstr ""
msgstr "صفحه ویکی حذف شد."
#: taiga/events/events.py:147
msgid "Wiki Page: {}"
msgstr ""
msgstr "صفحه ویکی: {}"
#: taiga/events/events.py:150
msgid "Sprint created"
msgstr ""
msgstr "سرعتی ساخته شد."
#: taiga/events/events.py:153
msgid "Sprint changed"
msgstr ""
msgstr "سرعتی تغییر داده شد."
#: taiga/events/events.py:156
msgid "Sprint deleted"
msgstr ""
msgstr "سرعتی حذف شد."
#: taiga/events/events.py:158
msgid "Sprint: {}"
msgstr ""
msgstr "سرعتی: {}"
#: taiga/export_import/api.py:127
msgid "We needed at least one role"
@ -1782,27 +1784,27 @@ msgstr "کاربر می‌بایست قبلاً از اعضای پروژه بو
#: taiga/projects/api.py:642
msgid "You can't delete user story due date by default"
msgstr ""
msgstr "شما سر رسید گزارش کاربر را نمی توانید بصورت عادی حذف کنید."
#: taiga/projects/api.py:658
msgid "Project already have due dates"
msgstr ""
msgstr "پروژه موعد سر رسید دارد."
#: taiga/projects/api.py:718
msgid "You can't delete task due date by default"
msgstr ""
msgstr "شما سر رسید وظیفه کاربر را نمی توانید بصورت عادی حذف کنید."
#: taiga/projects/api.py:734
msgid "Project already have task due dates"
msgstr ""
msgstr "پروژه موعد سر رسید وظیفه دارد."
#: taiga/projects/api.py:858
msgid "You can't delete issue due date by default"
msgstr ""
msgstr "شما سر رسید موضوع کاربر را نمی توانید بصورت عادی حذف کنید."
#: taiga/projects/api.py:874
msgid "Project already have issue due dates"
msgstr ""
msgstr "پروژه موعد سر رسید موضوع دارد."
#: taiga/projects/api.py:1023
msgid ""
@ -2051,11 +2053,11 @@ msgstr "موردی با همین نام وجود دارد."
#: taiga/projects/due_dates/models.py:21
msgid "due date"
msgstr ""
msgstr "موعد سر رسید"
#: taiga/projects/due_dates/models.py:24
msgid "reason for the due date"
msgstr ""
msgstr "دلیل موعد سر رسید."
#: taiga/projects/epics/api.py:94
msgid "You don't have permissions to set this status to this epic."
@ -2212,7 +2214,7 @@ msgstr "آزاد شد"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:164
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:175
msgid "Not set"
msgstr ""
msgstr "تنظیم نشده"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:286
#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:91
@ -2567,12 +2569,12 @@ msgstr "ارزش"
#: taiga/projects/models.py:614 taiga/projects/models.py:671
#: taiga/projects/models.py:789
msgid "by default"
msgstr ""
msgstr "بصورت عمومی"
#: taiga/projects/models.py:618 taiga/projects/models.py:675
#: taiga/projects/models.py:793
msgid "days to due"
msgstr ""
msgstr "روز تا موعد"
#: taiga/projects/models.py:823
msgid "default owner's role"
@ -2592,7 +2594,7 @@ msgstr "وضعیت‌های استوری‌های کاربری"
#: taiga/projects/models.py:849
msgid "us duedates"
msgstr ""
msgstr "موعد ما"
#: taiga/projects/models.py:850 taiga/projects/userstories/models.py:45
#: taiga/projects/userstories/models.py:78
@ -2605,7 +2607,7 @@ msgstr "وضعیت‌های وظایف"
#: taiga/projects/models.py:852
msgid "task duedates"
msgstr ""
msgstr "موعد وظیفه ها"
#: taiga/projects/models.py:853
msgid "issue statuses"
@ -2617,7 +2619,7 @@ msgstr "انواع موضوعات"
#: taiga/projects/models.py:855
msgid "issue duedates"
msgstr ""
msgstr "موعد موضوعات"
#: taiga/projects/models.py:856
msgid "priorities"
@ -4221,7 +4223,7 @@ msgstr "؟"
#. Translators: User story point value (value = 0)
#: taiga/projects/translations.py:47
msgid "0"
msgstr "۰"
msgstr "0"
#. Translators: User story point value (value = 0.5)
#: taiga/projects/translations.py:49
@ -4461,7 +4463,7 @@ msgstr "تاریخ تکمیل"
#: taiga/projects/userstories/models.py:102
msgid "assigned users"
msgstr ""
msgstr "کاربران متصل شده"
#: taiga/projects/userstories/models.py:111
msgid "generated from issue"
@ -4724,11 +4726,11 @@ msgstr "تاریخ عضویت"
#: taiga/users/models.py:155
msgid "accepted terms"
msgstr ""
msgstr "قبول شرایط"
#: taiga/users/models.py:156
msgid "new terms read"
msgstr ""
msgstr "خواندن شرایط جدید"
#: taiga/users/models.py:158
msgid "default language"
@ -4972,7 +4974,7 @@ msgstr "نام کاربری نامعتبر. نام کاربری دیگری ان
#: taiga/users/validators.py:73
msgid "Read new terms has to be true'"
msgstr ""
msgstr "خواندن شرایط جدید باید صحیح باشد"
#: taiga/userstorage/api.py:53
msgid ""
@ -5017,4 +5019,4 @@ msgstr "مدت زمان"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""
msgstr "آی پی آدرس غیر مجاز"

View File

@ -10,13 +10,14 @@
# Shun Yanaura <metroplexity@gmail.com>, 2016
# Suguru Sato <usagi.vs.tanuki@gmail.com>, 2016
# Tomonori Tanabe <tanb+github@me.com>, 2015
# Masaki Honda <mh35jp@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"PO-Revision-Date: 2018-12-28 01:50+0000\n"
"Last-Translator: Masaki Honda <mh35jp@gmail.com>\n"
"Language-Team: Japanese (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ja/)\n"
"MIME-Version: 1.0\n"
@ -31,7 +32,7 @@ msgstr "パブリックなレジスタは無効です。"
#: taiga/auth/api.py:93
msgid "You must accept our terms of service and privacy policy"
msgstr ""
msgstr "利用規約とプライバシーポリシーに同意する必要があります"
#: taiga/auth/api.py:102
msgid "invalid register type"

View File

@ -18,7 +18,7 @@
# Lucas Boscaini <lucasboscaini@gmail.com>, 2017
# Mairieli Wessel <mairieliw@alunos.utfpr.edu.br>, 2016
# Marlon Carvalho <m.lopes@qiwi.com>, 2015
# Michael Meneses <michael.douglas.meneses.2@gmail.com>, 2018
# Michael Douglas Meneses de Souza <michael.douglas.meneses.2@gmail.com>, 2018
# Michel Wilhelm <michelwilhelm@gmail.com>, 2016
# pedromvm <pedromvm@gmail.com>, 2015
# Pedro Rangel Raft <me@pedroraft.com>, 2017
@ -32,7 +32,8 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-11-17 17:51+0000\n"
"Last-Translator: Michael Meneses <michael.douglas.meneses.2@gmail.com>\n"
"Last-Translator: Michael Douglas Meneses de Souza <michael.douglas."
"meneses.2@gmail.com>\n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/pt_BR/)\n"
"MIME-Version: 1.0\n"

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
#
# Translators:
# Translators:
# Andy zhan <daliangzao189@126.com>, 2018
# Ares <yangzibin.cn@hotmail.com>, 2017
# gm l <linguangmo@gmail.com>, 2016
# Hanbing Yin <yin_suk@hotmail.com>, 2016
@ -25,8 +26,8 @@ msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"PO-Revision-Date: 2018-12-16 07:10+0000\n"
"Last-Translator: Andy zhan <daliangzao189@126.com>\n"
"Language-Team: Chinese Simplified (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/zh-Hans/)\n"
"MIME-Version: 1.0\n"
@ -4649,11 +4650,11 @@ msgstr "加入日期"
#: taiga/users/models.py:155
msgid "accepted terms"
msgstr ""
msgstr "已接受的条款"
#: taiga/users/models.py:156
msgid "new terms read"
msgstr ""
msgstr "阅读新条款"
#: taiga/users/models.py:158
msgid "default language"
@ -4894,7 +4895,7 @@ msgstr "无效用户名,请尝试其他的。"
#: taiga/users/validators.py:73
msgid "Read new terms has to be true'"
msgstr ""
msgstr "还没有阅读新条款"
#: taiga/userstorage/api.py:53
msgid ""
@ -4939,4 +4940,4 @@ msgstr "持续时间"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""
msgstr "IP地址被禁用"

View File

@ -25,6 +25,11 @@ def connect_issues_signals():
from taiga.projects.tagging import signals as tagging_handlers
from . import signals as handlers
# Cached prev object version
signals.pre_save.connect(handlers.cached_prev_issue,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="cached_prev_issue")
# Finished date
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
sender=apps.get_model("issues", "Issue"),
@ -35,6 +40,14 @@ def connect_issues_signals():
sender=apps.get_model("issues", "Issue"),
dispatch_uid="tags_normalization_issue")
# Open/Close US and Milestone
signals.post_save.connect(handlers.try_to_close_or_open_milestone_when_create_or_edit_issue,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="try_to_close_or_open_milestone_when_create_or_edit_issue")
signals.post_delete.connect(handlers.try_to_close_milestone_when_delete_issue,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="try_to_close_milestone_when_delete_issue")
def connect_issues_custom_attributes_signals():
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
@ -50,11 +63,19 @@ def connect_all_issues_signals():
def disconnect_issues_signals():
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="cached_prev_issue")
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="set_finished_date_when_edit_issue")
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="tags_normalization_issue")
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="try_to_close_or_open_milestone_when_create_or_edit_issue")
signals.post_delete.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="try_to_close_milestone_when_delete_issue")
def disconnect_issues_custom_attributes_signals():
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"),

View File

@ -16,9 +16,22 @@
# 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 contextlib import suppress
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
####################################
# Signals for cached prev task
####################################
# Define the previous version of the task for use it on the post_save handler
def cached_prev_issue(sender, instance, **kwargs):
instance.prev = None
if instance.id:
instance.prev = sender.objects.get(id=instance.id)
####################################
# Signals for set finished date
####################################
@ -30,3 +43,48 @@ def set_finished_date_when_edit_issue(sender, instance, **kwargs):
instance.finished_date = timezone.now()
elif not instance.status.is_closed and instance.finished_date:
instance.finished_date = None
def try_to_close_or_open_milestone_when_create_or_edit_issue(sender, instance, created, **kwargs):
if instance._importing:
return
_try_to_close_or_open_milestone_when_create_or_edit_issue(instance)
def try_to_close_milestone_when_delete_issue(sender, instance, **kwargs):
if instance._importing:
return
_try_to_close_milestone_when_delete_issue(instance)
# Milestone
def _try_to_close_or_open_milestone_when_create_or_edit_issue(instance):
if instance._importing:
return
from taiga.projects.milestones import services as milestone_service
if instance.milestone_id:
if milestone_service.calculate_milestone_is_closed(instance.milestone):
milestone_service.close_milestone(instance.milestone)
else:
milestone_service.open_milestone(instance.milestone)
if instance.prev and instance.prev.milestone_id and instance.prev.milestone_id != instance.milestone_id:
if milestone_service.calculate_milestone_is_closed(instance.prev.milestone):
milestone_service.close_milestone(instance.prev.milestone)
else:
milestone_service.open_milestone(instance.prev.milestone)
def _try_to_close_milestone_when_delete_issue(instance):
if instance._importing:
return
from taiga.projects.milestones import services as milestone_service
with suppress(ObjectDoesNotExist):
if instance.milestone_id and milestone_service.calculate_milestone_is_closed(instance.milestone):
milestone_service.close_milestone(instance.milestone)

View File

@ -27,11 +27,17 @@ from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.base.utils.db import get_object_or_none
from taiga.projects.models import Project
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.tasks.validators import UpdateMilestoneBulkValidator as \
TasksUpdateMilestoneValidator
from taiga.projects.issues.validators import UpdateMilestoneBulkValidator as \
IssuesUpdateMilestoneValidator
from . import serializers
from . import services
from . import validators
from . import models
from . import permissions
@ -142,6 +148,69 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
return response.Ok(milestone_stats)
@detail_route(methods=["POST"])
def move_userstories_to_sprint(self, request, pk=None, **kwargs):
milestone = get_object_or_404(models.Milestone, pk=pk)
self.check_permissions(request, "move_related_items", milestone)
validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
if not validator.is_valid():
return response.BadRequest(validator.errors)
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone_result = get_object_or_404(models.Milestone, pk=data["milestone_id"])
if data["bulk_stories"]:
self.check_permissions(request, "move_uss_to_sprint", project)
services.update_userstories_milestone_in_bulk(data["bulk_stories"], milestone_result)
services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user)
return response.NoContent()
@detail_route(methods=["POST"])
def move_tasks_to_sprint(self, request, pk=None, **kwargs):
milestone = get_object_or_404(models.Milestone, pk=pk)
self.check_permissions(request, "move_related_items", milestone)
validator = TasksUpdateMilestoneValidator(data=request.DATA)
if not validator.is_valid():
return response.BadRequest(validator.errors)
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone_result = get_object_or_404(models.Milestone, pk=data["milestone_id"])
if data["bulk_tasks"]:
self.check_permissions(request, "move_tasks_to_sprint", project)
services.update_tasks_milestone_in_bulk(data["bulk_tasks"], milestone_result)
services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user)
return response.NoContent()
@detail_route(methods=["POST"])
def move_issues_to_sprint(self, request, pk=None, **kwargs):
milestone = get_object_or_404(models.Milestone, pk=pk)
self.check_permissions(request, "move_related_items", milestone)
validator = IssuesUpdateMilestoneValidator(data=request.DATA)
if not validator.is_valid():
return response.BadRequest(validator.errors)
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone_result = get_object_or_404(models.Milestone, pk=data["milestone_id"])
if data["bulk_issues"]:
self.check_permissions(request, "move_issues_to_sprint", project)
services.update_issues_milestone_in_bulk(data["bulk_issues"], milestone_result)
services.snapshot_issues_in_bulk(data["bulk_issues"], request.user)
return response.NoContent()
class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.MilestoneWatchersPermission,)

View File

@ -33,6 +33,11 @@ class MilestonePermission(TaigaResourcePermission):
stats_perms = HasProjectPerm('view_milestones')
watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
move_related_items_perms = HasProjectPerm('modify_milestone')
move_uss_to_sprint_perms = HasProjectPerm('modify_us')
move_tasks_to_sprint_perms = HasProjectPerm('modify_task')
move_issues_to_sprint_perms = HasProjectPerm('modify_issue')
class MilestoneWatchersPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()

View File

@ -16,16 +16,29 @@
# 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.utils import timezone
from . import models
from taiga.base.utils import db
from taiga.events import events
from taiga.projects.history.services import take_snapshot
from taiga.projects.services import apply_order_updates
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
from taiga.projects.userstories.models import UserStory
def calculate_milestone_is_closed(milestone):
return (milestone.user_stories.all().count() > 0 and
all([task.status is not None and task.status.is_closed for task in milestone.tasks.all()]) and
all([user_story.is_closed for user_story in milestone.user_stories.all()]))
all_us_closed = all([user_story.is_closed for user_story in milestone.user_stories.all()])
all_tasks_closed = all([task.status is not None and task.status.is_closed for task in
milestone.tasks.all()])
all_issues_closed = all([issue.is_closed for issue in milestone.issues.all()])
uss_check = milestone.user_stories.all().count() > 0 \
and all_tasks_closed and all_us_closed and all_issues_closed
issues_check = milestone.issues.all().count() > 0 and all_issues_closed \
and all_tasks_closed and all_us_closed
tasks_check = milestone.tasks.all().count() > 0 and all_tasks_closed \
and all_issues_closed and all_us_closed
return uss_check or issues_check or tasks_check
def close_milestone(milestone):
@ -38,3 +51,137 @@ def open_milestone(milestone):
if milestone.closed:
milestone.closed = False
milestone.save(update_fields=["closed",])
def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone and the milestone order of some user stories adding
the extra orders needed to keep consistency.
`bulk_data` should be a list of dicts with the following format:
[{'us_id': <value>, 'order': <value>}, ...]
"""
user_stories = milestone.user_stories.all()
us_orders = {us.id: getattr(us, "sprint_order") for us in user_stories}
new_us_orders = {}
for e in bulk_data:
new_us_orders[e["us_id"]] = e["order"]
# The base orders where we apply the new orders must containg all
# the values
us_orders[e["us_id"]] = e["order"]
apply_order_updates(us_orders, new_us_orders)
us_milestones = {e["us_id"]: milestone.id for e in bulk_data}
user_story_ids = us_milestones.keys()
events.emit_event_for_ids(ids=user_story_ids,
content_type="userstories.userstory",
projectid=milestone.project.pk)
us_instance_list = []
us_values = []
for us_id in user_story_ids:
us = UserStory.objects.get(pk=us_id)
us_instance_list.append(us)
us_values.append({'milestone_id': milestone.id})
db.update_in_bulk(us_instance_list, us_values)
db.update_attr_in_bulk_for_ids(us_orders, "sprint_order", UserStory)
# Updating the milestone for the tasks
Task.objects.filter(
user_story_id__in=[e["us_id"] for e in bulk_data]).update(
milestone=milestone)
return us_orders
def snapshot_userstories_in_bulk(bulk_data, user):
for us_data in bulk_data:
try:
us = UserStory.objects.get(pk=us_data['us_id'])
take_snapshot(us, user=user)
except UserStory.DoesNotExist:
pass
def update_tasks_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone and the milestone order of some tasks adding
the extra orders needed to keep consistency.
`bulk_data` should be a list of dicts with the following format:
[{'task_id': <value>, 'order': <value>}, ...]
"""
tasks = milestone.tasks.all()
task_orders = {task.id: getattr(task, "taskboard_order") for task in tasks}
new_task_orders = {}
for e in bulk_data:
new_task_orders[e["task_id"]] = e["order"]
# The base orders where we apply the new orders must containg all
# the values
task_orders[e["task_id"]] = e["order"]
apply_order_updates(task_orders, new_task_orders)
task_milestones = {e["task_id"]: milestone.id for e in bulk_data}
task_ids = task_milestones.keys()
events.emit_event_for_ids(ids=task_ids,
content_type="tasks.task",
projectid=milestone.project.pk)
task_instance_list = []
task_values = []
for task_id in task_ids:
task = Task.objects.get(pk=task_id)
task_instance_list.append(task)
task_values.append({'milestone_id': milestone.id})
db.update_in_bulk(task_instance_list, task_values)
db.update_attr_in_bulk_for_ids(task_orders, "taskboard_order", Task)
return task_milestones
def snapshot_tasks_in_bulk(bulk_data, user):
for task_data in bulk_data:
try:
task = Task.objects.get(pk=task_data['task_id'])
take_snapshot(task, user=user)
except Task.DoesNotExist:
pass
def update_issues_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone some issues adding
`bulk_data` should be a list of dicts with the following format:
[{'task_id': <value>}, ...]
"""
issue_milestones = {e["issue_id"]: milestone.id for e in bulk_data}
issue_ids = issue_milestones.keys()
events.emit_event_for_ids(ids=issue_ids,
content_type="issues.issues",
projectid=milestone.project.pk)
issues_instance_list = []
issues_values = []
for issue_id in issue_ids:
issue = Issue.objects.get(pk=issue_id)
issues_instance_list.append(issue)
issues_values.append({'milestone_id': milestone.id})
db.update_in_bulk(issues_instance_list, issues_values)
return issue_milestones
def snapshot_issues_in_bulk(bulk_data, user):
for issue_data in bulk_data:
try:
issue = Issue.objects.get(pk=issue_data['issue_id'])
take_snapshot(issue, user=user)
except Issue.DoesNotExist:
pass

View File

@ -19,15 +19,17 @@
from django.utils.translation import ugettext as _
from taiga.base.exceptions import ValidationError
from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.projects.validators import DuplicatedNameInProjectValidator
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.userstories.models import UserStory
from taiga.projects.validators import DuplicatedNameInProjectValidator
from taiga.projects.validators import ProjectExistsValidator
from . import models
class MilestoneExistsValidator:
def validate_sprint_id(self, attrs, source):
def validate_milestone_id(self, attrs, source):
value = attrs[source]
if not models.Milestone.objects.filter(pk=value).exists():
msg = _("There's no milestone with that id")
@ -39,3 +41,28 @@ class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, va
class Meta:
model = models.Milestone
read_only_fields = ("id", "created_date", "modified_date")
# bulk validators
class _UserStoryMilestoneBulkValidator(validators.Validator):
us_id = serializers.IntegerField()
order = serializers.IntegerField()
class UpdateMilestoneBulkValidator(MilestoneExistsValidator,
ProjectExistsValidator,
validators.Validator):
project_id = serializers.IntegerField()
milestone_id = serializers.IntegerField()
bulk_stories = _UserStoryMilestoneBulkValidator(many=True)
def validate_bulk_stories(self, attrs, source):
filters = {
"project__id": attrs["project_id"],
"id__in": [us["us_id"] for us in attrs[source]]
}
if UserStory.objects.filter(**filters).count() != len(filters["id__in"]):
raise ValidationError(_("All the user stories must be from the same project"))
return attrs

View File

@ -44,6 +44,10 @@ from .models import HistoryChangeNotification, Watched
from .squashing import squash_history_entries
def remove_lr_cr(s):
return s.replace("\n", "").replace("\r", "")
def notify_policy_exists(project, user) -> bool:
"""
Check if policy exists for specified project
@ -313,10 +317,11 @@ def send_sync_notifications(notification_id):
msg_id = 'taiga-system'
now = datetime.datetime.now()
project_name = remove_lr_cr(notification.project.name)
format_args = {
"unsubscribe_url": resolve_front_url('settings-mail-notifications'),
"project_slug": notification.project.slug,
"project_name": notification.project.name,
"project_name": project_name,
"msg_id": msg_id,
"time": int(now.timestamp()),
"domain": domain

View File

@ -91,3 +91,7 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, DueDateM
def __str__(self):
return "({1}) {0}".format(self.ref, self.subject)
@property
def is_closed(self):
return self.status is not None and self.status.is_closed

View File

@ -31,9 +31,9 @@ def cached_prev_task(sender, instance, **kwargs):
instance.prev = sender.objects.get(id=instance.id)
####################################
# Signals for close US and Milestone
####################################
######################################
# Signals for close Task and Milestone
######################################
def try_to_close_or_open_us_and_milestone_when_create_or_edit_task(sender, instance, created, **kwargs):
_try_to_close_or_open_us_when_create_or_edit_task(instance)

View File

@ -72,7 +72,6 @@ def update_milestone_of_tasks_when_edit_us(sender, instance, created, **kwargs):
def try_to_close_or_open_us_and_milestone_when_create_or_edit_us(sender, instance, created, **kwargs):
if instance._importing:
return
_try_to_close_or_open_us_when_create_or_edit_us(instance)
_try_to_close_or_open_milestone_when_create_or_edit_us(instance)

View File

@ -626,6 +626,10 @@ def create_issue(**kwargs):
return IssueFactory.create(**defaults)
class Missing:
pass
def create_task(**kwargs):
"Create a task and along with its dependencies."
owner = kwargs.pop("owner", None)
@ -636,13 +640,23 @@ def create_task(**kwargs):
if project is None:
project = ProjectFactory.create(owner=owner)
status = kwargs.pop("status", None)
milestone = kwargs.pop("milestone", None)
defaults = {
"project": project,
"owner": owner,
"status": TaskStatusFactory.create(project=project),
"milestone": MilestoneFactory.create(project=project),
"user_story": UserStoryFactory.create(project=project, owner=owner),
"status": status or TaskStatusFactory.create(project=project),
"milestone": milestone or MilestoneFactory.create(project=project),
}
user_story = kwargs.pop("user_story", Missing)
defaults["user_story"] = (
UserStoryFactory.create(project=project, owner=owner, milestone=defaults["milestone"])
if user_story is Missing
else user_story
)
defaults.update(kwargs)
return TaskFactory.create(**defaults)

View File

@ -39,6 +39,78 @@ import pytest
pytestmark = pytest.mark.django_db
def create_filter_issues_context():
data = {}
data["project"] = f.ProjectFactory.create()
project = data["project"]
data["users"] = [f.UserFactory.create(is_superuser=True) for i in range(0, 3)]
data["roles"] = [f.RoleFactory.create() for i in range(0, 3)]
user_roles = zip(data["users"], data["roles"])
# Add membership fixtures
[f.MembershipFactory.create(user=user, project=project, role=role) for (user, role) in user_roles]
data["statuses"] = [f.IssueStatusFactory.create(project=project) for i in range(0, 4)]
data["types"] = [f.IssueTypeFactory.create(project=project) for i in range(0, 2)]
data["severities"] = [f.SeverityFactory.create(project=project) for i in range(0, 4)]
data["priorities"] = [f.PriorityFactory.create(project=project) for i in range(0, 4)]
data["tags"] = ["test1test2test3", "test1", "test2", "test3"]
# ------------------------------------------------------------------------------------------------
# | Issue | Owner | Assigned To | Status | Type | Priority | Severity | Tags |
# |-------#--------#-------------#---------#-------#-----------#-----------#---------------------|
# | 0 | user2 | None | status3 | type1 | priority2 | severity1 | tag1 |
# | 1 | user1 | None | status3 | type2 | priority2 | severity1 | tag2 |
# | 2 | user3 | None | status1 | type1 | priority3 | severity2 | tag1 tag2 |
# | 3 | user2 | None | status0 | type2 | priority3 | severity1 | tag3 |
# | 4 | user1 | user1 | status0 | type1 | priority2 | severity3 | tag1 tag2 tag3 |
# | 5 | user3 | user1 | status2 | type2 | priority3 | severity2 | tag3 |
# | 6 | user2 | user1 | status3 | type1 | priority2 | severity0 | tag1 tag2 |
# | 7 | user1 | user2 | status0 | type2 | priority1 | severity3 | tag3 |
# | 8 | user3 | user2 | status3 | type1 | priority0 | severity1 | tag1 |
# | 9 | user2 | user3 | status1 | type2 | priority0 | severity2 | tag0 |
# ------------------------------------------------------------------------------------------------
(user1, user2, user3, ) = data["users"]
(status0, status1, status2, status3 ) = data["statuses"]
(type1, type2, ) = data["types"]
(severity0, severity1, severity2, severity3, ) = data["severities"]
(priority0, priority1, priority2, priority3, ) = data["priorities"]
(tag0, tag1, tag2, tag3, ) = data["tags"]
f.IssueFactory.create(project=project, owner=user2, assigned_to=None,
status=status3, type=type1, priority=priority2, severity=severity1,
tags=[tag1])
f.IssueFactory.create(project=project, owner=user1, assigned_to=None,
status=status3, type=type2, priority=priority2, severity=severity1,
tags=[tag2])
f.IssueFactory.create(project=project, owner=user3, assigned_to=None,
status=status1, type=type1, priority=priority3, severity=severity2,
tags=[tag1, tag2])
f.IssueFactory.create(project=project, owner=user2, assigned_to=None,
status=status0, type=type2, priority=priority3, severity=severity1,
tags=[tag3])
f.IssueFactory.create(project=project, owner=user1, assigned_to=user1,
status=status0, type=type1, priority=priority2, severity=severity3,
tags=[tag1, tag2, tag3])
f.IssueFactory.create(project=project, owner=user3, assigned_to=user1,
status=status2, type=type2, priority=priority3, severity=severity2,
tags=[tag3])
f.IssueFactory.create(project=project, owner=user2, assigned_to=user1,
status=status3, type=type1, priority=priority2, severity=severity0,
tags=[tag1, tag2])
f.IssueFactory.create(project=project, owner=user1, assigned_to=user2,
status=status0, type=type2, priority=priority1, severity=severity3,
tags=[tag3])
f.IssueFactory.create(project=project, owner=user3, assigned_to=user2,
status=status3, type=type1, priority=priority0, severity=severity1,
tags=[tag1])
f.IssueFactory.create(project=project, owner=user2, assigned_to=user3,
status=status1, type=type2, priority=priority0, severity=severity2,
tags=[tag0])
return data
def test_get_issues_from_bulk():
data = """
Issue #1
@ -370,86 +442,63 @@ def test_api_filter_by_finished_date(client):
assert response.data[0]["ref"] == finished_issue.ref
@pytest.mark.parametrize("filter_name,collection,expected,exclude_expected,is_text", [
('type', 'types', 5, 5, False),
('severity', 'severities', 1, 9, False),
('priority', 'priorities', 2, 8, False),
('status', 'statuses', 3, 7, False),
('assigned_to', 'users', 3, 7, False),
('tags', 'tags', 1, 9, True),
('owner', 'users', 3, 7, False),
('role', 'roles', 3, 7, False),
])
def test_api_filters(client, filter_name, collection, expected, exclude_expected, is_text):
data = create_filter_issues_context()
project = data["project"]
options = data[collection]
client.login(data["users"][0])
if is_text:
param = options[0]
else:
param = options[0].id
# include test
url = "{}?project={}&{}={}".format(reverse('issues-list'), project.id, filter_name, param)
response = client.get(url)
assert response.status_code == 200
assert len(response.data) == expected
# exclude test
url = "{}?project={}&exclude_{}={}".format(reverse('issues-list'), project.id, filter_name, param)
response = client.get(url)
assert response.status_code == 200
assert len(response.data) == exclude_expected
def test_mulitple_exclude_filter_tags(client):
data = create_filter_issues_context()
project = data["project"]
client.login(data["users"][0])
tags = data["tags"]
url = "{}?project={}&exclude_tags={},{}".format(reverse('issues-list'), project.id, tags[1], tags[2])
response = client.get(url)
assert response.status_code == 200
assert len(response.data) == 4
def test_api_filters_data(client):
project = f.ProjectFactory.create()
user1 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user1, project=project)
user2 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user2, project=project)
user3 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user3, project=project)
status0 = f.IssueStatusFactory.create(project=project)
status1 = f.IssueStatusFactory.create(project=project)
status2 = f.IssueStatusFactory.create(project=project)
status3 = f.IssueStatusFactory.create(project=project)
type1 = f.IssueTypeFactory.create(project=project)
type2 = f.IssueTypeFactory.create(project=project)
severity0 = f.SeverityFactory.create(project=project)
severity1 = f.SeverityFactory.create(project=project)
severity2 = f.SeverityFactory.create(project=project)
severity3 = f.SeverityFactory.create(project=project)
priority0 = f.PriorityFactory.create(project=project)
priority1 = f.PriorityFactory.create(project=project)
priority2 = f.PriorityFactory.create(project=project)
priority3 = f.PriorityFactory.create(project=project)
tag0 = "test1test2test3"
tag1 = "test1"
tag2 = "test2"
tag3 = "test3"
# ------------------------------------------------------------------------------------------------
# | Issue | Owner | Assigned To | Status | Type | Priority | Severity | Tags |
# |-------#--------#-------------#---------#-------#-----------#-----------#---------------------|
# | 0 | user2 | None | status3 | type1 | priority2 | severity1 | tag1 |
# | 1 | user1 | None | status3 | type2 | priority2 | severity1 | tag2 |
# | 2 | user3 | None | status1 | type1 | priority3 | severity2 | tag1 tag2 |
# | 3 | user2 | None | status0 | type2 | priority3 | severity1 | tag3 |
# | 4 | user1 | user1 | status0 | type1 | priority2 | severity3 | tag1 tag2 tag3 |
# | 5 | user3 | user1 | status2 | type2 | priority3 | severity2 | tag3 |
# | 6 | user2 | user1 | status3 | type1 | priority2 | severity0 | tag1 tag2 |
# | 7 | user1 | user2 | status0 | type2 | priority1 | severity3 | tag3 |
# | 8 | user3 | user2 | status3 | type1 | priority0 | severity1 | tag1 |
# | 9 | user2 | user3 | status1 | type2 | priority0 | severity2 | tag0 |
# ------------------------------------------------------------------------------------------------
issue0 = f.IssueFactory.create(project=project, owner=user2, assigned_to=None,
status=status3, type=type1, priority=priority2, severity=severity1,
tags=[tag1])
issue1 = f.IssueFactory.create(project=project, owner=user1, assigned_to=None,
status=status3, type=type2, priority=priority2, severity=severity1,
tags=[tag2])
issue2 = f.IssueFactory.create(project=project, owner=user3, assigned_to=None,
status=status1, type=type1, priority=priority3, severity=severity2,
tags=[tag1, tag2])
issue3 = f.IssueFactory.create(project=project, owner=user2, assigned_to=None,
status=status0, type=type2, priority=priority3, severity=severity1,
tags=[tag3])
issue4 = f.IssueFactory.create(project=project, owner=user1, assigned_to=user1,
status=status0, type=type1, priority=priority2, severity=severity3,
tags=[tag1, tag2, tag3])
issue5 = f.IssueFactory.create(project=project, owner=user3, assigned_to=user1,
status=status2, type=type2, priority=priority3, severity=severity2,
tags=[tag3])
issue6 = f.IssueFactory.create(project=project, owner=user2, assigned_to=user1,
status=status3, type=type1, priority=priority2, severity=severity0,
tags=[tag1, tag2])
issue7 = f.IssueFactory.create(project=project, owner=user1, assigned_to=user2,
status=status0, type=type2, priority=priority1, severity=severity3,
tags=[tag3])
issue8 = f.IssueFactory.create(project=project, owner=user3, assigned_to=user2,
status=status3, type=type1, priority=priority0, severity=severity1,
tags=[tag1])
issue9 = f.IssueFactory.create(project=project, owner=user2, assigned_to=user3,
status=status1, type=type2, priority=priority0, severity=severity2,
tags=[tag0])
data = create_filter_issues_context()
project = data["project"]
(user1, user2, user3, ) = data["users"]
(status0, status1, status2, status3, ) = data["statuses"]
(type1, type2, ) = data["types"]
(priority0, priority1, priority2, priority3, ) = data["priorities"]
(severity0, severity1, severity2, severity3, ) = data["severities"]
(tag0, tag1, tag2, tag3, ) = data["tags"]
url = reverse("issues-filters-data") + "?project={}".format(project.id)
client.login(user1)
## No filter

View File

@ -26,7 +26,6 @@ from urllib.parse import quote
from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects.userstories.serializers import UserStorySerializer
from .. import factories as f
@ -180,3 +179,211 @@ def test_api_filter_by_milestone__estimated_start_and_end(client, field_name):
assert number_of_milestones == expection, param
if number_of_milestones > 0:
assert response.data[0]["slug"] == milestone.slug
def test_api_update_milestone_in_bulk_userstories(client):
project = f.create_project()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
milestone2 = f.MilestoneFactory.create(project=project)
us1 = f.create_userstory(project=project, milestone=milestone1,
sprint_order=1)
us2 = f.create_userstory(project=project, milestone=milestone1,
sprint_order=2)
assert project.milestones.get(id=milestone1.id).user_stories.count() == 2
url = reverse("milestones-move-userstories-to-sprint", kwargs={"pk": milestone1.pk})
data = {
"project_id": project.id,
"milestone_id": milestone2.id,
"bulk_stories": [{"us_id": us2.id, "order": 2}]
}
client.login(project.owner)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 204, response.data
assert project.milestones.get(id=milestone1.id).user_stories.count() == 1
assert project.milestones.get(id=milestone2.id).user_stories.count() == 1
def test_api_move_userstories_to_another_sprint(client):
project = f.create_project()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
milestone2 = f.MilestoneFactory.create(project=project)
us1 = f.create_userstory(project=project, milestone=milestone1,
sprint_order=1)
us2 = f.create_userstory(project=project, milestone=milestone1,
sprint_order=2)
assert project.milestones.get(id=milestone1.id).user_stories.count() == 2
url = reverse("milestones-move-userstories-to-sprint", kwargs={"pk": milestone1.pk})
data = {
"project_id": project.id,
"milestone_id": milestone2.id,
"bulk_stories": [{"us_id": us2.id, "order": 2}]
}
client.login(project.owner)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 204, response.data
assert project.milestones.get(id=milestone1.id).user_stories.count() == 1
assert project.milestones.get(id=milestone2.id).user_stories.count() == 1
def test_api_move_userstories_to_another_sprint_close_previous(client):
project = f.create_project()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
milestone2 = f.MilestoneFactory.create(project=project)
closed_status = f.UserStoryStatusFactory.create(is_closed=True)
us1 = f.create_userstory(project=project, milestone=milestone1,
sprint_order=1, status=closed_status)
us2 = f.create_userstory(project=project, milestone=milestone1, sprint_order=2)
assert milestone1.user_stories.count() == 2
assert not milestone1.closed
url = reverse("milestones-move-userstories-to-sprint", kwargs={"pk": milestone1.pk})
data = {
"project_id": project.id,
"milestone_id": milestone2.id,
"bulk_stories": [{"us_id": us2.id, "order": 2}]
}
client.login(project.owner)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 204, response.data
assert project.milestones.get(id=milestone1.id).user_stories.count() == 1
assert project.milestones.get(id=milestone2.id).user_stories.count() == 1
assert project.milestones.get(id=milestone1.id).closed
def test_api_move_tasks_to_another_sprint(client):
project = f.create_project()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
milestone2 = f.MilestoneFactory.create(project=project)
task1 = f.create_task(project=project, milestone=milestone1, taskboard_order=1)
task2 = f.create_task(project=project, milestone=milestone1, taskboard_order=2)
assert project.milestones.get(id=milestone1.id).tasks.count() == 2
url = reverse("milestones-move-tasks-to-sprint", kwargs={"pk": milestone1.pk})
data = {
"project_id": project.id,
"milestone_id": milestone2.id,
"bulk_tasks": [{"task_id": task2.id, "order": 2}]
}
client.login(project.owner)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 204, response.data
assert project.milestones.get(id=milestone1.id).tasks.count() == 1
assert project.milestones.get(id=milestone2.id).tasks.count() == 1
def test_api_move_tasks_to_another_sprint_close_previous(client):
project = f.create_project()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
milestone2 = f.MilestoneFactory.create(project=project)
closed_status = f.TaskStatusFactory.create(project=project, is_closed=True)
task1 = f.create_task(project=project, milestone=milestone1, taskboard_order=1,
status=closed_status, user_story=None)
task2 = f.create_task(project=project, milestone=milestone1, taskboard_order=2,
user_story=None)
assert project.milestones.get(id=milestone1.id).tasks.count() == 2
assert not milestone1.closed
url = reverse("milestones-move-tasks-to-sprint", kwargs={"pk": milestone1.pk})
data = {
"project_id": project.id,
"milestone_id": milestone2.id,
"bulk_tasks": [{"task_id": task2.id, "order": 2}]
}
client.login(project.owner)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 204, response.data
assert project.milestones.get(id=milestone1.id).tasks.count() == 1
assert project.milestones.get(id=milestone2.id).tasks.count() == 1
assert project.milestones.get(id=milestone1.id).closed
def test_api_move_issues_to_another_sprint(client):
project = f.create_project()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
milestone2 = f.MilestoneFactory.create(project=project)
issue1 = f.create_issue(project=project, milestone=milestone1)
issue2 = f.create_issue(project=project, milestone=milestone1)
assert project.milestones.get(id=milestone1.id).issues.count() == 2
url = reverse("milestones-move-issues-to-sprint", kwargs={"pk": milestone1.pk})
data = {
"project_id": project.id,
"milestone_id": milestone2.id,
"bulk_issues": [{"issue_id": issue2.id, "order": 2}]
}
client.login(project.owner)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 204, response.data
assert project.milestones.get(id=milestone1.id).issues.count() == 1
assert project.milestones.get(id=milestone2.id).issues.count() == 1
def test_api_move_issues_to_another_sprint_close_previous(client):
project = f.create_project()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
milestone2 = f.MilestoneFactory.create(project=project)
closed_status = f.IssueStatusFactory.create(project=project,
is_closed=True)
issue1 = f.create_issue(project=project, milestone=milestone1,
status=closed_status)
issue2 = f.create_issue(project=project, milestone=milestone1)
assert project.milestones.get(id=milestone1.id).closed is False
assert project.milestones.get(id=milestone1.id).issues.count() == 2
url = reverse("milestones-move-issues-to-sprint", kwargs={"pk": milestone1.pk})
data = {
"project_id": project.id,
"milestone_id": milestone2.id,
"bulk_issues": [{"issue_id": issue2.id, "order": 2}]
}
client.login(project.owner)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 204, response.data
assert project.milestones.get(id=milestone1.id).issues.count() == 1
assert project.milestones.get(id=milestone2.id).issues.count() == 1
assert project.milestones.get(id=milestone1.id).closed

View File

@ -55,6 +55,20 @@ def mail():
return mail
@pytest.mark.parametrize(
"header, expected",
[
("", ""),
("One line", "One line"),
("Two \nlines", "Two lines"),
("Mix \r\nCR and LF \rin the string", "Mix CR and LF in the string"),
]
)
def test_remove_lr_cr(header, expected):
rv = services.remove_lr_cr(header)
assert rv == expected
def test_create_retrieve_notify_policy():
project = f.ProjectFactory.create()

View File

@ -39,6 +39,62 @@ import pytest
pytestmark = pytest.mark.django_db
def create_tasks_fixtures():
data = {}
data["project"] = f.ProjectFactory.create()
project = data["project"]
data["users"] = [f.UserFactory.create(is_superuser=True) for i in range(0, 3)]
data["roles"] = [f.RoleFactory.create() for i in range(0, 3)]
user_roles = zip(data["users"], data["roles"])
# Add membership fixtures
[f.MembershipFactory.create(user=user, project=project, role=role) for (user, role) in user_roles]
data["statuses"] = [f.TaskStatusFactory.create(project=project) for i in range(0, 4)]
data["tags"] = ["test1test2test3", "test1", "test2", "test3"]
# ----------------------------------------------------------------
# | Task | Owner | Assigned To | Tags | Status |
# |-------#--------#-------------#---------------------|---------|
# | 0 | user2 | None | tag1 | status3 |
# | 1 | user1 | None | tag2 | status3 |
# | 2 | user3 | None | tag1 tag2 | status1 |
# | 3 | user2 | None | tag3 | status0 |
# | 4 | user1 | user1 | tag1 tag2 tag3 | status0 |
# | 5 | user3 | user1 | tag3 | status2 |
# | 6 | user2 | user1 | tag1 tag2 | status3 |
# | 7 | user1 | user2 | tag3 | status0 |
# | 8 | user3 | user2 | tag1 | status3 |
# | 9 | user2 | user3 | tag0 | status1 |
# ----------------------------------------------------------------
(user1, user2, user3, ) = data["users"]
(status0, status1, status2, status3 ) = data["statuses"]
(tag0, tag1, tag2, tag3, ) = data["tags"]
f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
status=status3, tags=[tag1])
f.TaskFactory.create(project=project, owner=user1, assigned_to=None,
status=status3, tags=[tag2])
f.TaskFactory.create(project=project, owner=user3, assigned_to=None,
status=status1, tags=[tag1, tag2])
f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
status=status0, tags=[tag3])
f.TaskFactory.create(project=project, owner=user1, assigned_to=user1,
status=status0, tags=[tag1, tag2, tag3])
f.TaskFactory.create(project=project, owner=user3, assigned_to=user1,
status=status2, tags=[tag3])
f.TaskFactory.create(project=project, owner=user2, assigned_to=user1,
status=status3, tags=[tag1, tag2])
f.TaskFactory.create(project=project, owner=user1, assigned_to=user2,
status=status0, tags=[tag3])
f.TaskFactory.create(project=project, owner=user3, assigned_to=user2,
status=status3, tags=[tag1])
f.TaskFactory.create(project=project, owner=user2, assigned_to=user3,
status=status1, tags=[tag0])
return data
def test_get_tasks_from_bulk():
data = """
Task #1
@ -796,63 +852,45 @@ def test_api_filter_by_milestone__estimated_start_and_end(client, field_name):
assert response.data[0]["subject"] == task.subject
@pytest.mark.parametrize("filter_name,collection,expected,exclude_expected,is_text", [
('status', 'statuses', 3, 7, False),
('assigned_to', 'users', 3, 7, False),
('tags', 'tags', 1, 9, True),
('owner', 'users', 3, 7, False),
('role', 'roles', 3, 7, False),
])
def test_api_filters(client, filter_name, collection, expected, exclude_expected, is_text):
data = create_tasks_fixtures()
project = data["project"]
options = data[collection]
client.login(data["users"][0])
if is_text:
param = options[0]
else:
param = options[0].id
# include test
url = "{}?project={}&{}={}".format(reverse('tasks-list'), project.id, filter_name, param)
response = client.get(url)
assert response.status_code == 200
assert len(response.data) == expected
# exclude test
url = "{}?project={}&exclude_{}={}".format(reverse('tasks-list'), project.id, filter_name, param)
response = client.get(url)
assert response.status_code == 200
assert len(response.data) == exclude_expected
def test_api_filters_data(client):
project = f.ProjectFactory.create()
user1 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user1, project=project)
user2 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user2, project=project)
user3 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user3, project=project)
status0 = f.TaskStatusFactory.create(project=project)
status1 = f.TaskStatusFactory.create(project=project)
status2 = f.TaskStatusFactory.create(project=project)
status3 = f.TaskStatusFactory.create(project=project)
tag0 = "test1test2test3"
tag1 = "test1"
tag2 = "test2"
tag3 = "test3"
# ------------------------------------------------------
# | Task | Owner | Assigned To | Tags |
# |-------#--------#-------------#---------------------|
# | 0 | user2 | None | tag1 |
# | 1 | user1 | None | tag2 |
# | 2 | user3 | None | tag1 tag2 |
# | 3 | user2 | None | tag3 |
# | 4 | user1 | user1 | tag1 tag2 tag3 |
# | 5 | user3 | user1 | tag3 |
# | 6 | user2 | user1 | tag1 tag2 |
# | 7 | user1 | user2 | tag3 |
# | 8 | user3 | user2 | tag1 |
# | 9 | user2 | user3 | tag0 |
# ------------------------------------------------------
task0 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
status=status3, tags=[tag1])
task1 = f.TaskFactory.create(project=project, owner=user1, assigned_to=None,
status=status3, tags=[tag2])
task2 = f.TaskFactory.create(project=project, owner=user3, assigned_to=None,
status=status1, tags=[tag1, tag2])
task3 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
status=status0, tags=[tag3])
task4 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user1,
status=status0, tags=[tag1, tag2, tag3])
task5 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user1,
status=status2, tags=[tag3])
task6 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user1,
status=status3, tags=[tag1, tag2])
task7 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user2,
status=status0, tags=[tag3])
task8 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user2,
status=status3, tags=[tag1])
task9 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user3,
status=status1, tags=[tag0])
data = create_tasks_fixtures()
project = data["project"]
(user1, user2, user3, ) = data["users"]
(status0, status1, status2, status3, ) = data["statuses"]
(tag0, tag1, tag2, tag3, ) = data["tags"]
url = reverse("tasks-filters-data") + "?project={}".format(project.id)
client.login(user1)
## No filter

View File

@ -38,6 +38,71 @@ import pytest
pytestmark = pytest.mark.django_db(transaction=True)
def create_uss_fixtures():
data = {}
data["project"] = f.ProjectFactory.create()
project = data["project"]
data["users"] = [f.UserFactory.create(is_superuser=True) for i in range(0, 3)]
data["roles"] = [f.RoleFactory.create() for i in range(0, 3)]
user_roles = zip(data["users"], data["roles"])
# Add membership fixtures
[f.MembershipFactory.create(user=user, project=project, role=role) for (user, role) in user_roles]
data["statuses"] = [f.UserStoryStatusFactory.create(project=project) for i in range(0, 4)]
data["epics"] = [f.EpicFactory.create(project=project) for i in range(0, 3)]
data["tags"] = ["test1test2test3", "test1", "test2", "test3"]
# ----------------------------------------------------------------------------------------------------
# | US | Status | Owner | Assigned To | Assigned Users | Tags | Epic |
# |-------#---------#--------#-------------#---------------------#---------------------#--------------
# | 0 | status3 | user2 | None | None | tag1 | epic0 |
# | 1 | status3 | user1 | None | user1 | tag2 | None |
# | 2 | status1 | user3 | None | None | tag1 tag2 | epic1 |
# | 3 | status0 | user2 | None | None | tag3 | None |
# | 4 | status0 | user1 | user1 | None | tag1 tag2 tag3 | epic0 |
# | 5 | status2 | user3 | user1 | None | tag3 | None |
# | 6 | status3 | user2 | user1 | None | tag1 tag2 | epic0 epic2 |
# | 7 | status0 | user1 | user2 | None | tag3 | None |
# | 8 | status3 | user3 | user2 | None | tag1 | epic2 |
# | 9 | status1 | user2 | user3 | user1 | tag0 | None |
# ----------------------------------------------------------------------------------------------------
(user1, user2, user3, ) = data["users"]
(status0, status1, status2, status3 ) = data["statuses"]
(epic0, epic1, epic2) = data["epics"]
(tag0, tag1, tag2, tag3, ) = data["tags"]
us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
status=status3, tags=[tag1])
f.RelatedUserStory.create(user_story=us0, epic=epic0)
us1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None,
status=status3, tags=[tag2], assigned_users=[user1])
us2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None,
status=status1, tags=[tag1, tag2])
f.RelatedUserStory.create(user_story=us2, epic=epic1)
us3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
status=status0, tags=[tag3])
us4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1,
status=status0, tags=[tag1, tag2, tag3])
f.RelatedUserStory.create(user_story=us4, epic=epic0)
us5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1,
status=status2, tags=[tag3])
us6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1,
status=status3, tags=[tag1, tag2])
f.RelatedUserStory.create(user_story=us6, epic=epic0)
f.RelatedUserStory.create(user_story=us6, epic=epic2)
us7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2,
status=status0, tags=[tag3])
us8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2,
status=status3, tags=[tag1])
f.RelatedUserStory.create(user_story=us8, epic=epic2)
us9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3,
status=status1, tags=[tag0], assigned_users=[user1])
return data
def test_get_userstories_from_bulk():
data = "User Story #1\nUser Story #2\n"
userstories = services.get_userstories_from_bulk(data)
@ -777,72 +842,14 @@ def test_api_filter_by_milestone__estimated_start_and_end(client, field_name):
def test_api_filters_data(client):
project = f.ProjectFactory.create()
user1 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user1, project=project)
user2 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user2, project=project)
user3 = f.UserFactory.create(is_superuser=True)
f.MembershipFactory.create(user=user3, project=project)
status0 = f.UserStoryStatusFactory.create(project=project)
status1 = f.UserStoryStatusFactory.create(project=project)
status2 = f.UserStoryStatusFactory.create(project=project)
status3 = f.UserStoryStatusFactory.create(project=project)
epic0 = f.EpicFactory.create(project=project)
epic1 = f.EpicFactory.create(project=project)
epic2 = f.EpicFactory.create(project=project)
tag0 = "test1test2test3"
tag1 = "test1"
tag2 = "test2"
tag3 = "test3"
# ----------------------------------------------------------------------------------------------------
# | US | Status | Owner | Assigned To | Assigned Users | Tags | Epic |
# |-------#---------#--------#-------------#---------------------#---------------------#--------------
# | 0 | status3 | user2 | None | None | tag1 | epic0 |
# | 1 | status3 | user1 | None | user1 | tag2 | None |
# | 2 | status1 | user3 | None | None | tag1 tag2 | epic1 |
# | 3 | status0 | user2 | None | None | tag3 | None |
# | 4 | status0 | user1 | user1 | None | tag1 tag2 tag3 | epic0 |
# | 5 | status2 | user3 | user1 | None | tag3 | None |
# | 6 | status3 | user2 | user1 | None | tag1 tag2 | epic0 epic2 |
# | 7 | status0 | user1 | user2 | None | tag3 | None |
# | 8 | status3 | user3 | user2 | None | tag1 | epic2 |
# | 9 | status1 | user2 | user3 | user1 | tag0 | None |
# ----------------------------------------------------------------------------------------------------
us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
status=status3, tags=[tag1])
f.RelatedUserStory.create(user_story=us0, epic=epic0)
us1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None,
status=status3, tags=[tag2], assigned_users=[user1])
us2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None,
status=status1, tags=[tag1, tag2])
f.RelatedUserStory.create(user_story=us2, epic=epic1)
us3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
status=status0, tags=[tag3])
us4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1,
status=status0, tags=[tag1, tag2, tag3])
f.RelatedUserStory.create(user_story=us4, epic=epic0)
us5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1,
status=status2, tags=[tag3])
us6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1,
status=status3, tags=[tag1, tag2])
f.RelatedUserStory.create(user_story=us6, epic=epic0)
f.RelatedUserStory.create(user_story=us6, epic=epic2)
us7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2,
status=status0, tags=[tag3])
us8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2,
status=status3, tags=[tag1])
f.RelatedUserStory.create(user_story=us8, epic=epic2)
us9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3,
status=status1, tags=[tag0], assigned_users=[user1])
data = create_uss_fixtures()
project = data["project"]
(user1, user2, user3, ) = data["users"]
(status0, status1, status2, status3, ) = data["statuses"]
(epic0, epic1, epic2, ) = data["epics"]
(tag0, tag1, tag2, tag3, ) = data["tags"]
url = reverse("userstories-filters-data") + "?project={}".format(project.id)
client.login(user1)
# No filter
@ -961,6 +968,37 @@ def test_api_filters_data(client):
assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2
@pytest.mark.parametrize("filter_name,collection,expected,exclude_expected,is_text", [
('status', 'statuses', 3, 7, False),
('tags', 'tags', 1, 9, True),
('owner', 'users', 3, 7, False),
('role', 'roles', 5, 5, False),
('assigned_users', 'users', 5, 5, False),
])
def test_api_filters(client, filter_name, collection, expected, exclude_expected, is_text):
data = create_uss_fixtures()
project = data["project"]
options = data[collection]
client.login(data["users"][0])
if is_text:
param = options[0]
else:
param = options[0].id
# include test
url = "{}?project={}&{}={}".format(reverse('userstories-list'), project.id, filter_name, param)
response = client.get(url)
assert response.status_code == 200
assert len(response.data) == expected
# exclude test
url = "{}?project={}&exclude_{}={}".format(reverse('userstories-list'), project.id, filter_name, param)
response = client.get(url)
assert response.status_code == 200
assert len(response.data) == exclude_expected
def test_api_filters_data_with_assigned_users(client):
project = f.ProjectFactory.create()
user1 = f.UserFactory.create(is_superuser=True)

View File

@ -0,0 +1,268 @@
import pytest
from .. import factories as f
from taiga.projects.milestones import services
pytestmark = pytest.mark.django_db
def test_issues_not_closed():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.IssueStatusFactory.create(project=project,
is_closed=True)
f.create_issue(project=project, milestone=milestone1,
status=closed_status)
f.create_issue(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_issues_closed():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.IssueStatusFactory.create(project=project,
is_closed=True)
f.create_issue(project=project, milestone=milestone1,
status=closed_status)
assert services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_issues_but_closed_tasks():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
tasks_closed_status = f.TaskStatusFactory.create(project=project,
is_closed=True)
f.create_task(project=project, milestone=milestone1,
taskboard_order=1, status=tasks_closed_status)
f.create_issue(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_issues_but_closed_uss():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
us_closed_status = f.UserStoryStatusFactory.create(project=project,
is_closed=True)
f.create_userstory(project=project, milestone=milestone1,
status=us_closed_status, is_closed=True)
f.create_issue(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_closed_issues_but_open_uss():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.IssueStatusFactory.create(project=project,
is_closed=True)
f.create_issue(project=project, milestone=milestone1,
status=closed_status)
f.create_userstory(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_closed_issues_but_open_tasks():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.IssueStatusFactory.create(project=project,
is_closed=True)
f.create_issue(project=project, milestone=milestone1,
status=closed_status)
f.create_task(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_tasks_not_closed():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.TaskStatusFactory.create(project=project,
is_closed=True)
f.create_task(project=project, milestone=milestone1,
status=closed_status)
f.create_task(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_tasks_closed():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.TaskStatusFactory.create(project=project,
is_closed=True)
f.create_task(project=project, milestone=milestone1,
status=closed_status, user_story=None)
assert services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_tasks_but_closed_issues():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
issue_closed_status = f.IssueStatusFactory.create(project=project,
is_closed=True)
f.create_issue(project=project, milestone=milestone1,
status=issue_closed_status)
f.create_task(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_tasks_but_closed_uss():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
us_closed_status = f.UserStoryStatusFactory.create(project=project,
is_closed=True)
f.create_userstory(project=project, milestone=milestone1,
status=us_closed_status, is_closed=True)
f.create_task(project=project, milestone=milestone1, user_story=None)
assert not services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_closed_tasks_but_open_uss():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.TaskStatusFactory.create(project=project,
is_closed=True)
f.create_task(project=project, milestone=milestone1,
status=closed_status, user_story=None)
f.create_userstory(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_closed_tasks_but_open_issues():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.TaskStatusFactory.create(project=project,
is_closed=True)
f.create_task(project=project, milestone=milestone1,
status=closed_status, user_story=None)
f.create_issue(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_uss_not_closed():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.UserStoryStatusFactory.create(project=project,
is_closed=True)
f.create_userstory(project=project, milestone=milestone1,
status=closed_status, is_closed=True)
f.create_userstory(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_uss_closed():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.UserStoryStatusFactory.create(project=project,
is_closed=True)
f.create_userstory(project=project, milestone=milestone1,
sprint_order=1, status=closed_status,
is_closed=True)
assert services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_uss_but_closed_tasks_and_us():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
us_closed_status = f.UserStoryStatusFactory.create(project=project,
is_closed=True)
us = f.create_userstory(project=project, milestone=milestone1,
status=us_closed_status, is_closed=True)
task_closed_status = f.TaskStatusFactory.create(project=project,
is_closed=True)
f.create_task(project=project, milestone=milestone1, user_story=us,
status=task_closed_status)
f.create_userstory(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_uss_but_closed_tasks():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.TaskStatusFactory.create(project=project,
is_closed=True)
f.create_task(project=project, milestone=milestone1,
status=closed_status, user_story=None)
f.create_userstory(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)
def test_stay_open_with_uss_but_closed_issues():
project = f.ProjectFactory()
f.MembershipFactory.create(project=project, user=project.owner,
is_admin=True)
milestone1 = f.MilestoneFactory.create(project=project)
closed_status = f.IssueStatusFactory.create(project=project,
is_closed=True)
f.create_issue(project=project, milestone=milestone1,
status=closed_status)
f.create_userstory(project=project, milestone=milestone1)
assert not services.calculate_milestone_is_closed(milestone1)