Merge pull request #994 from taigaio/issue/5137/imported-projects-notify-incorrect-users-in-some-cases
Imported projects notify incorrect users in some casesremotes/origin/fix-throttling-bug
commit
e7c58fa670
|
@ -24,6 +24,10 @@ _custom_tasks_attributes_cache = {}
|
||||||
_custom_issues_attributes_cache = {}
|
_custom_issues_attributes_cache = {}
|
||||||
_custom_userstories_attributes_cache = {}
|
_custom_userstories_attributes_cache = {}
|
||||||
_custom_epics_attributes_cache = {}
|
_custom_epics_attributes_cache = {}
|
||||||
|
_tasks_statuses_cache = {}
|
||||||
|
_issues_statuses_cache = {}
|
||||||
|
_userstories_statuses_cache = {}
|
||||||
|
_epics_statuses_cache = {}
|
||||||
|
|
||||||
def cached_get_user_by_pk(pk):
|
def cached_get_user_by_pk(pk):
|
||||||
if pk not in _cache_user_by_pk:
|
if pk not in _cache_user_by_pk:
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
@ -25,14 +26,15 @@ from taiga.projects.history import models as history_models
|
||||||
from taiga.projects.attachments import models as attachments_models
|
from taiga.projects.attachments import models as attachments_models
|
||||||
from taiga.projects.history import services as history_service
|
from taiga.projects.history import services as history_service
|
||||||
|
|
||||||
from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField,
|
from .cache import cached_get_user_by_pk
|
||||||
HistoryValuesField, FileField)
|
from .fields import (UserRelatedField, HistoryUserField,
|
||||||
|
HistoryDiffField, HistoryValuesField,
|
||||||
|
SlugRelatedField, FileField)
|
||||||
|
|
||||||
class HistoryExportSerializer(serializers.LightSerializer):
|
class HistoryExportSerializer(serializers.LightSerializer):
|
||||||
user = HistoryUserField()
|
user = HistoryUserField()
|
||||||
diff = HistoryDiffField()
|
diff = HistoryDiffField()
|
||||||
snapshot = Field()
|
snapshot = MethodField()
|
||||||
values = HistoryValuesField()
|
values = HistoryValuesField()
|
||||||
comment = Field()
|
comment = Field()
|
||||||
delete_comment_date = DateTimeField()
|
delete_comment_date = DateTimeField()
|
||||||
|
@ -44,17 +46,49 @@ class HistoryExportSerializer(serializers.LightSerializer):
|
||||||
is_snapshot = Field()
|
is_snapshot = Field()
|
||||||
type = Field()
|
type = Field()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Don't pass the extra ids args up to the superclass
|
||||||
|
self.statuses_queryset = kwargs.pop("statuses_queryset", {})
|
||||||
|
|
||||||
|
# Instantiate the superclass normally
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_snapshot(self, obj):
|
||||||
|
user_model_cls = get_user_model()
|
||||||
|
|
||||||
|
snapshot = obj.snapshot
|
||||||
|
if snapshot is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
owner = cached_get_user_by_pk(snapshot.get("owner", None))
|
||||||
|
snapshot["owner"] = owner.email
|
||||||
|
except user_model_cls.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
assigned_to = cached_get_user_by_pk(snapshot.get("assigned_to", None))
|
||||||
|
snapshot["assigned_to"] = assigned_to.email
|
||||||
|
except user_model_cls.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if "status" in snapshot:
|
||||||
|
snapshot["status"] = self.statuses_queryset.get(snapshot["status"])
|
||||||
|
|
||||||
|
return snapshot
|
||||||
|
|
||||||
class HistoryExportSerializerMixin(serializers.LightSerializer):
|
class HistoryExportSerializerMixin(serializers.LightSerializer):
|
||||||
history = MethodField("get_history")
|
history = MethodField("get_history")
|
||||||
|
|
||||||
|
def statuses_queryset(self, project):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_history(self, obj):
|
def get_history(self, obj):
|
||||||
history_qs = history_service.get_history_queryset_by_model_instance(
|
history_qs = history_service.get_history_queryset_by_model_instance(
|
||||||
obj,
|
obj,
|
||||||
types=(history_models.HistoryType.change, history_models.HistoryType.create,)
|
types=(history_models.HistoryType.change, history_models.HistoryType.create,)
|
||||||
)
|
)
|
||||||
|
return HistoryExportSerializer(history_qs, many=True, statuses_queryset=self.statuses_queryset(obj.project)).data
|
||||||
return HistoryExportSerializer(history_qs, many=True).data
|
|
||||||
|
|
||||||
|
|
||||||
class AttachmentExportSerializer(serializers.LightSerializer):
|
class AttachmentExportSerializer(serializers.LightSerializer):
|
||||||
|
|
|
@ -30,7 +30,11 @@ from .mixins import (HistoryExportSerializerMixin,
|
||||||
from .cache import (_custom_tasks_attributes_cache,
|
from .cache import (_custom_tasks_attributes_cache,
|
||||||
_custom_userstories_attributes_cache,
|
_custom_userstories_attributes_cache,
|
||||||
_custom_epics_attributes_cache,
|
_custom_epics_attributes_cache,
|
||||||
_custom_issues_attributes_cache)
|
_custom_issues_attributes_cache,
|
||||||
|
_tasks_statuses_cache,
|
||||||
|
_issues_statuses_cache,
|
||||||
|
_userstories_statuses_cache,
|
||||||
|
_epics_statuses_cache)
|
||||||
|
|
||||||
|
|
||||||
class RelatedExportSerializer(serializers.LightSerializer):
|
class RelatedExportSerializer(serializers.LightSerializer):
|
||||||
|
@ -217,6 +221,11 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin,
|
||||||
_custom_tasks_attributes_cache[project.id] = list(project.taskcustomattributes.all().values('id', 'name'))
|
_custom_tasks_attributes_cache[project.id] = list(project.taskcustomattributes.all().values('id', 'name'))
|
||||||
return _custom_tasks_attributes_cache[project.id]
|
return _custom_tasks_attributes_cache[project.id]
|
||||||
|
|
||||||
|
def statuses_queryset(self, project):
|
||||||
|
if project.id not in _tasks_statuses_cache:
|
||||||
|
_tasks_statuses_cache[project.id] = {s.id: s.name for s in project.task_statuses.all()}
|
||||||
|
return _tasks_statuses_cache[project.id]
|
||||||
|
|
||||||
|
|
||||||
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin,
|
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin,
|
||||||
HistoryExportSerializerMixin,
|
HistoryExportSerializerMixin,
|
||||||
|
@ -255,6 +264,10 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin,
|
||||||
)
|
)
|
||||||
return _custom_userstories_attributes_cache[project.id]
|
return _custom_userstories_attributes_cache[project.id]
|
||||||
|
|
||||||
|
def statuses_queryset(self, project):
|
||||||
|
if project.id not in _userstories_statuses_cache:
|
||||||
|
_userstories_statuses_cache[project.id] = {s.id: s.name for s in project.us_statuses.all()}
|
||||||
|
return _userstories_statuses_cache[project.id]
|
||||||
|
|
||||||
class EpicRelatedUserStoryExportSerializer(RelatedExportSerializer):
|
class EpicRelatedUserStoryExportSerializer(RelatedExportSerializer):
|
||||||
user_story = SlugRelatedField(slug_field="ref")
|
user_story = SlugRelatedField(slug_field="ref")
|
||||||
|
@ -285,15 +298,20 @@ class EpicExportSerializer(CustomAttributesValuesExportSerializerMixin,
|
||||||
related_user_stories = MethodField()
|
related_user_stories = MethodField()
|
||||||
|
|
||||||
def get_related_user_stories(self, obj):
|
def get_related_user_stories(self, obj):
|
||||||
return EpicRelatedUserStoryExportSerializer(obj.relateduserstory_set.all(), many=True).data
|
return EpicRelatedUserStoryExportSerializer(obj.relateduserstory_set.filter(epic__project=obj.project), many=True).data
|
||||||
|
|
||||||
def custom_attributes_queryset(self, project):
|
def custom_attributes_queryset(self, project):
|
||||||
if project.id not in _custom_epics_attributes_cache:
|
if project.id not in _custom_epics_attributes_cache:
|
||||||
_custom_epics_attributes_cache[project.id] = list(
|
_custom_epics_attributes_cache[project.id] = list(
|
||||||
project.userstorycustomattributes.all().values('id', 'name')
|
project.epiccustomattributes.all().values('id', 'name')
|
||||||
)
|
)
|
||||||
return _custom_epics_attributes_cache[project.id]
|
return _custom_epics_attributes_cache[project.id]
|
||||||
|
|
||||||
|
def statuses_queryset(self, project):
|
||||||
|
if project.id not in _epics_statuses_cache:
|
||||||
|
_epics_statuses_cache[project.id] = {s.id: s.name for s in project.epic_statuses.all()}
|
||||||
|
return _epics_statuses_cache[project.id]
|
||||||
|
|
||||||
|
|
||||||
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin,
|
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin,
|
||||||
HistoryExportSerializerMixin,
|
HistoryExportSerializerMixin,
|
||||||
|
@ -329,6 +347,10 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin,
|
||||||
_custom_issues_attributes_cache[project.id] = list(project.issuecustomattributes.all().values('id', 'name'))
|
_custom_issues_attributes_cache[project.id] = list(project.issuecustomattributes.all().values('id', 'name'))
|
||||||
return _custom_issues_attributes_cache[project.id]
|
return _custom_issues_attributes_cache[project.id]
|
||||||
|
|
||||||
|
def statuses_queryset(self, project):
|
||||||
|
if project.id not in _issues_statuses_cache:
|
||||||
|
_issues_statuses_cache[project.id] = {s.id: s.name for s in project.issue_statuses.all()}
|
||||||
|
return _issues_statuses_cache[project.id]
|
||||||
|
|
||||||
class WikiPageExportSerializer(HistoryExportSerializerMixin,
|
class WikiPageExportSerializer(HistoryExportSerializerMixin,
|
||||||
AttachmentExportSerializerMixin,
|
AttachmentExportSerializerMixin,
|
||||||
|
@ -342,6 +364,8 @@ class WikiPageExportSerializer(HistoryExportSerializerMixin,
|
||||||
content = Field()
|
content = Field()
|
||||||
version = Field()
|
version = Field()
|
||||||
|
|
||||||
|
def statuses_queryset(self, project):
|
||||||
|
return {}
|
||||||
|
|
||||||
class WikiLinkExportSerializer(RelatedExportSerializer):
|
class WikiLinkExportSerializer(RelatedExportSerializer):
|
||||||
title = Field()
|
title = Field()
|
||||||
|
|
|
@ -155,8 +155,8 @@ def _store_attachment(project, obj, attachment):
|
||||||
return validator
|
return validator
|
||||||
|
|
||||||
|
|
||||||
def _store_history(project, obj, history):
|
def _store_history(project, obj, history, statuses={}):
|
||||||
validator = validators.HistoryExportValidator(data=history, context={"project": project})
|
validator = validators.HistoryExportValidator(data=history, context={"project": project, "statuses": statuses})
|
||||||
if validator.is_valid():
|
if validator.is_valid():
|
||||||
validator.object.key = make_key_from_model_object(obj)
|
validator.object.key = make_key_from_model_object(obj)
|
||||||
if validator.object.diff is None:
|
if validator.object.diff is None:
|
||||||
|
@ -360,8 +360,9 @@ def store_user_story(project, data):
|
||||||
_store_role_point(project, validator.object, role_point)
|
_store_role_point(project, validator.object, role_point)
|
||||||
|
|
||||||
history_entries = data.get("history", [])
|
history_entries = data.get("history", [])
|
||||||
|
statuses = {s.name: s.id for s in project.us_statuses.all()}
|
||||||
for history in history_entries:
|
for history in history_entries:
|
||||||
_store_history(project, validator.object, history)
|
_store_history(project, validator.object, history, statuses)
|
||||||
|
|
||||||
if not history_entries:
|
if not history_entries:
|
||||||
take_snapshot(validator.object, user=validator.object.owner)
|
take_snapshot(validator.object, user=validator.object.owner)
|
||||||
|
@ -436,8 +437,9 @@ def store_epic(project, data):
|
||||||
_store_epic_related_user_story(project, validator.object, related_user_story)
|
_store_epic_related_user_story(project, validator.object, related_user_story)
|
||||||
|
|
||||||
history_entries = data.get("history", [])
|
history_entries = data.get("history", [])
|
||||||
|
statuses = {s.name: s.id for s in project.epic_statuses.all()}
|
||||||
for history in history_entries:
|
for history in history_entries:
|
||||||
_store_history(project, validator.object, history)
|
_store_history(project, validator.object, history, statuses)
|
||||||
|
|
||||||
if not history_entries:
|
if not history_entries:
|
||||||
take_snapshot(validator.object, user=validator.object.owner)
|
take_snapshot(validator.object, user=validator.object.owner)
|
||||||
|
@ -496,8 +498,9 @@ def store_task(project, data):
|
||||||
_store_attachment(project, validator.object, task_attachment)
|
_store_attachment(project, validator.object, task_attachment)
|
||||||
|
|
||||||
history_entries = data.get("history", [])
|
history_entries = data.get("history", [])
|
||||||
|
statuses = {s.name: s.id for s in project.task_statuses.all()}
|
||||||
for history in history_entries:
|
for history in history_entries:
|
||||||
_store_history(project, validator.object, history)
|
_store_history(project, validator.object, history, statuses)
|
||||||
|
|
||||||
if not history_entries:
|
if not history_entries:
|
||||||
take_snapshot(validator.object, user=validator.object.owner)
|
take_snapshot(validator.object, user=validator.object.owner)
|
||||||
|
@ -567,8 +570,9 @@ def store_issue(project, data):
|
||||||
_store_attachment(project, validator.object, attachment)
|
_store_attachment(project, validator.object, attachment)
|
||||||
|
|
||||||
history_entries = data.get("history", [])
|
history_entries = data.get("history", [])
|
||||||
|
statuses = {s.name: s.id for s in project.issue_statuses.all()}
|
||||||
for history in history_entries:
|
for history in history_entries:
|
||||||
_store_history(project, validator.object, history)
|
_store_history(project, validator.object, history, statuses)
|
||||||
|
|
||||||
if not history_entries:
|
if not history_entries:
|
||||||
take_snapshot(validator.object, user=validator.object.owner)
|
take_snapshot(validator.object, user=validator.object.owner)
|
||||||
|
|
|
@ -12,6 +12,7 @@ from .validators import UserStoryCustomAttributeExportValidator
|
||||||
from .validators import TaskCustomAttributeExportValidator
|
from .validators import TaskCustomAttributeExportValidator
|
||||||
from .validators import IssueCustomAttributeExportValidator
|
from .validators import IssueCustomAttributeExportValidator
|
||||||
from .validators import BaseCustomAttributesValuesExportValidator
|
from .validators import BaseCustomAttributesValuesExportValidator
|
||||||
|
from .validators import EpicCustomAttributesValuesExportValidator
|
||||||
from .validators import UserStoryCustomAttributesValuesExportValidator
|
from .validators import UserStoryCustomAttributesValuesExportValidator
|
||||||
from .validators import TaskCustomAttributesValuesExportValidator
|
from .validators import TaskCustomAttributesValuesExportValidator
|
||||||
from .validators import IssueCustomAttributesValuesExportValidator
|
from .validators import IssueCustomAttributesValuesExportValidator
|
||||||
|
|
|
@ -144,6 +144,21 @@ class ProjectRelatedField(serializers.RelatedField):
|
||||||
raise ValidationError(_("{}=\"{}\" not found in this project".format(self.slug_field, data)))
|
raise ValidationError(_("{}=\"{}\" not found in this project".format(self.slug_field, data)))
|
||||||
|
|
||||||
|
|
||||||
|
class HistorySnapshotField(JSONField):
|
||||||
|
def from_native(self, data):
|
||||||
|
if data is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
owner = UserRelatedField().from_native(data.get("owner"))
|
||||||
|
if owner:
|
||||||
|
data["owner"] = owner.pk
|
||||||
|
|
||||||
|
assigned_to = UserRelatedField().from_native(data.get("assigned_to"))
|
||||||
|
if assigned_to:
|
||||||
|
data["assigned_to"] = assigned_to.pk
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
class HistoryUserField(JSONField):
|
class HistoryUserField(JSONField):
|
||||||
def from_native(self, data):
|
def from_native(self, data):
|
||||||
if data is None:
|
if data is None:
|
||||||
|
|
|
@ -28,13 +28,14 @@ from taiga.projects.notifications import services as notifications_services
|
||||||
from taiga.projects.history import services as history_service
|
from taiga.projects.history import services as history_service
|
||||||
|
|
||||||
from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField,
|
from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField,
|
||||||
JSONField, HistoryValuesField, CommentField, FileField)
|
JSONField, HistorySnapshotField,
|
||||||
|
HistoryValuesField, CommentField, FileField)
|
||||||
|
|
||||||
|
|
||||||
class HistoryExportValidator(validators.ModelValidator):
|
class HistoryExportValidator(validators.ModelValidator):
|
||||||
user = HistoryUserField()
|
user = HistoryUserField()
|
||||||
diff = HistoryDiffField(required=False)
|
diff = HistoryDiffField(required=False)
|
||||||
snapshot = JSONField(required=False)
|
snapshot = HistorySnapshotField(required=False)
|
||||||
values = HistoryValuesField(required=False)
|
values = HistoryValuesField(required=False)
|
||||||
comment = CommentField(required=False)
|
comment = CommentField(required=False)
|
||||||
delete_comment_date = serializers.DateTimeField(required=False)
|
delete_comment_date = serializers.DateTimeField(required=False)
|
||||||
|
@ -44,6 +45,15 @@ class HistoryExportValidator(validators.ModelValidator):
|
||||||
model = history_models.HistoryEntry
|
model = history_models.HistoryEntry
|
||||||
exclude = ("id", "comment_html", "key", "project")
|
exclude = ("id", "comment_html", "key", "project")
|
||||||
|
|
||||||
|
def restore_object(self, attrs, instance=None):
|
||||||
|
snapshot = attrs["snapshot"]
|
||||||
|
statuses = self.context.get("statuses", {})
|
||||||
|
if "status" in snapshot:
|
||||||
|
status_id = statuses.get(snapshot["status"], None)
|
||||||
|
attrs["snapshot"]["status"] = status_id
|
||||||
|
|
||||||
|
instance = super(HistoryExportValidator, self).restore_object(attrs, instance)
|
||||||
|
return instance
|
||||||
|
|
||||||
class AttachmentExportValidator(validators.ModelValidator):
|
class AttachmentExportValidator(validators.ModelValidator):
|
||||||
owner = UserRelatedField(required=False)
|
owner = UserRelatedField(required=False)
|
||||||
|
|
Loading…
Reference in New Issue