models.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  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 itertools
  17. import uuid
  18. from django.core.exceptions import ValidationError
  19. from django.db import models
  20. from django.db.models import signals
  21. from django.apps import apps
  22. from django.conf import settings
  23. from django.dispatch import receiver
  24. from django.contrib.auth import get_user_model
  25. from django.utils.translation import ugettext_lazy as _
  26. from django.utils import timezone
  27. from django_pgjson.fields import JsonField
  28. from djorm_pgarray.fields import TextArrayField
  29. from taiga.permissions.permissions import ANON_PERMISSIONS, MEMBERS_PERMISSIONS
  30. from taiga.base.tags import TaggedMixin
  31. from taiga.base.utils.slug import slugify_uniquely
  32. from taiga.base.utils.dicts import dict_sum
  33. from taiga.base.utils.sequence import arithmetic_progression
  34. from taiga.base.utils.slug import slugify_uniquely_for_queryset
  35. from . import choices
  36. from . notifications.mixins import WatchedModelMixin
  37. class Membership(models.Model):
  38. # This model stores all project memberships. Also
  39. # stores invitations to memberships that does not have
  40. # assigned user.
  41. user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,
  42. related_name="memberships")
  43. project = models.ForeignKey("Project", null=False, blank=False,
  44. related_name="memberships")
  45. role = models.ForeignKey("users.Role", null=False, blank=False,
  46. related_name="memberships")
  47. is_owner = models.BooleanField(default=False, null=False, blank=False)
  48. # Invitation metadata
  49. email = models.EmailField(max_length=255, default=None, null=True, blank=True,
  50. verbose_name=_("email"))
  51. created_at = models.DateTimeField(default=timezone.now,
  52. verbose_name=_("create at"))
  53. token = models.CharField(max_length=60, blank=True, null=True, default=None,
  54. verbose_name=_("token"))
  55. invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="ihaveinvited+",
  56. null=True, blank=True)
  57. invitation_extra_text = models.TextField(null=True, blank=True,
  58. verbose_name=_("invitation extra text"))
  59. user_order = models.IntegerField(default=10000, null=False, blank=False,
  60. verbose_name=_("user order"))
  61. def clean(self):
  62. # TODO: Review and do it more robust
  63. memberships = Membership.objects.filter(user=self.user, project=self.project)
  64. if self.user and memberships.count() > 0 and memberships[0].id != self.id:
  65. raise ValidationError(_('The user is already member of the project'))
  66. class Meta:
  67. verbose_name = "membership"
  68. verbose_name_plural = "membershipss"
  69. unique_together = ("user", "project",)
  70. ordering = ["project", "user__full_name", "user__username", "user__email", "email"]
  71. permissions = (
  72. ("view_membership", "Can view membership"),
  73. )
  74. class ProjectDefaults(models.Model):
  75. default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL,
  76. related_name="+", null=True, blank=True,
  77. verbose_name=_("default points"))
  78. default_us_status = models.OneToOneField("projects.UserStoryStatus",
  79. on_delete=models.SET_NULL, related_name="+",
  80. null=True, blank=True,
  81. verbose_name=_("default US status"))
  82. default_task_status = models.OneToOneField("projects.TaskStatus",
  83. on_delete=models.SET_NULL, related_name="+",
  84. null=True, blank=True,
  85. verbose_name=_("default task status"))
  86. default_priority = models.OneToOneField("projects.Priority", on_delete=models.SET_NULL,
  87. related_name="+", null=True, blank=True,
  88. verbose_name=_("default priority"))
  89. default_severity = models.OneToOneField("projects.Severity", on_delete=models.SET_NULL,
  90. related_name="+", null=True, blank=True,
  91. verbose_name=_("default severity"))
  92. default_issue_status = models.OneToOneField("projects.IssueStatus",
  93. on_delete=models.SET_NULL, related_name="+",
  94. null=True, blank=True,
  95. verbose_name=_("default issue status"))
  96. default_issue_type = models.OneToOneField("projects.IssueType",
  97. on_delete=models.SET_NULL, related_name="+",
  98. null=True, blank=True,
  99. verbose_name=_("default issue type"))
  100. class Meta:
  101. abstract = True
  102. class Project(ProjectDefaults, WatchedModelMixin, TaggedMixin, models.Model):
  103. name = models.CharField(max_length=250, null=False, blank=False,
  104. verbose_name=_("name"))
  105. slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
  106. verbose_name=_("slug"))
  107. description = models.TextField(null=False, blank=False,
  108. verbose_name=_("description"))
  109. created_date = models.DateTimeField(null=False, blank=False,
  110. verbose_name=_("created date"),
  111. default=timezone.now)
  112. modified_date = models.DateTimeField(null=False, blank=False,
  113. verbose_name=_("modified date"))
  114. owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
  115. related_name="owned_projects", verbose_name=_("owner"))
  116. members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="projects",
  117. through="Membership", verbose_name=_("members"),
  118. through_fields=("project", "user"))
  119. total_milestones = models.IntegerField(default=0, null=False, blank=False,
  120. verbose_name=_("total of milestones"))
  121. total_story_points = models.FloatField(default=0, verbose_name=_("total story points"))
  122. is_backlog_activated = models.BooleanField(default=True, null=False, blank=True,
  123. verbose_name=_("active backlog panel"))
  124. is_kanban_activated = models.BooleanField(default=False, null=False, blank=True,
  125. verbose_name=_("active kanban panel"))
  126. is_wiki_activated = models.BooleanField(default=True, null=False, blank=True,
  127. verbose_name=_("active wiki panel"))
  128. is_issues_activated = models.BooleanField(default=True, null=False, blank=True,
  129. verbose_name=_("active issues panel"))
  130. videoconferences = models.CharField(max_length=250, null=True, blank=True,
  131. choices=choices.VIDEOCONFERENCES_CHOICES,
  132. verbose_name=_("videoconference system"))
  133. videoconferences_extra_data = models.CharField(max_length=250, null=True, blank=True,
  134. verbose_name=_("videoconference extra data"))
  135. creation_template = models.ForeignKey("projects.ProjectTemplate",
  136. related_name="projects", null=True,
  137. blank=True, default=None,
  138. verbose_name=_("creation template"))
  139. anon_permissions = TextArrayField(blank=True, null=True,
  140. default=[],
  141. verbose_name=_("anonymous permissions"),
  142. choices=ANON_PERMISSIONS)
  143. public_permissions = TextArrayField(blank=True, null=True,
  144. default=[],
  145. verbose_name=_("user permissions"),
  146. choices=MEMBERS_PERMISSIONS)
  147. is_private = models.BooleanField(default=True, null=False, blank=True,
  148. verbose_name=_("is private"))
  149. userstories_csv_uuid = models.CharField(max_length=32, editable=False,
  150. null=True, blank=True,
  151. default=None, db_index=True)
  152. tasks_csv_uuid = models.CharField(max_length=32, editable=False, null=True,
  153. blank=True, default=None, db_index=True)
  154. issues_csv_uuid = models.CharField(max_length=32, editable=False,
  155. null=True, blank=True, default=None,
  156. db_index=True)
  157. tags_colors = TextArrayField(dimension=2, null=False, blank=True, verbose_name=_("tags colors"), default=[])
  158. _importing = None
  159. class Meta:
  160. verbose_name = "project"
  161. verbose_name_plural = "projects"
  162. ordering = ["name"]
  163. permissions = (
  164. ("view_project", "Can view project"),
  165. )
  166. def __str__(self):
  167. return self.name
  168. def __repr__(self):
  169. return "<Project {0}>".format(self.id)
  170. def save(self, *args, **kwargs):
  171. if not self._importing or not self.modified_date:
  172. self.modified_date = timezone.now()
  173. if not self.slug:
  174. base_name = "{}-{}".format(self.owner.username, self.name)
  175. base_slug = slugify_uniquely(base_name, self.__class__)
  176. slug = base_slug
  177. for i in arithmetic_progression():
  178. if not type(self).objects.filter(slug=slug).exists() or i > 100:
  179. break
  180. slug = "{}-{}".format(base_slug, i)
  181. self.slug = slug
  182. if not self.videoconferences:
  183. self.videoconferences_extra_data = None
  184. super().save(*args, **kwargs)
  185. def get_roles(self):
  186. return self.roles.all()
  187. def get_users(self):
  188. user_model = get_user_model()
  189. members = self.memberships.values_list("user", flat=True)
  190. return user_model.objects.filter(id__in=list(members))
  191. def update_role_points(self, user_stories=None):
  192. RolePoints = apps.get_model("userstories", "RolePoints")
  193. Role = apps.get_model("users", "Role")
  194. # Get all available roles on this project
  195. roles = self.get_roles().filter(computable=True)
  196. if roles.count() == 0:
  197. return
  198. # Iter over all project user stories and create
  199. # role point instance for new created roles.
  200. if user_stories is None:
  201. user_stories = self.user_stories.all()
  202. # Get point instance that represent a null/undefined
  203. # The current model allows duplicate values. Because
  204. # of it, we should get all poins with None as value
  205. # and use the first one.
  206. # In case of that not exists, creates one for avoid
  207. # unexpected errors.
  208. none_points = list(self.points.filter(value=None))
  209. if none_points:
  210. null_points_value = none_points[0]
  211. else:
  212. name = slugify_uniquely_for_queryset("?", self.points.all(), slugfield="name")
  213. null_points_value = Points.objects.create(name=name, value=None, project=self)
  214. for us in user_stories:
  215. usroles = Role.objects.filter(role_points__in=us.role_points.all()).distinct()
  216. new_roles = roles.exclude(id__in=usroles)
  217. new_rolepoints = [RolePoints(role=role, user_story=us, points=null_points_value)
  218. for role in new_roles]
  219. RolePoints.objects.bulk_create(new_rolepoints)
  220. # Now remove rolepoints associated with not existing roles.
  221. rp_query = RolePoints.objects.filter(user_story__in=self.user_stories.all())
  222. rp_query = rp_query.exclude(role__id__in=roles.values_list("id", flat=True))
  223. rp_query.delete()
  224. def _get_user_stories_points(self, user_stories):
  225. role_points = [us.role_points.all() for us in user_stories]
  226. flat_role_points = itertools.chain(*role_points)
  227. flat_role_dicts = map(lambda x: {x.role_id: x.points.value if x.points.value else 0},
  228. flat_role_points)
  229. return dict_sum(*flat_role_dicts)
  230. def _get_points_increment(self, client_requirement, team_requirement):
  231. last_milestones = self.milestones.order_by('-estimated_finish')
  232. last_milestone = last_milestones[0] if last_milestones else None
  233. if last_milestone:
  234. user_stories = self.user_stories.filter(
  235. created_date__gte=last_milestone.estimated_finish,
  236. client_requirement=client_requirement,
  237. team_requirement=team_requirement
  238. )
  239. else:
  240. user_stories = self.user_stories.filter(
  241. client_requirement=client_requirement,
  242. team_requirement=team_requirement
  243. )
  244. user_stories = user_stories.prefetch_related('role_points', 'role_points__points')
  245. return self._get_user_stories_points(user_stories)
  246. @property
  247. def project(self):
  248. return self
  249. @property
  250. def project(self):
  251. return self
  252. @property
  253. def future_team_increment(self):
  254. team_increment = self._get_points_increment(False, True)
  255. shared_increment = {key: value / 2 for key, value in self.future_shared_increment.items()}
  256. return dict_sum(team_increment, shared_increment)
  257. @property
  258. def future_client_increment(self):
  259. client_increment = self._get_points_increment(True, False)
  260. shared_increment = {key: value / 2 for key, value in self.future_shared_increment.items()}
  261. return dict_sum(client_increment, shared_increment)
  262. @property
  263. def future_shared_increment(self):
  264. return self._get_points_increment(True, True)
  265. @property
  266. def closed_points(self):
  267. return self.calculated_points["closed"]
  268. @property
  269. def defined_points(self):
  270. return self.calculated_points["defined"]
  271. @property
  272. def assigned_points(self):
  273. return self.calculated_points["assigned"]
  274. @property
  275. def calculated_points(self):
  276. user_stories = self.user_stories.all().prefetch_related('role_points', 'role_points__points')
  277. closed_user_stories = user_stories.filter(is_closed=True)
  278. assigned_user_stories = user_stories.filter(milestone__isnull=False)
  279. return {
  280. "defined": self._get_user_stories_points(user_stories),
  281. "closed": self._get_user_stories_points(closed_user_stories),
  282. "assigned": self._get_user_stories_points(assigned_user_stories),
  283. }
  284. class ProjectModulesConfig(models.Model):
  285. project = models.OneToOneField("Project", null=False, blank=False,
  286. related_name="modules_config", verbose_name=_("project"))
  287. config = JsonField(null=True, blank=True, verbose_name=_("modules config"))
  288. class Meta:
  289. verbose_name = "project modules config"
  290. verbose_name_plural = "project modules configs"
  291. ordering = ["project"]
  292. # User Stories common Models
  293. class UserStoryStatus(models.Model):
  294. name = models.CharField(max_length=255, null=False, blank=False,
  295. verbose_name=_("name"))
  296. slug = models.SlugField(max_length=255, null=False, blank=True,
  297. verbose_name=_("slug"))
  298. order = models.IntegerField(default=10, null=False, blank=False,
  299. verbose_name=_("order"))
  300. is_closed = models.BooleanField(default=False, null=False, blank=True,
  301. verbose_name=_("is closed"))
  302. is_archived = models.BooleanField(default=False, null=False, blank=True,
  303. verbose_name=_("is archived"))
  304. color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
  305. verbose_name=_("color"))
  306. wip_limit = models.IntegerField(null=True, blank=True, default=None,
  307. verbose_name=_("work in progress limit"))
  308. project = models.ForeignKey("Project", null=False, blank=False,
  309. related_name="us_statuses", verbose_name=_("project"))
  310. class Meta:
  311. verbose_name = "user story status"
  312. verbose_name_plural = "user story statuses"
  313. ordering = ["project", "order", "name"]
  314. unique_together = (("project", "name"), ("project", "slug"))
  315. permissions = (
  316. ("view_userstorystatus", "Can view user story status"),
  317. )
  318. def __str__(self):
  319. return self.name
  320. def save(self, *args, **kwargs):
  321. qs = self.project.us_statuses
  322. if self.id:
  323. qs = qs.exclude(id=self.id)
  324. self.slug = slugify_uniquely_for_queryset(self.name, qs)
  325. return super().save(*args, **kwargs)
  326. class Points(models.Model):
  327. name = models.CharField(max_length=255, null=False, blank=False,
  328. verbose_name=_("name"))
  329. order = models.IntegerField(default=10, null=False, blank=False,
  330. verbose_name=_("order"))
  331. value = models.FloatField(default=None, null=True, blank=True,
  332. verbose_name=_("value"))
  333. project = models.ForeignKey("Project", null=False, blank=False,
  334. related_name="points", verbose_name=_("project"))
  335. class Meta:
  336. verbose_name = "points"
  337. verbose_name_plural = "points"
  338. ordering = ["project", "order", "name"]
  339. unique_together = ("project", "name")
  340. permissions = (
  341. ("view_points", "Can view points"),
  342. )
  343. def __str__(self):
  344. return self.name
  345. # Tasks common models
  346. class TaskStatus(models.Model):
  347. name = models.CharField(max_length=255, null=False, blank=False,
  348. verbose_name=_("name"))
  349. slug = models.SlugField(max_length=255, null=False, blank=True,
  350. verbose_name=_("slug"))
  351. order = models.IntegerField(default=10, null=False, blank=False,
  352. verbose_name=_("order"))
  353. is_closed = models.BooleanField(default=False, null=False, blank=True,
  354. verbose_name=_("is closed"))
  355. color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
  356. verbose_name=_("color"))
  357. project = models.ForeignKey("Project", null=False, blank=False,
  358. related_name="task_statuses", verbose_name=_("project"))
  359. class Meta:
  360. verbose_name = "task status"
  361. verbose_name_plural = "task statuses"
  362. ordering = ["project", "order", "name"]
  363. unique_together = (("project", "name"), ("project", "slug"))
  364. permissions = (
  365. ("view_taskstatus", "Can view task status"),
  366. )
  367. def __str__(self):
  368. return self.name
  369. def save(self, *args, **kwargs):
  370. qs = self.project.task_statuses
  371. if self.id:
  372. qs = qs.exclude(id=self.id)
  373. self.slug = slugify_uniquely_for_queryset(self.name, qs)
  374. return super().save(*args, **kwargs)
  375. # Issue common Models
  376. class Priority(models.Model):
  377. name = models.CharField(max_length=255, null=False, blank=False,
  378. verbose_name=_("name"))
  379. order = models.IntegerField(default=10, null=False, blank=False,
  380. verbose_name=_("order"))
  381. color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
  382. verbose_name=_("color"))
  383. project = models.ForeignKey("Project", null=False, blank=False,
  384. related_name="priorities", verbose_name=_("project"))
  385. class Meta:
  386. verbose_name = "priority"
  387. verbose_name_plural = "priorities"
  388. ordering = ["project", "order", "name"]
  389. unique_together = ("project", "name")
  390. permissions = (
  391. ("view_priority", "Can view priority"),
  392. )
  393. def __str__(self):
  394. return self.name
  395. class Severity(models.Model):
  396. name = models.CharField(max_length=255, null=False, blank=False,
  397. verbose_name=_("name"))
  398. order = models.IntegerField(default=10, null=False, blank=False,
  399. verbose_name=_("order"))
  400. color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
  401. verbose_name=_("color"))
  402. project = models.ForeignKey("Project", null=False, blank=False,
  403. related_name="severities", verbose_name=_("project"))
  404. class Meta:
  405. verbose_name = "severity"
  406. verbose_name_plural = "severities"
  407. ordering = ["project", "order", "name"]
  408. unique_together = ("project", "name")
  409. permissions = (
  410. ("view_severity", "Can view severity"),
  411. )
  412. def __str__(self):
  413. return self.name
  414. class IssueStatus(models.Model):
  415. name = models.CharField(max_length=255, null=False, blank=False,
  416. verbose_name=_("name"))
  417. slug = models.SlugField(max_length=255, null=False, blank=True,
  418. verbose_name=_("slug"))
  419. order = models.IntegerField(default=10, null=False, blank=False,
  420. verbose_name=_("order"))
  421. is_closed = models.BooleanField(default=False, null=False, blank=True,
  422. verbose_name=_("is closed"))
  423. color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
  424. verbose_name=_("color"))
  425. project = models.ForeignKey("Project", null=False, blank=False,
  426. related_name="issue_statuses", verbose_name=_("project"))
  427. class Meta:
  428. verbose_name = "issue status"
  429. verbose_name_plural = "issue statuses"
  430. ordering = ["project", "order", "name"]
  431. unique_together = (("project", "name"), ("project", "slug"))
  432. permissions = (
  433. ("view_issuestatus", "Can view issue status"),
  434. )
  435. def __str__(self):
  436. return self.name
  437. def save(self, *args, **kwargs):
  438. qs = self.project.issue_statuses
  439. if self.id:
  440. qs = qs.exclude(id=self.id)
  441. self.slug = slugify_uniquely_for_queryset(self.name, qs)
  442. return super().save(*args, **kwargs)
  443. class IssueType(models.Model):
  444. name = models.CharField(max_length=255, null=False, blank=False,
  445. verbose_name=_("name"))
  446. order = models.IntegerField(default=10, null=False, blank=False,
  447. verbose_name=_("order"))
  448. color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
  449. verbose_name=_("color"))
  450. project = models.ForeignKey("Project", null=False, blank=False,
  451. related_name="issue_types", verbose_name=_("project"))
  452. class Meta:
  453. verbose_name = "issue type"
  454. verbose_name_plural = "issue types"
  455. ordering = ["project", "order", "name"]
  456. unique_together = ("project", "name")
  457. permissions = (
  458. ("view_issuetype", "Can view issue type"),
  459. )
  460. def __str__(self):
  461. return self.name
  462. class ProjectTemplate(models.Model):
  463. name = models.CharField(max_length=250, null=False, blank=False,
  464. verbose_name=_("name"))
  465. slug = models.SlugField(max_length=250, null=False, blank=True,
  466. verbose_name=_("slug"), unique=True)
  467. description = models.TextField(null=False, blank=False,
  468. verbose_name=_("description"))
  469. created_date = models.DateTimeField(null=False, blank=False,
  470. verbose_name=_("created date"),
  471. default=timezone.now)
  472. modified_date = models.DateTimeField(null=False, blank=False,
  473. verbose_name=_("modified date"))
  474. default_owner_role = models.CharField(max_length=50, null=False,
  475. blank=False,
  476. verbose_name=_("default owner's role"))
  477. is_backlog_activated = models.BooleanField(default=True, null=False, blank=True,
  478. verbose_name=_("active backlog panel"))
  479. is_kanban_activated = models.BooleanField(default=False, null=False, blank=True,
  480. verbose_name=_("active kanban panel"))
  481. is_wiki_activated = models.BooleanField(default=True, null=False, blank=True,
  482. verbose_name=_("active wiki panel"))
  483. is_issues_activated = models.BooleanField(default=True, null=False, blank=True,
  484. verbose_name=_("active issues panel"))
  485. videoconferences = models.CharField(max_length=250, null=True, blank=True,
  486. choices=choices.VIDEOCONFERENCES_CHOICES,
  487. verbose_name=_("videoconference system"))
  488. videoconferences_extra_data = models.CharField(max_length=250, null=True, blank=True,
  489. verbose_name=_("videoconference extra data"))
  490. default_options = JsonField(null=True, blank=True, verbose_name=_("default options"))
  491. us_statuses = JsonField(null=True, blank=True, verbose_name=_("us statuses"))
  492. points = JsonField(null=True, blank=True, verbose_name=_("points"))
  493. task_statuses = JsonField(null=True, blank=True, verbose_name=_("task statuses"))
  494. issue_statuses = JsonField(null=True, blank=True, verbose_name=_("issue statuses"))
  495. issue_types = JsonField(null=True, blank=True, verbose_name=_("issue types"))
  496. priorities = JsonField(null=True, blank=True, verbose_name=_("priorities"))
  497. severities = JsonField(null=True, blank=True, verbose_name=_("severities"))
  498. roles = JsonField(null=True, blank=True, verbose_name=_("roles"))
  499. _importing = None
  500. class Meta:
  501. verbose_name = "project template"
  502. verbose_name_plural = "project templates"
  503. ordering = ["name"]
  504. def __str__(self):
  505. return self.name
  506. def __repr__(self):
  507. return "<Project Template {0}>".format(self.slug)
  508. def save(self, *args, **kwargs):
  509. if not self._importing or not self.modified_date:
  510. self.modified_date = timezone.now()
  511. super().save(*args, **kwargs)
  512. def load_data_from_project(self, project):
  513. self.is_backlog_activated = project.is_backlog_activated
  514. self.is_kanban_activated = project.is_kanban_activated
  515. self.is_wiki_activated = project.is_wiki_activated
  516. self.is_issues_activated = project.is_issues_activated
  517. self.videoconferences = project.videoconferences
  518. self.videoconferences_extra_data = project.videoconferences_extra_data
  519. self.default_options = {
  520. "points": getattr(project.default_points, "name", None),
  521. "us_status": getattr(project.default_us_status, "name", None),
  522. "task_status": getattr(project.default_task_status, "name", None),
  523. "issue_status": getattr(project.default_issue_status, "name", None),
  524. "issue_type": getattr(project.default_issue_type, "name", None),
  525. "priority": getattr(project.default_priority, "name", None),
  526. "severity": getattr(project.default_severity, "name", None)
  527. }
  528. self.us_statuses = []
  529. for us_status in project.us_statuses.all():
  530. self.us_statuses.append({
  531. "name": us_status.name,
  532. "slug": us_status.slug,
  533. "is_closed": us_status.is_closed,
  534. "is_archived": us_status.is_archived,
  535. "color": us_status.color,
  536. "wip_limit": us_status.wip_limit,
  537. "order": us_status.order,
  538. })
  539. self.points = []
  540. for us_point in project.points.all():
  541. self.points.append({
  542. "name": us_point.name,
  543. "value": us_point.value,
  544. "order": us_point.order,
  545. })
  546. self.task_statuses = []
  547. for task_status in project.task_statuses.all():
  548. self.task_statuses.append({
  549. "name": task_status.name,
  550. "slug": task_status.slug,
  551. "is_closed": task_status.is_closed,
  552. "color": task_status.color,
  553. "order": task_status.order,
  554. })
  555. self.issue_statuses = []
  556. for issue_status in project.issue_statuses.all():
  557. self.issue_statuses.append({
  558. "name": issue_status.name,
  559. "slug": issue_status.slug,
  560. "is_closed": issue_status.is_closed,
  561. "color": issue_status.color,
  562. "order": issue_status.order,
  563. })
  564. self.issue_types = []
  565. for issue_type in project.issue_types.all():
  566. self.issue_types.append({
  567. "name": issue_type.name,
  568. "color": issue_type.color,
  569. "order": issue_type.order,
  570. })
  571. self.priorities = []
  572. for priority in project.priorities.all():
  573. self.priorities.append({
  574. "name": priority.name,
  575. "color": priority.color,
  576. "order": priority.order,
  577. })
  578. self.severities = []
  579. for severity in project.severities.all():
  580. self.severities.append({
  581. "name": severity.name,
  582. "color": severity.color,
  583. "order": severity.order,
  584. })
  585. self.roles = []
  586. for role in project.roles.all():
  587. self.roles.append({
  588. "name": role.name,
  589. "slug": role.slug,
  590. "permissions": role.permissions,
  591. "order": role.order,
  592. "computable": role.computable
  593. })
  594. try:
  595. owner_membership = Membership.objects.get(project=project, user=project.owner)
  596. self.default_owner_role = owner_membership.role.slug
  597. except Membership.DoesNotExist:
  598. self.default_owner_role = self.roles[0].get("slug", None)
  599. def apply_to_project(self, project):
  600. Role = apps.get_model("users", "Role")
  601. if project.id is None:
  602. raise Exception("Project need an id (must be a saved project)")
  603. project.creation_template = self
  604. project.is_backlog_activated = self.is_backlog_activated
  605. project.is_kanban_activated = self.is_kanban_activated
  606. project.is_wiki_activated = self.is_wiki_activated
  607. project.is_issues_activated = self.is_issues_activated
  608. project.videoconferences = self.videoconferences
  609. project.videoconferences_extra_data = self.videoconferences_extra_data
  610. for us_status in self.us_statuses:
  611. UserStoryStatus.objects.create(
  612. name=us_status["name"],
  613. slug=us_status["slug"],
  614. is_closed=us_status["is_closed"],
  615. is_archived=us_status["is_archived"],
  616. color=us_status["color"],
  617. wip_limit=us_status["wip_limit"],
  618. order=us_status["order"],
  619. project=project
  620. )
  621. for point in self.points:
  622. Points.objects.create(
  623. name=point["name"],
  624. value=point["value"],
  625. order=point["order"],
  626. project=project
  627. )
  628. for task_status in self.task_statuses:
  629. TaskStatus.objects.create(
  630. name=task_status["name"],
  631. slug=task_status["slug"],
  632. is_closed=task_status["is_closed"],
  633. color=task_status["color"],
  634. order=task_status["order"],
  635. project=project
  636. )
  637. for issue_status in self.issue_statuses:
  638. IssueStatus.objects.create(
  639. name=issue_status["name"],
  640. slug=issue_status["slug"],
  641. is_closed=issue_status["is_closed"],
  642. color=issue_status["color"],
  643. order=issue_status["order"],
  644. project=project
  645. )
  646. for issue_type in self.issue_types:
  647. IssueType.objects.create(
  648. name=issue_type["name"],
  649. color=issue_type["color"],
  650. order=issue_type["order"],
  651. project=project
  652. )
  653. for priority in self.priorities:
  654. Priority.objects.create(
  655. name=priority["name"],
  656. color=priority["color"],
  657. order=priority["order"],
  658. project=project
  659. )
  660. for severity in self.severities:
  661. Severity.objects.create(
  662. name=severity["name"],
  663. color=severity["color"],
  664. order=severity["order"],
  665. project=project
  666. )
  667. for role in self.roles:
  668. Role.objects.create(
  669. name=role["name"],
  670. slug=role["slug"],
  671. order=role["order"],
  672. computable=role["computable"],
  673. project=project,
  674. permissions=role['permissions']
  675. )
  676. if self.points:
  677. project.default_points = Points.objects.get(name=self.default_options["points"],
  678. project=project)
  679. if self.us_statuses:
  680. project.default_us_status = UserStoryStatus.objects.get(name=self.default_options["us_status"],
  681. project=project)
  682. if self.task_statuses:
  683. project.default_task_status = TaskStatus.objects.get(name=self.default_options["task_status"],
  684. project=project)
  685. if self.issue_statuses:
  686. project.default_issue_status = IssueStatus.objects.get(name=self.default_options["issue_status"],
  687. project=project)
  688. if self.issue_types:
  689. project.default_issue_type = IssueType.objects.get(name=self.default_options["issue_type"],
  690. project=project)
  691. if self.priorities:
  692. project.default_priority = Priority.objects.get(name=self.default_options["priority"], project=project)
  693. if self.severities:
  694. project.default_severity = Severity.objects.get(name=self.default_options["severity"], project=project)
  695. return project