api.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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 uuid
  17. from django.db.models import signals
  18. from django.core.exceptions import ValidationError
  19. from django.utils.translation import ugettext as _
  20. from taiga.base import filters
  21. from taiga.base import response
  22. from taiga.base import exceptions as exc
  23. from taiga.base.decorators import list_route
  24. from taiga.base.decorators import detail_route
  25. from taiga.base.api import ModelCrudViewSet, ModelListViewSet
  26. from taiga.base.api.permissions import AllowAnyPermission
  27. from taiga.base.api.utils import get_object_or_404
  28. from taiga.base.utils.slug import slugify_uniquely
  29. from taiga.projects.history.mixins import HistoryResourceMixin
  30. from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
  31. from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
  32. from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
  33. from taiga.projects.userstories.models import UserStory, RolePoints
  34. from taiga.projects.tasks.models import Task
  35. from taiga.projects.issues.models import Issue
  36. from taiga.permissions import service as permissions_service
  37. from . import serializers
  38. from . import models
  39. from . import permissions
  40. from . import services
  41. from .votes.mixins.viewsets import StarredResourceMixin, VotersViewSetMixin
  42. ######################################################
  43. ## Project
  44. ######################################################
  45. class ProjectViewSet(StarredResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
  46. queryset = models.Project.objects.all()
  47. serializer_class = serializers.ProjectDetailSerializer
  48. admin_serializer_class = serializers.ProjectDetailAdminSerializer
  49. list_serializer_class = serializers.ProjectSerializer
  50. permission_classes = (permissions.ProjectPermission, )
  51. filter_backends = (filters.CanViewProjectObjFilterBackend,)
  52. filter_fields = (('member', 'members'),)
  53. order_by_fields = ("memberships__user_order",)
  54. def get_queryset(self):
  55. qs = super().get_queryset()
  56. qs = self.attach_votes_attrs_to_queryset(qs)
  57. return self.attach_watchers_attrs_to_queryset(qs)
  58. @list_route(methods=["POST"])
  59. def bulk_update_order(self, request, **kwargs):
  60. if self.request.user.is_anonymous():
  61. return response.Unauthorized()
  62. serializer = serializers.UpdateProjectOrderBulkSerializer(data=request.DATA, many=True)
  63. if not serializer.is_valid():
  64. return response.BadRequest(serializer.errors)
  65. data = serializer.data
  66. services.update_projects_order_in_bulk(data, "user_order", request.user)
  67. return response.NoContent(data=None)
  68. def get_serializer_class(self):
  69. if self.action == "list":
  70. return self.list_serializer_class
  71. elif self.action == "create":
  72. return self.serializer_class
  73. if self.action == "by_slug":
  74. slug = self.request.QUERY_PARAMS.get("slug", None)
  75. project = get_object_or_404(models.Project, slug=slug)
  76. else:
  77. project = self.get_object()
  78. if permissions_service.is_project_owner(self.request.user, project):
  79. return self.admin_serializer_class
  80. return self.serializer_class
  81. @list_route(methods=["GET"])
  82. def by_slug(self, request):
  83. slug = request.QUERY_PARAMS.get("slug", None)
  84. project = get_object_or_404(models.Project, slug=slug)
  85. return self.retrieve(request, pk=project.pk)
  86. @detail_route(methods=["GET", "PATCH"])
  87. def modules(self, request, pk=None):
  88. project = self.get_object()
  89. self.check_permissions(request, 'modules', project)
  90. modules_config = services.get_modules_config(project)
  91. if request.method == "GET":
  92. return response.Ok(modules_config.config)
  93. else:
  94. modules_config.config.update(request.DATA)
  95. modules_config.save()
  96. return response.NoContent()
  97. @detail_route(methods=["GET"])
  98. def stats(self, request, pk=None):
  99. project = self.get_object()
  100. self.check_permissions(request, "stats", project)
  101. return response.Ok(services.get_stats_for_project(project))
  102. def _regenerate_csv_uuid(self, project, field):
  103. uuid_value = uuid.uuid4().hex
  104. setattr(project, field, uuid_value)
  105. project.save()
  106. return uuid_value
  107. @detail_route(methods=["POST"])
  108. def regenerate_userstories_csv_uuid(self, request, pk=None):
  109. project = self.get_object()
  110. self.check_permissions(request, "regenerate_userstories_csv_uuid", project)
  111. data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")}
  112. return response.Ok(data)
  113. @detail_route(methods=["POST"])
  114. def regenerate_issues_csv_uuid(self, request, pk=None):
  115. project = self.get_object()
  116. self.check_permissions(request, "regenerate_issues_csv_uuid", project)
  117. data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")}
  118. return response.Ok(data)
  119. @detail_route(methods=["POST"])
  120. def regenerate_tasks_csv_uuid(self, request, pk=None):
  121. project = self.get_object()
  122. self.check_permissions(request, "regenerate_tasks_csv_uuid", project)
  123. data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")}
  124. return response.Ok(data)
  125. @detail_route(methods=["GET"])
  126. def member_stats(self, request, pk=None):
  127. project = self.get_object()
  128. self.check_permissions(request, "member_stats", project)
  129. return response.Ok(services.get_member_stats_for_project(project))
  130. @detail_route(methods=["GET"])
  131. def issues_stats(self, request, pk=None):
  132. project = self.get_object()
  133. self.check_permissions(request, "issues_stats", project)
  134. return response.Ok(services.get_stats_for_project_issues(project))
  135. @detail_route(methods=["GET"])
  136. def tags_colors(self, request, pk=None):
  137. project = self.get_object()
  138. self.check_permissions(request, "tags_colors", project)
  139. return response.Ok(dict(project.tags_colors))
  140. @detail_route(methods=["POST"])
  141. def create_template(self, request, **kwargs):
  142. template_name = request.DATA.get('template_name', None)
  143. template_description = request.DATA.get('template_description', None)
  144. if not template_name:
  145. raise response.BadRequest(_("Not valid template name"))
  146. if not template_description:
  147. raise response.BadRequest(_("Not valid template description"))
  148. template_slug = slugify_uniquely(template_name, models.ProjectTemplate)
  149. project = self.get_object()
  150. self.check_permissions(request, 'create_template', project)
  151. template = models.ProjectTemplate(
  152. name=template_name,
  153. slug=template_slug,
  154. description=template_description,
  155. )
  156. template.load_data_from_project(project)
  157. template.save()
  158. return response.Created(serializers.ProjectTemplateSerializer(template).data)
  159. @detail_route(methods=['post'])
  160. def leave(self, request, pk=None):
  161. project = self.get_object()
  162. self.check_permissions(request, 'leave', project)
  163. services.remove_user_from_project(request.user, project)
  164. return response.Ok()
  165. def _set_base_permissions(self, obj):
  166. update_permissions = False
  167. if not obj.id:
  168. if not obj.is_private:
  169. # Creating a public project
  170. update_permissions = True
  171. else:
  172. if self.get_object().is_private != obj.is_private:
  173. # Changing project public state
  174. update_permissions = True
  175. if update_permissions:
  176. permissions_service.set_base_permissions_for_project(obj)
  177. def pre_save(self, obj):
  178. if not obj.id:
  179. obj.owner = self.request.user
  180. # TODO REFACTOR THIS
  181. if not obj.id:
  182. obj.template = self.request.QUERY_PARAMS.get('template', None)
  183. self._set_base_permissions(obj)
  184. super().pre_save(obj)
  185. def destroy(self, request, *args, **kwargs):
  186. from taiga.events.apps import connect_events_signals, disconnect_events_signals
  187. from taiga.projects.tasks.apps import connect_all_tasks_signals, disconnect_all_tasks_signals
  188. from taiga.projects.userstories.apps import connect_all_userstories_signals, disconnect_all_userstories_signals
  189. from taiga.projects.issues.apps import connect_all_issues_signals, disconnect_all_issues_signals
  190. from taiga.projects.apps import connect_memberships_signals, disconnect_memberships_signals
  191. obj = self.get_object_or_none()
  192. self.check_permissions(request, 'destroy', obj)
  193. if obj is None:
  194. raise Http404
  195. disconnect_events_signals()
  196. disconnect_all_issues_signals()
  197. disconnect_all_tasks_signals()
  198. disconnect_all_userstories_signals()
  199. disconnect_memberships_signals()
  200. try:
  201. obj.tasks.all().delete()
  202. obj.user_stories.all().delete()
  203. obj.issues.all().delete()
  204. obj.memberships.all().delete()
  205. obj.roles.all().delete()
  206. finally:
  207. connect_events_signals()
  208. connect_all_issues_signals()
  209. connect_all_tasks_signals()
  210. connect_all_userstories_signals()
  211. connect_memberships_signals()
  212. self.pre_delete(obj)
  213. self.pre_conditions_on_delete(obj)
  214. obj.delete()
  215. self.post_delete(obj)
  216. return response.NoContent()
  217. class ProjectFansViewSet(VotersViewSetMixin, ModelListViewSet):
  218. permission_classes = (permissions.ProjectFansPermission,)
  219. resource_model = models.Project
  220. class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
  221. permission_classes = (permissions.ProjectWatchersPermission,)
  222. resource_model = models.Project
  223. ######################################################
  224. ## Custom values for selectors
  225. ######################################################
  226. class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
  227. model = models.Points
  228. serializer_class = serializers.PointsSerializer
  229. permission_classes = (permissions.PointsPermission,)
  230. filter_backends = (filters.CanViewProjectFilterBackend,)
  231. filter_fields = ('project',)
  232. bulk_update_param = "bulk_points"
  233. bulk_update_perm = "change_points"
  234. bulk_update_order_action = services.bulk_update_points_order
  235. move_on_destroy_related_class = RolePoints
  236. move_on_destroy_related_field = "points"
  237. move_on_destroy_project_default_field = "default_points"
  238. class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
  239. model = models.UserStoryStatus
  240. serializer_class = serializers.UserStoryStatusSerializer
  241. permission_classes = (permissions.UserStoryStatusPermission,)
  242. filter_backends = (filters.CanViewProjectFilterBackend,)
  243. filter_fields = ('project',)
  244. bulk_update_param = "bulk_userstory_statuses"
  245. bulk_update_perm = "change_userstorystatus"
  246. bulk_update_order_action = services.bulk_update_userstory_status_order
  247. move_on_destroy_related_class = UserStory
  248. move_on_destroy_related_field = "status"
  249. move_on_destroy_project_default_field = "default_us_status"
  250. class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
  251. model = models.TaskStatus
  252. serializer_class = serializers.TaskStatusSerializer
  253. permission_classes = (permissions.TaskStatusPermission,)
  254. filter_backends = (filters.CanViewProjectFilterBackend,)
  255. filter_fields = ("project",)
  256. bulk_update_param = "bulk_task_statuses"
  257. bulk_update_perm = "change_taskstatus"
  258. bulk_update_order_action = services.bulk_update_task_status_order
  259. move_on_destroy_related_class = Task
  260. move_on_destroy_related_field = "status"
  261. move_on_destroy_project_default_field = "default_task_status"
  262. class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
  263. model = models.Severity
  264. serializer_class = serializers.SeveritySerializer
  265. permission_classes = (permissions.SeverityPermission,)
  266. filter_backends = (filters.CanViewProjectFilterBackend,)
  267. filter_fields = ("project",)
  268. bulk_update_param = "bulk_severities"
  269. bulk_update_perm = "change_severity"
  270. bulk_update_order_action = services.bulk_update_severity_order
  271. move_on_destroy_related_class = Issue
  272. move_on_destroy_related_field = "severity"
  273. move_on_destroy_project_default_field = "default_severity"
  274. class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
  275. model = models.Priority
  276. serializer_class = serializers.PrioritySerializer
  277. permission_classes = (permissions.PriorityPermission,)
  278. filter_backends = (filters.CanViewProjectFilterBackend,)
  279. filter_fields = ("project",)
  280. bulk_update_param = "bulk_priorities"
  281. bulk_update_perm = "change_priority"
  282. bulk_update_order_action = services.bulk_update_priority_order
  283. move_on_destroy_related_class = Issue
  284. move_on_destroy_related_field = "priority"
  285. move_on_destroy_project_default_field = "default_priority"
  286. class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
  287. model = models.IssueType
  288. serializer_class = serializers.IssueTypeSerializer
  289. permission_classes = (permissions.IssueTypePermission,)
  290. filter_backends = (filters.CanViewProjectFilterBackend,)
  291. filter_fields = ("project",)
  292. bulk_update_param = "bulk_issue_types"
  293. bulk_update_perm = "change_issuetype"
  294. bulk_update_order_action = services.bulk_update_issue_type_order
  295. move_on_destroy_related_class = Issue
  296. move_on_destroy_related_field = "type"
  297. move_on_destroy_project_default_field = "default_issue_type"
  298. class IssueStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
  299. model = models.IssueStatus
  300. serializer_class = serializers.IssueStatusSerializer
  301. permission_classes = (permissions.IssueStatusPermission,)
  302. filter_backends = (filters.CanViewProjectFilterBackend,)
  303. filter_fields = ("project",)
  304. bulk_update_param = "bulk_issue_statuses"
  305. bulk_update_perm = "change_issuestatus"
  306. bulk_update_order_action = services.bulk_update_issue_status_order
  307. move_on_destroy_related_class = Issue
  308. move_on_destroy_related_field = "status"
  309. move_on_destroy_project_default_field = "default_issue_status"
  310. ######################################################
  311. ## Project Template
  312. ######################################################
  313. class ProjectTemplateViewSet(ModelCrudViewSet):
  314. model = models.ProjectTemplate
  315. serializer_class = serializers.ProjectTemplateSerializer
  316. permission_classes = (permissions.ProjectTemplatePermission,)
  317. def get_queryset(self):
  318. return models.ProjectTemplate.objects.all()
  319. ######################################################
  320. ## Members & Invitations
  321. ######################################################
  322. class MembershipViewSet(ModelCrudViewSet):
  323. model = models.Membership
  324. admin_serializer_class = serializers.MembershipAdminSerializer
  325. serializer_class = serializers.MembershipSerializer
  326. permission_classes = (permissions.MembershipPermission,)
  327. filter_backends = (filters.CanViewProjectFilterBackend,)
  328. filter_fields = ("project", "role")
  329. def get_serializer_class(self):
  330. project_id = self.request.QUERY_PARAMS.get("project", None)
  331. if project_id is None:
  332. # Creation
  333. if self.request.method == 'POST':
  334. return self.admin_serializer_class
  335. return self.serializer_class
  336. project = get_object_or_404(models.Project, pk=project_id)
  337. if permissions_service.is_project_owner(self.request.user, project):
  338. return self.admin_serializer_class
  339. return self.serializer_class
  340. @list_route(methods=["POST"])
  341. def bulk_create(self, request, **kwargs):
  342. serializer = serializers.MembersBulkSerializer(data=request.DATA)
  343. if not serializer.is_valid():
  344. return response.BadRequest(serializer.errors)
  345. data = serializer.data
  346. project = models.Project.objects.get(id=data["project_id"])
  347. invitation_extra_text = data.get("invitation_extra_text", None)
  348. self.check_permissions(request, 'bulk_create', project)
  349. # TODO: this should be moved to main exception handler instead
  350. # of handling explicit exception catchin here.
  351. try:
  352. members = services.create_members_in_bulk(data["bulk_memberships"],
  353. project=project,
  354. invitation_extra_text=invitation_extra_text,
  355. callback=self.post_save,
  356. precall=self.pre_save)
  357. except ValidationError as err:
  358. return response.BadRequest(err.message_dict)
  359. members_serialized = self.admin_serializer_class(members, many=True)
  360. return response.Ok(members_serialized.data)
  361. @detail_route(methods=["POST"])
  362. def resend_invitation(self, request, **kwargs):
  363. invitation = self.get_object()
  364. self.check_permissions(request, 'resend_invitation', invitation.project)
  365. services.send_invitation(invitation=invitation)
  366. return response.NoContent()
  367. def pre_delete(self, obj):
  368. if obj.user is not None and not services.can_user_leave_project(obj.user, obj.project):
  369. raise exc.BadRequest(_("At least one of the user must be an active admin"))
  370. def pre_save(self, obj):
  371. if not obj.token:
  372. obj.token = str(uuid.uuid1())
  373. obj.invited_by = self.request.user
  374. obj.user = services.find_invited_user(obj.email, default=obj.user)
  375. super().pre_save(obj)
  376. def post_save(self, object, created=False):
  377. super().post_save(object, created=created)
  378. if not created:
  379. return
  380. # Send email only if a new membership is created
  381. services.send_invitation(invitation=object)
  382. class InvitationViewSet(ModelListViewSet):
  383. """
  384. Only used by front for get invitation by it token.
  385. """
  386. queryset = models.Membership.objects.filter(user__isnull=True)
  387. serializer_class = serializers.MembershipSerializer
  388. lookup_field = "token"
  389. permission_classes = (AllowAnyPermission,)
  390. def list(self, *args, **kwargs):
  391. raise exc.PermissionDenied(_("You don't have permisions to see that."))