Fixing slug duplications on race conditions
parent
5eb6bfe1e0
commit
3e9bfd7523
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
from django_pglocks import advisory_lock
|
from django_pglocks import advisory_lock
|
||||||
|
|
||||||
|
|
||||||
def detail_route(methods=['get'], **kwargs):
|
def detail_route(methods=['get'], **kwargs):
|
||||||
"""
|
"""
|
||||||
Used to mark a method on a ViewSet that should be routed for detail requests.
|
Used to mark a method on a ViewSet that should be routed for detail requests.
|
||||||
|
@ -51,12 +52,11 @@ def model_pk_lock(func):
|
||||||
"""
|
"""
|
||||||
def decorator(self, *args, **kwargs):
|
def decorator(self, *args, **kwargs):
|
||||||
from taiga.base.utils.db import get_typename_for_model_class
|
from taiga.base.utils.db import get_typename_for_model_class
|
||||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
|
||||||
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
pk = self.kwargs.get(self.pk_url_kwarg, None)
|
||||||
tn = get_typename_for_model_class(self.get_queryset().model)
|
tn = get_typename_for_model_class(self.get_queryset().model)
|
||||||
key = "{0}:{1}".format(tn, pk)
|
key = "{0}:{1}".format(tn, pk)
|
||||||
|
|
||||||
with advisory_lock(key) as acquired_key_lock:
|
with advisory_lock(key):
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
|
@ -26,6 +26,8 @@ from django.http import Http404
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from django_pglocks import advisory_lock
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
|
@ -214,6 +216,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
if not template_description:
|
if not template_description:
|
||||||
raise response.BadRequest(_("Not valid template description"))
|
raise response.BadRequest(_("Not valid template description"))
|
||||||
|
|
||||||
|
with advisory_lock("create-project-template") as acquired_key_lock:
|
||||||
template_slug = slugify_uniquely(template_name, models.ProjectTemplate)
|
template_slug = slugify_uniquely(template_name, models.ProjectTemplate)
|
||||||
|
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
|
@ -227,6 +230,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
)
|
)
|
||||||
|
|
||||||
template.load_data_from_project(project)
|
template.load_data_from_project(project)
|
||||||
|
|
||||||
template.save()
|
template.save()
|
||||||
return response.Created(serializers.ProjectTemplateSerializer(template).data)
|
return response.Created(serializers.ProjectTemplateSerializer(template).data)
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# 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.apps import apps
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Prefetch, Count
|
from django.db.models import Count
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -28,7 +27,7 @@ from django.utils.functional import cached_property
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
from taiga.base.utils.dicts import dict_sum
|
from taiga.base.utils.dicts import dict_sum
|
||||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||||
from taiga.projects.userstories.models import UserStory
|
from django_pglocks import advisory_lock
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -84,8 +83,10 @@ class Milestone(WatchedModelMixin, models.Model):
|
||||||
if not self._importing or not self.modified_date:
|
if not self._importing or not self.modified_date:
|
||||||
self.modified_date = timezone.now()
|
self.modified_date = timezone.now()
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
|
with advisory_lock("milestone-creation-{}".format(self.project_id)):
|
||||||
self.slug = slugify_uniquely(self.name, self.__class__)
|
self.slug = slugify_uniquely(self.name, self.__class__)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
else:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|
|
@ -16,27 +16,23 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import itertools
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import signals, Q
|
from django.db.models import Q
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
from django_pglocks import advisory_lock
|
||||||
|
|
||||||
from django_pgjson.fields import JsonField
|
from django_pgjson.fields import JsonField
|
||||||
|
|
||||||
from taiga.projects.tagging.models import TaggedMixin
|
from taiga.projects.tagging.models import TaggedMixin
|
||||||
from taiga.projects.tagging.models import TagsColorsdMixin
|
from taiga.projects.tagging.models import TagsColorsdMixin
|
||||||
from taiga.base.utils.dicts import dict_sum
|
|
||||||
from taiga.base.utils.files import get_file_path
|
from taiga.base.utils.files import get_file_path
|
||||||
from taiga.base.utils.sequence import arithmetic_progression
|
from taiga.base.utils.sequence import arithmetic_progression
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
|
@ -270,16 +266,6 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model):
|
||||||
if not self._importing or not self.modified_date:
|
if not self._importing or not self.modified_date:
|
||||||
self.modified_date = timezone.now()
|
self.modified_date = timezone.now()
|
||||||
|
|
||||||
if not self.slug:
|
|
||||||
base_name = "{}-{}".format(self.owner.username, self.name)
|
|
||||||
base_slug = slugify_uniquely(base_name, self.__class__)
|
|
||||||
slug = base_slug
|
|
||||||
for i in arithmetic_progression():
|
|
||||||
if not type(self).objects.filter(slug=slug).exists() or i > 100:
|
|
||||||
break
|
|
||||||
slug = "{}-{}".format(base_slug, i)
|
|
||||||
self.slug = slug
|
|
||||||
|
|
||||||
if not self.is_backlog_activated:
|
if not self.is_backlog_activated:
|
||||||
self.total_milestones = None
|
self.total_milestones = None
|
||||||
self.total_story_points = None
|
self.total_story_points = None
|
||||||
|
@ -290,12 +276,24 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model):
|
||||||
if not self.is_looking_for_people:
|
if not self.is_looking_for_people:
|
||||||
self.looking_for_people_note = ""
|
self.looking_for_people_note = ""
|
||||||
|
|
||||||
if self.anon_permissions == None:
|
if self.anon_permissions is None:
|
||||||
self.anon_permissions = []
|
self.anon_permissions = []
|
||||||
|
|
||||||
if self.public_permissions == None:
|
if self.public_permissions is None:
|
||||||
self.public_permissions = []
|
self.public_permissions = []
|
||||||
|
|
||||||
|
if not self.slug:
|
||||||
|
with advisory_lock("project-creation"):
|
||||||
|
base_name = "{}-{}".format(self.owner.username, self.name)
|
||||||
|
base_slug = slugify_uniquely(base_name, self.__class__)
|
||||||
|
slug = base_slug
|
||||||
|
for i in arithmetic_progression():
|
||||||
|
if not type(self).objects.filter(slug=slug).exists() or i > 100:
|
||||||
|
break
|
||||||
|
slug = "{}-{}".format(base_slug, i)
|
||||||
|
self.slug = slug
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
else:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def refresh_totals(self, save=True):
|
def refresh_totals(self, save=True):
|
||||||
|
|
|
@ -21,6 +21,8 @@ from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django_pglocks import advisory_lock
|
||||||
|
|
||||||
from taiga.base.utils.slug import slugify_uniquely_for_queryset
|
from taiga.base.utils.slug import slugify_uniquely_for_queryset
|
||||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||||
from taiga.projects.occ import OCCModelMixin
|
from taiga.projects.occ import OCCModelMixin
|
||||||
|
@ -84,7 +86,9 @@ class WikiLink(models.Model):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.href:
|
if not self.href:
|
||||||
|
with advisory_lock("wiki-page-creation-{}".format(self.project_id)):
|
||||||
wl_qs = self.project.wiki_links.all()
|
wl_qs = self.project.wiki_links.all()
|
||||||
self.href = slugify_uniquely_for_queryset(self.title, wl_qs, slugfield="href")
|
self.href = slugify_uniquely_for_queryset(self.title, wl_qs, slugfield="href")
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
else:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
|
@ -35,6 +35,7 @@ from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from django_pgjson.fields import JsonField
|
from django_pgjson.fields import JsonField
|
||||||
|
from django_pglocks import advisory_lock
|
||||||
|
|
||||||
from taiga.auth.tokens import get_token_for_user
|
from taiga.auth.tokens import get_token_for_user
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
|
@ -265,6 +266,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
|
with advisory_lock("delete-user"):
|
||||||
self.username = slugify_uniquely("deleted-user", User, slugfield="username")
|
self.username = slugify_uniquely("deleted-user", User, slugfield="username")
|
||||||
self.email = "{}@taiga.io".format(self.username)
|
self.email = "{}@taiga.io".format(self.username)
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
|
|
Loading…
Reference in New Issue