services.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. # Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
  2. # Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
  3. # Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as
  6. # published by the Free Software Foundation, either version 3 of the
  7. # License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import io
  17. import csv
  18. from collections import OrderedDict
  19. from operator import itemgetter
  20. from contextlib import closing
  21. from django.db import connection
  22. from django.utils.translation import ugettext as _
  23. from taiga.base.utils import db, text
  24. from taiga.projects.issues.apps import (
  25. connect_issues_signals,
  26. disconnect_issues_signals)
  27. from taiga.projects.votes import services as votes_services
  28. from . import models
  29. def get_issues_from_bulk(bulk_data, **additional_fields):
  30. """Convert `bulk_data` into a list of issues.
  31. :param bulk_data: List of issues in bulk format.
  32. :param additional_fields: Additional fields when instantiating each issue.
  33. :return: List of `Issue` instances.
  34. """
  35. return [models.Issue(subject=line, **additional_fields)
  36. for line in text.split_in_lines(bulk_data)]
  37. def create_issues_in_bulk(bulk_data, callback=None, precall=None, **additional_fields):
  38. """Create issues from `bulk_data`.
  39. :param bulk_data: List of issues in bulk format.
  40. :param callback: Callback to execute after each issue save.
  41. :param additional_fields: Additional fields when instantiating each issue.
  42. :return: List of created `Issue` instances.
  43. """
  44. issues = get_issues_from_bulk(bulk_data, **additional_fields)
  45. disconnect_issues_signals()
  46. try:
  47. db.save_in_bulk(issues, callback, precall)
  48. finally:
  49. connect_issues_signals()
  50. return issues
  51. def update_issues_order_in_bulk(bulk_data):
  52. """Update the order of some issues.
  53. `bulk_data` should be a list of tuples with the following format:
  54. [(<issue id>, <new issue order value>), ...]
  55. """
  56. issue_ids = []
  57. new_order_values = []
  58. for issue_id, new_order_value in bulk_data:
  59. issue_ids.append(issue_id)
  60. new_order_values.append({"order": new_order_value})
  61. db.update_in_bulk_with_ids(issue_ids, new_order_values, model=models.Issue)
  62. def issues_to_csv(project, queryset):
  63. csv_data = io.StringIO()
  64. fieldnames = ["ref", "subject", "description", "milestone", "owner",
  65. "owner_full_name", "assigned_to", "assigned_to_full_name",
  66. "status", "severity", "priority", "type", "is_closed",
  67. "attachments", "external_reference", "tags",
  68. "watchers", "voters"]
  69. for custom_attr in project.issuecustomattributes.all():
  70. fieldnames.append(custom_attr.name)
  71. writer = csv.DictWriter(csv_data, fieldnames=fieldnames)
  72. writer.writeheader()
  73. for issue in queryset:
  74. issue_data = {
  75. "ref": issue.ref,
  76. "subject": issue.subject,
  77. "description": issue.description,
  78. "milestone": issue.milestone.name if issue.milestone else None,
  79. "owner": issue.owner.username,
  80. "owner_full_name": issue.owner.get_full_name(),
  81. "assigned_to": issue.assigned_to.username if issue.assigned_to else None,
  82. "assigned_to_full_name": issue.assigned_to.get_full_name() if issue.assigned_to else None,
  83. "status": issue.status.name,
  84. "severity": issue.severity.name,
  85. "priority": issue.priority.name,
  86. "type": issue.type.name,
  87. "is_closed": issue.is_closed,
  88. "attachments": issue.attachments.count(),
  89. "external_reference": issue.external_reference,
  90. "tags": ",".join(issue.tags or []),
  91. "watchers": [u.id for u in issue.get_watchers()],
  92. "voters": votes_services.get_voters(issue).count(),
  93. }
  94. for custom_attr in project.issuecustomattributes.all():
  95. value = issue.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
  96. issue_data[custom_attr.name] = value
  97. writer.writerow(issue_data)
  98. return csv_data
  99. def _get_issues_statuses(project, queryset):
  100. compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
  101. queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
  102. where = queryset_where_tuple[0]
  103. where_params = queryset_where_tuple[1]
  104. extra_sql = """
  105. SELECT "projects_issuestatus"."id",
  106. "projects_issuestatus"."name",
  107. "projects_issuestatus"."color",
  108. "projects_issuestatus"."order",
  109. (SELECT count(*)
  110. FROM "issues_issue"
  111. INNER JOIN "projects_project" ON
  112. ("issues_issue"."project_id" = "projects_project"."id")
  113. WHERE {where} AND "issues_issue"."status_id" = "projects_issuestatus"."id")
  114. FROM "projects_issuestatus"
  115. WHERE "projects_issuestatus"."project_id" = %s
  116. ORDER BY "projects_issuestatus"."order";
  117. """.format(where=where)
  118. with closing(connection.cursor()) as cursor:
  119. cursor.execute(extra_sql, where_params + [project.id])
  120. rows = cursor.fetchall()
  121. result = []
  122. for id, name, color, order, count in rows:
  123. result.append({
  124. "id": id,
  125. "name": _(name),
  126. "color": color,
  127. "order": order,
  128. "count": count,
  129. })
  130. return sorted(result, key=itemgetter("order"))
  131. def _get_issues_types(project, queryset):
  132. compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
  133. queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
  134. where = queryset_where_tuple[0]
  135. where_params = queryset_where_tuple[1]
  136. extra_sql = """
  137. SELECT "projects_issuetype"."id",
  138. "projects_issuetype"."name",
  139. "projects_issuetype"."color",
  140. "projects_issuetype"."order",
  141. (SELECT count(*)
  142. FROM "issues_issue"
  143. INNER JOIN "projects_project" ON
  144. ("issues_issue"."project_id" = "projects_project"."id")
  145. WHERE {where} AND "issues_issue"."type_id" = "projects_issuetype"."id")
  146. FROM "projects_issuetype"
  147. WHERE "projects_issuetype"."project_id" = %s
  148. ORDER BY "projects_issuetype"."order";
  149. """.format(where=where)
  150. with closing(connection.cursor()) as cursor:
  151. cursor.execute(extra_sql, where_params + [project.id])
  152. rows = cursor.fetchall()
  153. result = []
  154. for id, name, color, order, count in rows:
  155. result.append({
  156. "id": id,
  157. "name": _(name),
  158. "color": color,
  159. "order": order,
  160. "count": count,
  161. })
  162. return sorted(result, key=itemgetter("order"))
  163. def _get_issues_priorities(project, queryset):
  164. compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
  165. queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
  166. where = queryset_where_tuple[0]
  167. where_params = queryset_where_tuple[1]
  168. extra_sql = """
  169. SELECT "projects_priority"."id",
  170. "projects_priority"."name",
  171. "projects_priority"."color",
  172. "projects_priority"."order",
  173. (SELECT count(*)
  174. FROM "issues_issue"
  175. INNER JOIN "projects_project" ON
  176. ("issues_issue"."project_id" = "projects_project"."id")
  177. WHERE {where} AND "issues_issue"."priority_id" = "projects_priority"."id")
  178. FROM "projects_priority"
  179. WHERE "projects_priority"."project_id" = %s
  180. ORDER BY "projects_priority"."order";
  181. """.format(where=where)
  182. with closing(connection.cursor()) as cursor:
  183. cursor.execute(extra_sql, where_params + [project.id])
  184. rows = cursor.fetchall()
  185. result = []
  186. for id, name, color, order, count in rows:
  187. result.append({
  188. "id": id,
  189. "name": _(name),
  190. "color": color,
  191. "order": order,
  192. "count": count,
  193. })
  194. return sorted(result, key=itemgetter("order"))
  195. def _get_issues_severities(project, queryset):
  196. compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
  197. queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
  198. where = queryset_where_tuple[0]
  199. where_params = queryset_where_tuple[1]
  200. extra_sql = """
  201. SELECT "projects_severity"."id",
  202. "projects_severity"."name",
  203. "projects_severity"."color",
  204. "projects_severity"."order",
  205. (SELECT count(*)
  206. FROM "issues_issue"
  207. INNER JOIN "projects_project" ON
  208. ("issues_issue"."project_id" = "projects_project"."id")
  209. WHERE {where} AND "issues_issue"."severity_id" = "projects_severity"."id")
  210. FROM "projects_severity"
  211. WHERE "projects_severity"."project_id" = %s
  212. ORDER BY "projects_severity"."order";
  213. """.format(where=where)
  214. with closing(connection.cursor()) as cursor:
  215. cursor.execute(extra_sql, where_params + [project.id])
  216. rows = cursor.fetchall()
  217. result = []
  218. for id, name, color, order, count in rows:
  219. result.append({
  220. "id": id,
  221. "name": _(name),
  222. "color": color,
  223. "order": order,
  224. "count": count,
  225. })
  226. return sorted(result, key=itemgetter("order"))
  227. def _get_issues_assigned_to(project, queryset):
  228. compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
  229. queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
  230. where = queryset_where_tuple[0]
  231. where_params = queryset_where_tuple[1]
  232. extra_sql = """
  233. SELECT NULL,
  234. NULL,
  235. (SELECT count(*)
  236. FROM "issues_issue"
  237. INNER JOIN "projects_project" ON
  238. ("issues_issue"."project_id" = "projects_project"."id" )
  239. WHERE {where} AND "issues_issue"."assigned_to_id" IS NULL)
  240. UNION SELECT "users_user"."id",
  241. "users_user"."full_name",
  242. (SELECT count(*)
  243. FROM "issues_issue"
  244. INNER JOIN "projects_project" ON
  245. ("issues_issue"."project_id" = "projects_project"."id" )
  246. WHERE {where} AND "issues_issue"."assigned_to_id" = "projects_membership"."user_id")
  247. FROM "projects_membership"
  248. INNER JOIN "users_user" ON
  249. ("projects_membership"."user_id" = "users_user"."id")
  250. WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL;
  251. """.format(where=where)
  252. with closing(connection.cursor()) as cursor:
  253. cursor.execute(extra_sql, where_params + where_params + [project.id])
  254. rows = cursor.fetchall()
  255. result = []
  256. for id, full_name, count in rows:
  257. result.append({
  258. "id": id,
  259. "full_name": full_name or "",
  260. "count": count,
  261. })
  262. return sorted(result, key=itemgetter("full_name"))
  263. def _get_issues_owners(project, queryset):
  264. compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
  265. queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
  266. where = queryset_where_tuple[0]
  267. where_params = queryset_where_tuple[1]
  268. extra_sql = """
  269. SELECT "users_user"."id",
  270. "users_user"."full_name",
  271. (SELECT count(*)
  272. FROM "issues_issue"
  273. INNER JOIN "projects_project" ON
  274. ("issues_issue"."project_id" = "projects_project"."id")
  275. WHERE {where} and "issues_issue"."owner_id" = "projects_membership"."user_id")
  276. FROM "projects_membership"
  277. RIGHT OUTER JOIN "users_user" ON
  278. ("projects_membership"."user_id" = "users_user"."id")
  279. WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL)
  280. OR ("users_user"."is_system" IS TRUE);
  281. """.format(where=where)
  282. with closing(connection.cursor()) as cursor:
  283. cursor.execute(extra_sql, where_params + [project.id])
  284. rows = cursor.fetchall()
  285. result = []
  286. for id, full_name, count in rows:
  287. if count > 0:
  288. result.append({
  289. "id": id,
  290. "full_name": full_name,
  291. "count": count,
  292. })
  293. return sorted(result, key=itemgetter("full_name"))
  294. def _get_issues_tags(queryset):
  295. tags = []
  296. for t_list in queryset.values_list("tags", flat=True):
  297. if t_list is None:
  298. continue
  299. tags += list(t_list)
  300. tags = [{"name":e, "count":tags.count(e)} for e in set(tags)]
  301. return sorted(tags, key=itemgetter("name"))
  302. def get_issues_filters_data(project, querysets):
  303. """
  304. Given a project and an issues queryset, return a simple data structure
  305. of all possible filters for the issues in the queryset.
  306. """
  307. data = OrderedDict([
  308. ("types", _get_issues_types(project, querysets["types"])),
  309. ("statuses", _get_issues_statuses(project, querysets["statuses"])),
  310. ("priorities", _get_issues_priorities(project, querysets["priorities"])),
  311. ("severities", _get_issues_severities(project, querysets["severities"])),
  312. ("assigned_to", _get_issues_assigned_to(project, querysets["assigned_to"])),
  313. ("owners", _get_issues_owners(project, querysets["owners"])),
  314. ("tags", _get_issues_tags(querysets["tags"])),
  315. ])
  316. return data