serializers.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  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 base64
  17. import copy
  18. import os
  19. from collections import OrderedDict
  20. from django.apps import apps
  21. from django.core.files.base import ContentFile
  22. from django.core.exceptions import ObjectDoesNotExist
  23. from django.core.exceptions import ValidationError
  24. from django.core.exceptions import ObjectDoesNotExist
  25. from django.utils.translation import ugettext as _
  26. from django.contrib.contenttypes.models import ContentType
  27. from taiga import mdrender
  28. from taiga.base.api import serializers
  29. from taiga.base.fields import JsonField, PgArrayField
  30. from taiga.projects import models as projects_models
  31. from taiga.projects.custom_attributes import models as custom_attributes_models
  32. from taiga.projects.userstories import models as userstories_models
  33. from taiga.projects.tasks import models as tasks_models
  34. from taiga.projects.issues import models as issues_models
  35. from taiga.projects.milestones import models as milestones_models
  36. from taiga.projects.wiki import models as wiki_models
  37. from taiga.projects.history import models as history_models
  38. from taiga.projects.attachments import models as attachments_models
  39. from taiga.timeline import models as timeline_models
  40. from taiga.timeline import service as timeline_service
  41. from taiga.users import models as users_models
  42. from taiga.projects.notifications import services as notifications_services
  43. from taiga.projects.votes import services as votes_service
  44. from taiga.projects.history import services as history_service
  45. class AttachedFileField(serializers.WritableField):
  46. read_only = False
  47. def to_native(self, obj):
  48. if not obj:
  49. return None
  50. data = base64.b64encode(obj.read()).decode('utf-8')
  51. return OrderedDict([
  52. ("data", data),
  53. ("name", os.path.basename(obj.name)),
  54. ])
  55. def from_native(self, data):
  56. if not data:
  57. return None
  58. return ContentFile(base64.b64decode(data['data']), name=data['name'])
  59. class RelatedNoneSafeField(serializers.RelatedField):
  60. def field_from_native(self, data, files, field_name, into):
  61. if self.read_only:
  62. return
  63. try:
  64. if self.many:
  65. try:
  66. # Form data
  67. value = data.getlist(field_name)
  68. if value == [''] or value == []:
  69. raise KeyError
  70. except AttributeError:
  71. # Non-form data
  72. value = data[field_name]
  73. else:
  74. value = data[field_name]
  75. except KeyError:
  76. if self.partial:
  77. return
  78. value = self.get_default_value()
  79. key = self.source or field_name
  80. if value in self.null_values:
  81. if self.required:
  82. raise ValidationError(self.error_messages['required'])
  83. into[key] = None
  84. elif self.many:
  85. into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None]
  86. else:
  87. into[key] = self.from_native(value)
  88. class UserRelatedField(RelatedNoneSafeField):
  89. read_only = False
  90. def to_native(self, obj):
  91. if obj:
  92. return obj.email
  93. return None
  94. def from_native(self, data):
  95. try:
  96. return users_models.User.objects.get(email=data)
  97. except users_models.User.DoesNotExist:
  98. return None
  99. class UserPkField(serializers.RelatedField):
  100. read_only = False
  101. def to_native(self, obj):
  102. try:
  103. user = users_models.User.objects.get(pk=obj)
  104. return user.email
  105. except users_models.User.DoesNotExist:
  106. return None
  107. def from_native(self, data):
  108. try:
  109. user = users_models.User.objects.get(email=data)
  110. return user.pk
  111. except users_models.User.DoesNotExist:
  112. return None
  113. class CommentField(serializers.WritableField):
  114. read_only = False
  115. def field_from_native(self, data, files, field_name, into):
  116. super().field_from_native(data, files, field_name, into)
  117. into["comment_html"] = mdrender.render(self.context['project'], data.get("comment", ""))
  118. class ProjectRelatedField(serializers.RelatedField):
  119. read_only = False
  120. def __init__(self, slug_field, *args, **kwargs):
  121. self.slug_field = slug_field
  122. super().__init__(*args, **kwargs)
  123. def to_native(self, obj):
  124. if obj:
  125. return getattr(obj, self.slug_field)
  126. return None
  127. def from_native(self, data):
  128. try:
  129. kwargs = {self.slug_field: data, "project": self.context['project']}
  130. return self.queryset.get(**kwargs)
  131. except ObjectDoesNotExist:
  132. raise ValidationError(_("{}=\"{}\" not found in this project".format(self.slug_field, data)))
  133. class HistoryUserField(JsonField):
  134. def to_native(self, obj):
  135. if obj is None or obj == {}:
  136. return []
  137. try:
  138. user = users_models.User.objects.get(pk=obj['pk'])
  139. except users_models.User.DoesNotExist:
  140. user = None
  141. return (UserRelatedField().to_native(user), obj['name'])
  142. def from_native(self, data):
  143. if data is None:
  144. return {}
  145. if len(data) < 2:
  146. return {}
  147. user = UserRelatedField().from_native(data[0])
  148. if user:
  149. pk = user.pk
  150. else:
  151. pk = None
  152. return {"pk": pk, "name": data[1]}
  153. class HistoryValuesField(JsonField):
  154. def to_native(self, obj):
  155. if obj is None:
  156. return []
  157. if "users" in obj:
  158. obj['users'] = list(map(UserPkField().to_native, obj['users']))
  159. return obj
  160. def from_native(self, data):
  161. if data is None:
  162. return []
  163. if "users" in data:
  164. data['users'] = list(map(UserPkField().from_native, data['users']))
  165. return data
  166. class HistoryDiffField(JsonField):
  167. def to_native(self, obj):
  168. if obj is None:
  169. return []
  170. if "assigned_to" in obj:
  171. obj['assigned_to'] = list(map(UserPkField().to_native, obj['assigned_to']))
  172. return obj
  173. def from_native(self, data):
  174. if data is None:
  175. return []
  176. if "assigned_to" in data:
  177. data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to']))
  178. return data
  179. class WatcheableObjectModelSerializer(serializers.ModelSerializer):
  180. watchers = UserRelatedField(many=True, required=False)
  181. def __init__(self, *args, **kwargs):
  182. self._watchers_field = self.base_fields.pop("watchers", None)
  183. super(WatcheableObjectModelSerializer, self).__init__(*args, **kwargs)
  184. """
  185. watchers is not a field from the model so we need to do some magic to make it work like a normal field
  186. It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances
  187. """
  188. def restore_object(self, attrs, instance=None):
  189. watcher_field = self.fields.pop("watchers", None)
  190. instance = super(WatcheableObjectModelSerializer, self).restore_object(attrs, instance)
  191. self._watchers = self.init_data.get("watchers", [])
  192. return instance
  193. def save_watchers(self):
  194. new_watcher_emails = set(self._watchers)
  195. old_watcher_emails = set(notifications_services.get_watchers(self.object).values_list("email", flat=True))
  196. adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
  197. removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
  198. User = apps.get_model("users", "User")
  199. adding_users = User.objects.filter(email__in=adding_watcher_emails)
  200. removing_users = User.objects.filter(email__in=removing_watcher_emails)
  201. for user in adding_users:
  202. notifications_services.add_watcher(self.object, user)
  203. for user in removing_users:
  204. notifications_services.remove_watcher(self.object, user)
  205. self.object.watchers = notifications_services.get_watchers(self.object)
  206. def to_native(self, obj):
  207. ret = super(WatcheableObjectModelSerializer, self).to_native(obj)
  208. ret["watchers"] = [user.email for user in notifications_services.get_watchers(obj)]
  209. return ret
  210. class HistoryExportSerializer(serializers.ModelSerializer):
  211. user = HistoryUserField()
  212. diff = HistoryDiffField(required=False)
  213. snapshot = JsonField(required=False)
  214. values = HistoryValuesField(required=False)
  215. comment = CommentField(required=False)
  216. delete_comment_date = serializers.DateTimeField(required=False)
  217. delete_comment_user = HistoryUserField(required=False)
  218. class Meta:
  219. model = history_models.HistoryEntry
  220. exclude = ("id", "comment_html", "key")
  221. class HistoryExportSerializerMixin(serializers.ModelSerializer):
  222. history = serializers.SerializerMethodField("get_history")
  223. def get_history(self, obj):
  224. history_qs = history_service.get_history_queryset_by_model_instance(obj,
  225. types=(history_models.HistoryType.change, history_models.HistoryType.create,))
  226. return HistoryExportSerializer(history_qs, many=True).data
  227. class AttachmentExportSerializer(serializers.ModelSerializer):
  228. owner = UserRelatedField(required=False)
  229. attached_file = AttachedFileField()
  230. modified_date = serializers.DateTimeField(required=False)
  231. class Meta:
  232. model = attachments_models.Attachment
  233. exclude = ('id', 'content_type', 'object_id', 'project')
  234. class AttachmentExportSerializerMixin(serializers.ModelSerializer):
  235. attachments = serializers.SerializerMethodField("get_attachments")
  236. def get_attachments(self, obj):
  237. content_type = ContentType.objects.get_for_model(obj.__class__)
  238. attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk,
  239. content_type=content_type)
  240. return AttachmentExportSerializer(attachments_qs, many=True).data
  241. class PointsExportSerializer(serializers.ModelSerializer):
  242. class Meta:
  243. model = projects_models.Points
  244. exclude = ('id', 'project')
  245. class UserStoryStatusExportSerializer(serializers.ModelSerializer):
  246. class Meta:
  247. model = projects_models.UserStoryStatus
  248. exclude = ('id', 'project')
  249. class TaskStatusExportSerializer(serializers.ModelSerializer):
  250. class Meta:
  251. model = projects_models.TaskStatus
  252. exclude = ('id', 'project')
  253. class IssueStatusExportSerializer(serializers.ModelSerializer):
  254. class Meta:
  255. model = projects_models.IssueStatus
  256. exclude = ('id', 'project')
  257. class PriorityExportSerializer(serializers.ModelSerializer):
  258. class Meta:
  259. model = projects_models.Priority
  260. exclude = ('id', 'project')
  261. class SeverityExportSerializer(serializers.ModelSerializer):
  262. class Meta:
  263. model = projects_models.Severity
  264. exclude = ('id', 'project')
  265. class IssueTypeExportSerializer(serializers.ModelSerializer):
  266. class Meta:
  267. model = projects_models.IssueType
  268. exclude = ('id', 'project')
  269. class RoleExportSerializer(serializers.ModelSerializer):
  270. permissions = PgArrayField(required=False)
  271. class Meta:
  272. model = users_models.Role
  273. exclude = ('id', 'project')
  274. class UserStoryCustomAttributeExportSerializer(serializers.ModelSerializer):
  275. modified_date = serializers.DateTimeField(required=False)
  276. class Meta:
  277. model = custom_attributes_models.UserStoryCustomAttribute
  278. exclude = ('id', 'project')
  279. class TaskCustomAttributeExportSerializer(serializers.ModelSerializer):
  280. modified_date = serializers.DateTimeField(required=False)
  281. class Meta:
  282. model = custom_attributes_models.TaskCustomAttribute
  283. exclude = ('id', 'project')
  284. class IssueCustomAttributeExportSerializer(serializers.ModelSerializer):
  285. modified_date = serializers.DateTimeField(required=False)
  286. class Meta:
  287. model = custom_attributes_models.IssueCustomAttribute
  288. exclude = ('id', 'project')
  289. class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer):
  290. custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values")
  291. def custom_attributes_queryset(self, project):
  292. raise NotImplementedError()
  293. def get_custom_attributes_values(self, obj):
  294. def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values):
  295. ret = {}
  296. for attr in custom_attributes:
  297. value = values.get(str(attr["id"]), None)
  298. if value is not None:
  299. ret[attr["name"]] = value
  300. return ret
  301. try:
  302. values = obj.custom_attributes_values.attributes_values
  303. custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name')
  304. return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values)
  305. except ObjectDoesNotExist:
  306. return None
  307. class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer):
  308. attributes_values = JsonField(source="attributes_values",required=True)
  309. _custom_attribute_model = None
  310. _container_field = None
  311. class Meta:
  312. exclude = ("id",)
  313. def validate_attributes_values(self, attrs, source):
  314. # values must be a dict
  315. data_values = attrs.get("attributes_values", None)
  316. if self.object:
  317. data_values = (data_values or self.object.attributes_values)
  318. if type(data_values) is not dict:
  319. raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
  320. # Values keys must be in the container object project
  321. data_container = attrs.get(self._container_field, None)
  322. if data_container:
  323. project_id = data_container.project_id
  324. elif self.object:
  325. project_id = getattr(self.object, self._container_field).project_id
  326. else:
  327. project_id = None
  328. values_ids = list(data_values.keys())
  329. qs = self._custom_attribute_model.objects.filter(project=project_id,
  330. id__in=values_ids)
  331. if qs.count() != len(values_ids):
  332. raise ValidationError(_("It contain invalid custom fields."))
  333. return attrs
  334. class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
  335. _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
  336. _container_model = "userstories.UserStory"
  337. _container_field = "user_story"
  338. class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
  339. model = custom_attributes_models.UserStoryCustomAttributesValues
  340. class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
  341. _custom_attribute_model = custom_attributes_models.TaskCustomAttribute
  342. _container_field = "task"
  343. class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
  344. model = custom_attributes_models.TaskCustomAttributesValues
  345. class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
  346. _custom_attribute_model = custom_attributes_models.IssueCustomAttribute
  347. _container_field = "issue"
  348. class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
  349. model = custom_attributes_models.IssueCustomAttributesValues
  350. class MembershipExportSerializer(serializers.ModelSerializer):
  351. user = UserRelatedField(required=False)
  352. role = ProjectRelatedField(slug_field="name")
  353. invited_by = UserRelatedField(required=False)
  354. class Meta:
  355. model = projects_models.Membership
  356. exclude = ('id', 'project', 'token')
  357. def full_clean(self, instance):
  358. return instance
  359. class RolePointsExportSerializer(serializers.ModelSerializer):
  360. role = ProjectRelatedField(slug_field="name")
  361. points = ProjectRelatedField(slug_field="name")
  362. class Meta:
  363. model = userstories_models.RolePoints
  364. exclude = ('id', 'user_story')
  365. class MilestoneExportSerializer(WatcheableObjectModelSerializer):
  366. owner = UserRelatedField(required=False)
  367. modified_date = serializers.DateTimeField(required=False)
  368. def __init__(self, *args, **kwargs):
  369. project = kwargs.pop('project', None)
  370. super(MilestoneExportSerializer, self).__init__(*args, **kwargs)
  371. if project:
  372. self.project = project
  373. def validate_name(self, attrs, source):
  374. """
  375. Check the milestone name is not duplicated in the project
  376. """
  377. name = attrs[source]
  378. qs = self.project.milestones.filter(name=name)
  379. if qs.exists():
  380. raise serializers.ValidationError(_("Name duplicated for the project"))
  381. return attrs
  382. class Meta:
  383. model = milestones_models.Milestone
  384. exclude = ('id', 'project')
  385. class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
  386. AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
  387. owner = UserRelatedField(required=False)
  388. status = ProjectRelatedField(slug_field="name")
  389. user_story = ProjectRelatedField(slug_field="ref", required=False)
  390. milestone = ProjectRelatedField(slug_field="name", required=False)
  391. assigned_to = UserRelatedField(required=False)
  392. modified_date = serializers.DateTimeField(required=False)
  393. class Meta:
  394. model = tasks_models.Task
  395. exclude = ('id', 'project')
  396. def custom_attributes_queryset(self, project):
  397. return project.taskcustomattributes.all()
  398. class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
  399. AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
  400. role_points = RolePointsExportSerializer(many=True, required=False)
  401. owner = UserRelatedField(required=False)
  402. assigned_to = UserRelatedField(required=False)
  403. status = ProjectRelatedField(slug_field="name")
  404. milestone = ProjectRelatedField(slug_field="name", required=False)
  405. modified_date = serializers.DateTimeField(required=False)
  406. generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
  407. class Meta:
  408. model = userstories_models.UserStory
  409. exclude = ('id', 'project', 'points', 'tasks')
  410. def custom_attributes_queryset(self, project):
  411. return project.userstorycustomattributes.all()
  412. class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
  413. AttachmentExportSerializerMixin, WatcheableObjectModelSerializer):
  414. owner = UserRelatedField(required=False)
  415. status = ProjectRelatedField(slug_field="name")
  416. assigned_to = UserRelatedField(required=False)
  417. priority = ProjectRelatedField(slug_field="name")
  418. severity = ProjectRelatedField(slug_field="name")
  419. type = ProjectRelatedField(slug_field="name")
  420. milestone = ProjectRelatedField(slug_field="name", required=False)
  421. votes = serializers.SerializerMethodField("get_votes")
  422. modified_date = serializers.DateTimeField(required=False)
  423. class Meta:
  424. model = issues_models.Issue
  425. exclude = ('id', 'project')
  426. def get_votes(self, obj):
  427. return [x.email for x in votes_service.get_voters(obj)]
  428. def custom_attributes_queryset(self, project):
  429. return project.issuecustomattributes.all()
  430. class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
  431. WatcheableObjectModelSerializer):
  432. owner = UserRelatedField(required=False)
  433. last_modifier = UserRelatedField(required=False)
  434. modified_date = serializers.DateTimeField(required=False)
  435. class Meta:
  436. model = wiki_models.WikiPage
  437. exclude = ('id', 'project')
  438. class WikiLinkExportSerializer(serializers.ModelSerializer):
  439. class Meta:
  440. model = wiki_models.WikiLink
  441. exclude = ('id', 'project')
  442. class TimelineDataField(serializers.WritableField):
  443. read_only = False
  444. def to_native(self, data):
  445. new_data = copy.deepcopy(data)
  446. try:
  447. user = users_models.User.objects.get(pk=new_data["user"]["id"])
  448. new_data["user"]["email"] = user.email
  449. del new_data["user"]["id"]
  450. except users_models.User.DoesNotExist:
  451. pass
  452. return new_data
  453. def from_native(self, data):
  454. new_data = copy.deepcopy(data)
  455. try:
  456. user = users_models.User.objects.get(email=new_data["user"]["email"])
  457. new_data["user"]["id"] = user.id
  458. del new_data["user"]["email"]
  459. except users_models.User.DoesNotExist:
  460. pass
  461. return new_data
  462. class TimelineExportSerializer(serializers.ModelSerializer):
  463. data = TimelineDataField()
  464. class Meta:
  465. model = timeline_models.Timeline
  466. exclude = ('id', 'project', 'namespace', 'object_id')
  467. class ProjectExportSerializer(WatcheableObjectModelSerializer):
  468. owner = UserRelatedField(required=False)
  469. default_points = serializers.SlugRelatedField(slug_field="name", required=False)
  470. default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)
  471. default_task_status = serializers.SlugRelatedField(slug_field="name", required=False)
  472. default_priority = serializers.SlugRelatedField(slug_field="name", required=False)
  473. default_severity = serializers.SlugRelatedField(slug_field="name", required=False)
  474. default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False)
  475. default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False)
  476. memberships = MembershipExportSerializer(many=True, required=False)
  477. points = PointsExportSerializer(many=True, required=False)
  478. us_statuses = UserStoryStatusExportSerializer(many=True, required=False)
  479. task_statuses = TaskStatusExportSerializer(many=True, required=False)
  480. issue_statuses = IssueStatusExportSerializer(many=True, required=False)
  481. priorities = PriorityExportSerializer(many=True, required=False)
  482. severities = SeverityExportSerializer(many=True, required=False)
  483. issue_types = IssueTypeExportSerializer(many=True, required=False)
  484. userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False)
  485. taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False)
  486. issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False)
  487. roles = RoleExportSerializer(many=True, required=False)
  488. milestones = MilestoneExportSerializer(many=True, required=False)
  489. wiki_pages = WikiPageExportSerializer(many=True, required=False)
  490. wiki_links = WikiLinkExportSerializer(many=True, required=False)
  491. user_stories = UserStoryExportSerializer(many=True, required=False)
  492. tasks = TaskExportSerializer(many=True, required=False)
  493. issues = IssueExportSerializer(many=True, required=False)
  494. tags_colors = JsonField(required=False)
  495. anon_permissions = PgArrayField(required=False)
  496. public_permissions = PgArrayField(required=False)
  497. modified_date = serializers.DateTimeField(required=False)
  498. timeline = serializers.SerializerMethodField("get_timeline")
  499. class Meta:
  500. model = projects_models.Project
  501. exclude = ('id', 'creation_template', 'members')
  502. def get_timeline(self, obj):
  503. timeline_qs = timeline_service.get_project_timeline(obj)
  504. return TimelineExportSerializer(timeline_qs, many=True).data