Merge branch 'master' into stable
commit
3aed6371ff
|
@ -20,12 +20,18 @@ answer newbie questions, and generally made taiga that much better:
|
||||||
- Andrea Stagi <stagi.andrea@gmail.com>
|
- Andrea Stagi <stagi.andrea@gmail.com>
|
||||||
- Andrés Moya <andres.moya@kaleidos.net>
|
- Andrés Moya <andres.moya@kaleidos.net>
|
||||||
- Andrey Alekseenko <al42and@gmail.com>
|
- Andrey Alekseenko <al42and@gmail.com>
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
- Brett Profitt <brett.profitt@gmail.com>
|
||||||
|
>>>>>>> master
|
||||||
- Bruno Clermont <bruno@robotinfra.com>
|
- Bruno Clermont <bruno@robotinfra.com>
|
||||||
- Chris Wilson <chris.wilson@aridhia.com>
|
- Chris Wilson <chris.wilson@aridhia.com>
|
||||||
- David Burke <david@burkesoftware.com>
|
- David Burke <david@burkesoftware.com>
|
||||||
- Hector Colina <hcolina@gmail.com>
|
- Hector Colina <hcolina@gmail.com>
|
||||||
- Joe Letts
|
- Joe Letts
|
||||||
- Julien Palard
|
- Julien Palard
|
||||||
|
- luyikei <luyikei.qmltu@gmail.com>
|
||||||
|
- Motius GmbH <mail@motius.de>
|
||||||
- Ricky Posner <e@eposner.com>
|
- Ricky Posner <e@eposner.com>
|
||||||
- Yamila Moreno <yamila.moreno@kaleidos.net>
|
- Yamila Moreno <yamila.moreno@kaleidos.net>
|
||||||
- Brett Profitt <brett.profitt@gmail.com>
|
- Yaser Alraddadi <yaser@yr.sa>
|
||||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,6 +1,19 @@
|
||||||
# Changelog #
|
# Changelog #
|
||||||
|
|
||||||
|
|
||||||
|
## 2.0.0 Pulsatilla Patens (2016-04-04)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Ability to create url custom fields. (thanks to [@astagi](https://github.com/astagi)).
|
||||||
|
- Blocked projects support
|
||||||
|
- Transfer projects ownership support
|
||||||
|
- Customizable max private and public projects per user
|
||||||
|
- Customizable max of memberships per owned private and public projects
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
- Lots of small and not so small bugfixes.
|
||||||
|
|
||||||
|
|
||||||
## 1.10.0 Dryas Octopetala (2016-01-30)
|
## 1.10.0 Dryas Octopetala (2016-01-30)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
factory_boy==2.6.0
|
factory_boy==2.6.1
|
||||||
py==1.4.31
|
py==1.4.31
|
||||||
pytest==2.8.5
|
pytest==2.8.7
|
||||||
pytest-django==2.9.1
|
pytest-django==2.9.1
|
||||||
pytest-pythonpath==0.7
|
pytest-pythonpath==0.7
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,36 @@
|
||||||
Django==1.8.6
|
Django==1.9.2
|
||||||
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
|
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
|
||||||
django-picklefield==0.3.2
|
django-picklefield==0.3.2
|
||||||
django-sampledatahelper==0.3.0
|
django-sampledatahelper==0.4.0
|
||||||
gunicorn==19.3.0
|
gunicorn==19.4.5
|
||||||
psycopg2==2.6.1
|
psycopg2==2.6.1
|
||||||
Pillow==2.9.0
|
Pillow==3.1.1
|
||||||
pytz==2015.7
|
pytz==2015.7
|
||||||
six==1.10.0
|
six==1.10.0
|
||||||
amqp==1.4.7
|
amqp==1.4.9
|
||||||
djmail==0.11
|
djmail==0.12.0.post1
|
||||||
django-pgjson==0.3.1
|
django-pgjson==0.3.1
|
||||||
djorm-pgarray==1.2
|
djorm-pgarray==1.2
|
||||||
django-jinja==2.1.1
|
django-jinja==2.1.2
|
||||||
jinja2==2.8
|
jinja2==2.8
|
||||||
pygments==2.0.2
|
pygments==2.0.2
|
||||||
django-sites==0.8
|
django-sites==0.9
|
||||||
Markdown==2.6.5
|
Markdown==2.6.5
|
||||||
fn==0.4.3
|
fn==0.4.3
|
||||||
diff-match-patch==20121119
|
diff-match-patch==20121119
|
||||||
requests==2.8.1
|
requests==2.9.1
|
||||||
django-sr==0.0.4
|
django-sr==0.0.4
|
||||||
easy-thumbnails==2.2.1
|
easy-thumbnails==2.3
|
||||||
celery==3.1.19
|
celery==3.1.20
|
||||||
redis==2.10.5
|
redis==2.10.5
|
||||||
Unidecode==0.04.18
|
Unidecode==0.04.19
|
||||||
raven==5.9.2
|
raven==5.10.2
|
||||||
bleach==1.4.2
|
bleach==1.4.2
|
||||||
django-ipware==1.1.2
|
django-ipware==1.1.3
|
||||||
premailer==2.9.6
|
premailer==2.9.7
|
||||||
cssutils==1.0.1 # Compatible with python 3.5
|
cssutils==1.0.1 # Compatible with python 3.5
|
||||||
django-transactional-cleanup==0.1.15
|
|
||||||
lxml==3.5.0
|
lxml==3.5.0
|
||||||
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
|
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
|
||||||
pyjwkest==1.0.9
|
pyjwkest==1.1.5
|
||||||
python-dateutil==2.4.2
|
python-dateutil==2.4.2
|
||||||
netaddr==0.7.18
|
netaddr==0.7.18
|
||||||
|
|
|
@ -30,7 +30,7 @@ DEBUG = False
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "transaction_hooks.backends.postgresql_psycopg2",
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"NAME": "taiga",
|
"NAME": "taiga",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -320,7 +320,6 @@ INSTALLED_APPS = [
|
||||||
"sr",
|
"sr",
|
||||||
"easy_thumbnails",
|
"easy_thumbnails",
|
||||||
"raven.contrib.django.raven_compat",
|
"raven.contrib.django.raven_compat",
|
||||||
"django_transactional_cleanup",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "taiga.wsgi.application"
|
WSGI_APPLICATION = "taiga.wsgi.application"
|
||||||
|
@ -347,7 +346,7 @@ LOGGING = {
|
||||||
"handlers": {
|
"handlers": {
|
||||||
"null": {
|
"null": {
|
||||||
"level":"DEBUG",
|
"level":"DEBUG",
|
||||||
"class":"django.utils.log.NullHandler",
|
"class":"logging.NullHandler",
|
||||||
},
|
},
|
||||||
"console":{
|
"console":{
|
||||||
"level":"DEBUG",
|
"level":"DEBUG",
|
||||||
|
@ -434,7 +433,9 @@ REST_FRAMEWORK = {
|
||||||
# Extra expose header related to Taiga APP (see taiga.base.middleware.cors=)
|
# Extra expose header related to Taiga APP (see taiga.base.middleware.cors=)
|
||||||
APP_EXTRA_EXPOSE_HEADERS = [
|
APP_EXTRA_EXPOSE_HEADERS = [
|
||||||
"taiga-info-total-opened-milestones",
|
"taiga-info-total-opened-milestones",
|
||||||
"taiga-info-total-closed-milestones"
|
"taiga-info-total-closed-milestones",
|
||||||
|
"taiga-info-project-memberships",
|
||||||
|
"taiga-info-project-is-private"
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_PROJECT_TEMPLATE = "scrum"
|
DEFAULT_PROJECT_TEMPLATE = "scrum"
|
||||||
|
@ -522,6 +523,12 @@ WEBHOOKS_ENABLED = False
|
||||||
FRONT_SITEMAP_ENABLED = False
|
FRONT_SITEMAP_ENABLED = False
|
||||||
FRONT_SITEMAP_CACHE_TIMEOUT = 24*60*60 # In second
|
FRONT_SITEMAP_CACHE_TIMEOUT = 24*60*60 # In second
|
||||||
|
|
||||||
|
EXTRA_BLOCKING_CODES = []
|
||||||
|
|
||||||
|
MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit
|
||||||
|
MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit
|
||||||
|
MAX_MEMBERSHIPS_PRIVATE_PROJECTS = None # None == no limit
|
||||||
|
MAX_MEMBERSHIPS_PUBLIC_PROJECTS = None # None == no limit
|
||||||
|
|
||||||
from .sr import *
|
from .sr import *
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Affero General Public License as
|
# it under the terms of the GNU Affero General Public License as
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
@ -24,7 +25,7 @@ from .development import *
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': 'taiga',
|
'NAME': 'taiga',
|
||||||
'USER': 'taiga',
|
'USER': 'taiga',
|
||||||
'PASSWORD': 'changeme',
|
'PASSWORD': 'changeme',
|
||||||
|
|
|
@ -25,6 +25,7 @@ not uses clasess and uses simple functions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction as tx
|
from django.db import transaction as tx
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
@ -69,7 +70,7 @@ def is_user_already_registered(*, username:str, email:str) -> (bool, str):
|
||||||
and in case he does whats the duplicated attribute
|
and in case he does whats the duplicated attribute
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user_model = apps.get_model("users", "User")
|
user_model = get_user_model()
|
||||||
if user_model.objects.filter(username=username):
|
if user_model.objects.filter(username=username):
|
||||||
return (True, _("Username is already in use."))
|
return (True, _("Username is already in use."))
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ def public_register(username:str, password:str, email:str, full_name:str):
|
||||||
if is_registered:
|
if is_registered:
|
||||||
raise exc.WrongArguments(reason)
|
raise exc.WrongArguments(reason)
|
||||||
|
|
||||||
user_model = apps.get_model("users", "User")
|
user_model = get_user_model()
|
||||||
user = user_model(username=username,
|
user = user_model(username=username,
|
||||||
email=email,
|
email=email,
|
||||||
full_name=full_name)
|
full_name=full_name)
|
||||||
|
@ -159,7 +160,7 @@ def private_register_for_new_user(token:str, username:str, email:str,
|
||||||
if is_registered:
|
if is_registered:
|
||||||
raise exc.WrongArguments(reason)
|
raise exc.WrongArguments(reason)
|
||||||
|
|
||||||
user_model = apps.get_model("users", "User")
|
user_model = get_user_model()
|
||||||
user = user_model(username=username,
|
user = user_model(username=username,
|
||||||
email=email,
|
email=email,
|
||||||
full_name=full_name)
|
full_name=full_name)
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
#
|
#
|
||||||
# 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.contrib.auth import get_user_model
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -47,7 +47,7 @@ def get_user_for_token(token, scope, max_age=None):
|
||||||
except signing.BadSignature:
|
except signing.BadSignature:
|
||||||
raise exc.NotAuthenticated(_("Invalid token"))
|
raise exc.NotAuthenticated(_("Invalid token"))
|
||||||
|
|
||||||
model_cls = apps.get_model("users", "User")
|
model_cls = get_user_model()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = model_cls.objects.get(pk=data["user_%s_id" % (scope)])
|
user = model_cls.objects.get(pk=data["user_%s_id" % (scope)])
|
||||||
|
|
|
@ -64,17 +64,17 @@ from django.utils.encoding import is_protected_type
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import ugettext
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.datastructures import SortedDict
|
|
||||||
|
|
||||||
from . import ISO_8601
|
from . import ISO_8601
|
||||||
from .settings import api_settings
|
from .settings import api_settings
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from decimal import Decimal, DecimalException
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from decimal import Decimal, DecimalException
|
|
||||||
|
|
||||||
|
|
||||||
def is_non_str_iterable(obj):
|
def is_non_str_iterable(obj):
|
||||||
|
@ -255,7 +255,7 @@ class Field(object):
|
||||||
return [self.to_native(item) for item in value]
|
return [self.to_native(item) for item in value]
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
# Make sure we preserve field ordering, if it exists
|
# Make sure we preserve field ordering, if it exists
|
||||||
ret = SortedDict()
|
ret = OrderedDict()
|
||||||
for key, val in value.items():
|
for key, val in value.items():
|
||||||
ret[key] = self.to_native(val)
|
ret[key] = self.to_native(val)
|
||||||
return ret
|
return ret
|
||||||
|
@ -270,7 +270,7 @@ class Field(object):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
metadata = SortedDict()
|
metadata = OrderedDict()
|
||||||
metadata["type"] = self.type_label
|
metadata["type"] = self.type_label
|
||||||
metadata["required"] = getattr(self, "required", False)
|
metadata["required"] = getattr(self, "required", False)
|
||||||
optional_attrs = ["read_only", "label", "help_text",
|
optional_attrs = ["read_only", "label", "help_text",
|
||||||
|
|
|
@ -53,9 +53,9 @@ from taiga.base import response
|
||||||
from .settings import api_settings
|
from .settings import api_settings
|
||||||
from .utils import get_object_or_404
|
from .utils import get_object_or_404
|
||||||
|
|
||||||
|
from .. import exceptions as exc
|
||||||
from ..decorators import model_pk_lock
|
from ..decorators import model_pk_lock
|
||||||
|
|
||||||
|
|
||||||
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
|
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
|
||||||
"""
|
"""
|
||||||
Given a model instance, and an optional pk and slug field,
|
Given a model instance, and an optional pk and slug field,
|
||||||
|
@ -243,3 +243,32 @@ class DestroyModelMixin:
|
||||||
obj.delete()
|
obj.delete()
|
||||||
self.post_delete(obj)
|
self.post_delete(obj)
|
||||||
return response.NoContent()
|
return response.NoContent()
|
||||||
|
|
||||||
|
|
||||||
|
class BlockeableModelMixin:
|
||||||
|
def is_blocked(self, obj):
|
||||||
|
raise NotImplementedError("is_blocked must be overridden")
|
||||||
|
|
||||||
|
def pre_conditions_blocked(self, obj):
|
||||||
|
#Raises permission exception
|
||||||
|
if obj is not None and self.is_blocked(obj):
|
||||||
|
raise exc.Blocked(_("Blocked element"))
|
||||||
|
|
||||||
|
|
||||||
|
class BlockeableSaveMixin(BlockeableModelMixin):
|
||||||
|
def pre_conditions_on_save(self, obj):
|
||||||
|
# Called on create and update calls
|
||||||
|
self.pre_conditions_blocked(obj)
|
||||||
|
super().pre_conditions_on_save(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class BlockeableDeleteMixin():
|
||||||
|
def pre_conditions_on_delete(self, obj):
|
||||||
|
# Called on destroy call
|
||||||
|
self.pre_conditions_blocked(obj)
|
||||||
|
super().pre_conditions_on_delete(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class BlockedByProjectMixin(BlockeableSaveMixin, BlockeableDeleteMixin):
|
||||||
|
def is_blocked(self, obj):
|
||||||
|
return obj.project is not None and obj.project.blocked_code is not None
|
||||||
|
|
|
@ -20,7 +20,7 @@ import abc
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
from taiga.base.utils import sequence as sq
|
from taiga.base.utils import sequence as sq
|
||||||
from taiga.permissions.service import user_has_perm, is_project_owner
|
from taiga.permissions.service import user_has_perm, is_project_admin
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
@ -206,9 +206,9 @@ class HasMandatoryParam(PermissionComponent):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class IsProjectOwner(PermissionComponent):
|
class IsProjectAdmin(PermissionComponent):
|
||||||
def check_permissions(self, request, view, obj=None):
|
def check_permissions(self, request, view, obj=None):
|
||||||
return is_project_owner(request.user, obj)
|
return is_project_admin(request.user, obj)
|
||||||
|
|
||||||
|
|
||||||
class IsObjectOwner(PermissionComponent):
|
class IsObjectOwner(PermissionComponent):
|
||||||
|
|
|
@ -59,11 +59,11 @@ from django.core.paginator import Page
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.datastructures import SortedDict
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from .settings import api_settings
|
from .settings import api_settings
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -148,7 +148,7 @@ class DictWithMetadata(dict):
|
||||||
return dict(self)
|
return dict(self)
|
||||||
|
|
||||||
|
|
||||||
class SortedDictWithMetadata(SortedDict):
|
class OrderedDictWithMetadata(OrderedDict):
|
||||||
"""
|
"""
|
||||||
A sorted dict-like object, that can have additional properties attached.
|
A sorted dict-like object, that can have additional properties attached.
|
||||||
"""
|
"""
|
||||||
|
@ -158,7 +158,7 @@ class SortedDictWithMetadata(SortedDict):
|
||||||
Overriden to remove the metadata from the dict, since it shouldn't be
|
Overriden to remove the metadata from the dict, since it shouldn't be
|
||||||
pickle and may in some instances be unpickleable.
|
pickle and may in some instances be unpickleable.
|
||||||
"""
|
"""
|
||||||
return SortedDict(self).__dict__
|
return OrderedDict(self).__dict__
|
||||||
|
|
||||||
|
|
||||||
def _is_protected_type(obj):
|
def _is_protected_type(obj):
|
||||||
|
@ -194,7 +194,7 @@ def _get_declared_fields(bases, attrs):
|
||||||
if hasattr(base, "base_fields"):
|
if hasattr(base, "base_fields"):
|
||||||
fields = list(base.base_fields.items()) + fields
|
fields = list(base.base_fields.items()) + fields
|
||||||
|
|
||||||
return SortedDict(fields)
|
return OrderedDict(fields)
|
||||||
|
|
||||||
|
|
||||||
class SerializerMetaclass(type):
|
class SerializerMetaclass(type):
|
||||||
|
@ -222,7 +222,7 @@ class BaseSerializer(WritableField):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
_options_class = SerializerOptions
|
_options_class = SerializerOptions
|
||||||
_dict_class = SortedDictWithMetadata
|
_dict_class = OrderedDictWithMetadata
|
||||||
|
|
||||||
def __init__(self, instance=None, data=None, files=None,
|
def __init__(self, instance=None, data=None, files=None,
|
||||||
context=None, partial=False, many=None,
|
context=None, partial=False, many=None,
|
||||||
|
@ -268,7 +268,7 @@ class BaseSerializer(WritableField):
|
||||||
This will be the set of any explicitly declared fields,
|
This will be the set of any explicitly declared fields,
|
||||||
plus the set of fields returned by get_default_fields().
|
plus the set of fields returned by get_default_fields().
|
||||||
"""
|
"""
|
||||||
ret = SortedDict()
|
ret = OrderedDict()
|
||||||
|
|
||||||
# Get the explicitly declared fields
|
# Get the explicitly declared fields
|
||||||
base_fields = copy.deepcopy(self.base_fields)
|
base_fields = copy.deepcopy(self.base_fields)
|
||||||
|
@ -284,7 +284,7 @@ class BaseSerializer(WritableField):
|
||||||
# If "fields" is specified, use those fields, in that order.
|
# If "fields" is specified, use those fields, in that order.
|
||||||
if self.opts.fields:
|
if self.opts.fields:
|
||||||
assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple"
|
assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple"
|
||||||
new = SortedDict()
|
new = OrderedDict()
|
||||||
for key in self.opts.fields:
|
for key in self.opts.fields:
|
||||||
new[key] = ret[key]
|
new[key] = ret[key]
|
||||||
ret = new
|
ret = new
|
||||||
|
@ -458,7 +458,10 @@ class BaseSerializer(WritableField):
|
||||||
many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type))
|
many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type))
|
||||||
|
|
||||||
if many:
|
if many:
|
||||||
return [self.to_native(item) for item in value]
|
try:
|
||||||
|
return [self.to_native(item) for item in value]
|
||||||
|
except TypeError:
|
||||||
|
pass # LazyObject is iterable so we need to catch this
|
||||||
return self.to_native(value)
|
return self.to_native(value)
|
||||||
|
|
||||||
def field_from_native(self, data, files, field_name, into):
|
def field_from_native(self, data, files, field_name, into):
|
||||||
|
@ -610,7 +613,10 @@ class BaseSerializer(WritableField):
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
if many:
|
if many:
|
||||||
self._data = [self.to_native(item) for item in obj]
|
try:
|
||||||
|
self._data = [self.to_native(item) for item in obj]
|
||||||
|
except TypeError:
|
||||||
|
self._data = self.to_native(obj) # LazyObject is iterable so we need to catch this
|
||||||
else:
|
else:
|
||||||
self._data = self.to_native(obj)
|
self._data = self.to_native(obj)
|
||||||
|
|
||||||
|
@ -645,7 +651,7 @@ class BaseSerializer(WritableField):
|
||||||
Useful for things like responding to OPTIONS requests, or generating
|
Useful for things like responding to OPTIONS requests, or generating
|
||||||
API schemas for auto-documentation.
|
API schemas for auto-documentation.
|
||||||
"""
|
"""
|
||||||
return SortedDict(
|
return OrderedDict(
|
||||||
[(field_name, field.metadata())
|
[(field_name, field.metadata())
|
||||||
for field_name, field in six.iteritems(self.fields)]
|
for field_name, field in six.iteritems(self.fields)]
|
||||||
)
|
)
|
||||||
|
@ -740,7 +746,7 @@ class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer)))
|
||||||
assert cls is not None, \
|
assert cls is not None, \
|
||||||
"Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__
|
"Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__
|
||||||
opts = cls._meta.concrete_model._meta
|
opts = cls._meta.concrete_model._meta
|
||||||
ret = SortedDict()
|
ret = OrderedDict()
|
||||||
nested = bool(self.opts.depth)
|
nested = bool(self.opts.depth)
|
||||||
|
|
||||||
# Deal with adding the primary key field
|
# Deal with adding the primary key field
|
||||||
|
|
|
@ -62,9 +62,10 @@ back to the defaults.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import importlib
|
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
from . import ISO_8601
|
from . import ISO_8601
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
{% load url from future %}
|
|
||||||
{% load api %}
|
{% load api %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
{% load url from future %}
|
|
||||||
{% load api %}
|
{% load api %}
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
|
|
|
@ -45,13 +45,10 @@
|
||||||
Helper classes for parsers.
|
Helper classes for parsers.
|
||||||
"""
|
"""
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.utils.datastructures import SortedDict
|
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
from taiga.base.api.serializers import DictWithMetadata, SortedDictWithMetadata
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import types
|
import types
|
||||||
|
|
|
@ -43,6 +43,8 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
|
@ -50,7 +52,6 @@ from django.http.response import HttpResponseBase
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.defaults import server_error
|
from django.views.defaults import server_error
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.utils.datastructures import SortedDict
|
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
@ -462,7 +463,7 @@ class APIView(View):
|
||||||
# By default we can't provide any form-like information, however the
|
# By default we can't provide any form-like information, however the
|
||||||
# generic views override this implementation and add additional
|
# generic views override this implementation and add additional
|
||||||
# information for POST and PUT methods, based on the serializer.
|
# information for POST and PUT methods, based on the serializer.
|
||||||
ret = SortedDict()
|
ret = OrderedDict()
|
||||||
ret['name'] = self.get_view_name()
|
ret['name'] = self.get_view_name()
|
||||||
ret['description'] = self.get_view_description()
|
ret['description'] = self.get_view_description()
|
||||||
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
||||||
|
|
|
@ -187,11 +187,13 @@ class ModelListViewSet(mixins.RetrieveModelMixin,
|
||||||
GenericViewSet):
|
GenericViewSet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ModelUpdateRetrieveViewSet(mixins.UpdateModelMixin,
|
class ModelUpdateRetrieveViewSet(mixins.UpdateModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
GenericViewSet):
|
GenericViewSet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ModelRetrieveViewSet(mixins.RetrieveModelMixin,
|
class ModelRetrieveViewSet(mixins.RetrieveModelMixin,
|
||||||
GenericViewSet):
|
GenericViewSet):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -17,12 +17,14 @@
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
from .signals.thumbnails import connect_thumbnail_signals
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAppConfig(AppConfig):
|
class BaseAppConfig(AppConfig):
|
||||||
name = "taiga.base"
|
name = "taiga.base"
|
||||||
verbose_name = "Base App Config"
|
verbose_name = "Base App Config"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
from .signals.thumbnails import connect_thumbnail_signals
|
||||||
|
from .signals.cleanup_files import connect_cleanup_files_signals
|
||||||
|
|
||||||
connect_thumbnail_signals()
|
connect_thumbnail_signals()
|
||||||
|
connect_cleanup_files_signals()
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
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.
|
||||||
|
|
|
@ -201,6 +201,28 @@ class NotAuthenticated(NotAuthenticated):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Blocked(APIException):
|
||||||
|
"""
|
||||||
|
Exception used on blocked projects
|
||||||
|
"""
|
||||||
|
status_code = status.HTTP_451_BLOCKED
|
||||||
|
default_detail = _("Blocked element")
|
||||||
|
|
||||||
|
|
||||||
|
class NotEnoughSlotsForProject(BaseException):
|
||||||
|
"""
|
||||||
|
Exception used on import/edition/creation project errors where the user
|
||||||
|
hasn't slots enough
|
||||||
|
"""
|
||||||
|
default_detail = _("No room left for more projects.")
|
||||||
|
|
||||||
|
def __init__(self, is_private, total_memberships, detail=None):
|
||||||
|
self.detail = detail or self.default_detail
|
||||||
|
self.project_data = {
|
||||||
|
"is_private": is_private,
|
||||||
|
"total_memberships": total_memberships
|
||||||
|
}
|
||||||
|
|
||||||
def format_exception(exc):
|
def format_exception(exc):
|
||||||
if isinstance(exc.detail, (dict, list, tuple,)):
|
if isinstance(exc.detail, (dict, list, tuple,)):
|
||||||
detail = exc.detail
|
detail = exc.detail
|
||||||
|
@ -232,6 +254,9 @@ def exception_handler(exc):
|
||||||
headers["WWW-Authenticate"] = exc.auth_header
|
headers["WWW-Authenticate"] = exc.auth_header
|
||||||
if getattr(exc, "wait", None):
|
if getattr(exc, "wait", None):
|
||||||
headers["X-Throttle-Wait-Seconds"] = "%d" % exc.wait
|
headers["X-Throttle-Wait-Seconds"] = "%d" % exc.wait
|
||||||
|
if getattr(exc, "project_data", None):
|
||||||
|
headers["Taiga-Info-Project-Memberships"] = exc.project_data["total_memberships"]
|
||||||
|
headers["Taiga-Info-Project-Is-Private"] = exc.project_data["is_private"]
|
||||||
|
|
||||||
detail = format_exception(exc)
|
detail = format_exception(exc)
|
||||||
return response.Response(detail, status=exc.status_code, headers=headers)
|
return response.Response(detail, status=exc.status_code, headers=headers)
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#
|
#
|
||||||
# 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 logging
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -141,7 +142,7 @@ class PermissionBasedFilterBackend(FilterBackend):
|
||||||
if project_id:
|
if project_id:
|
||||||
memberships_qs = memberships_qs.filter(project_id=project_id)
|
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||||
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) |
|
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) |
|
||||||
Q(is_owner=True))
|
Q(is_admin=True))
|
||||||
|
|
||||||
projects_list = [membership.project_id for membership in memberships_qs]
|
projects_list = [membership.project_id for membership in memberships_qs]
|
||||||
|
|
||||||
|
@ -242,7 +243,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend):
|
||||||
if project_id:
|
if project_id:
|
||||||
memberships_qs = memberships_qs.filter(project_id=project_id)
|
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||||
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) |
|
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) |
|
||||||
Q(is_owner=True))
|
Q(is_admin=True))
|
||||||
|
|
||||||
projects_list = [membership.project_id for membership in memberships_qs]
|
projects_list = [membership.project_id for membership in memberships_qs]
|
||||||
|
|
||||||
|
@ -286,7 +287,7 @@ class BaseIsProjectAdminFilterBackend(object):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
membership_model = apps.get_model('projects', 'Membership')
|
membership_model = apps.get_model('projects', 'Membership')
|
||||||
memberships_qs = membership_model.objects.filter(user=request.user, is_owner=True)
|
memberships_qs = membership_model.objects.filter(user=request.user, is_admin=True)
|
||||||
if project_id:
|
if project_id:
|
||||||
memberships_qs = memberships_qs.filter(project_id=project_id)
|
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,8 @@ import datetime
|
||||||
|
|
||||||
from optparse import make_option
|
from optparse import make_option
|
||||||
|
|
||||||
from django.db.models.loading import get_model
|
from django.apps import apps
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -28,7 +29,6 @@ from taiga.base.mails import mail_builder
|
||||||
from taiga.projects.models import Project, Membership
|
from taiga.projects.models import Project, Membership
|
||||||
from taiga.projects.history.models import HistoryEntry
|
from taiga.projects.history.models import HistoryEntry
|
||||||
from taiga.projects.history.services import get_history_queryset_by_model_instance
|
from taiga.projects.history.services import get_history_queryset_by_model_instance
|
||||||
from taiga.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -50,7 +50,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
# Register email
|
# Register email
|
||||||
context = {"lang": locale,
|
context = {"lang": locale,
|
||||||
"user": User.objects.all().order_by("?").first(),
|
"user": get_user_model().objects.all().order_by("?").first(),
|
||||||
"cancel_token": "cancel-token"}
|
"cancel_token": "cancel-token"}
|
||||||
|
|
||||||
email = mail_builder.registered_user(test_email, context)
|
email = mail_builder.registered_user(test_email, context)
|
||||||
|
@ -58,7 +58,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
# Membership invitation
|
# Membership invitation
|
||||||
membership = Membership.objects.order_by("?").filter(user__isnull=True).first()
|
membership = Membership.objects.order_by("?").filter(user__isnull=True).first()
|
||||||
membership.invited_by = User.objects.all().order_by("?").first()
|
membership.invited_by = get_user_model().objects.all().order_by("?").first()
|
||||||
membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example"
|
membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example"
|
||||||
|
|
||||||
context = {"lang": locale, "membership": membership}
|
context = {"lang": locale, "membership": membership}
|
||||||
|
@ -88,19 +88,19 @@ class Command(BaseCommand):
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Password recovery
|
# Password recovery
|
||||||
context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
|
context = {"lang": locale, "user": get_user_model().objects.all().order_by("?").first()}
|
||||||
email = mail_builder.password_recovery(test_email, context)
|
email = mail_builder.password_recovery(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Change email
|
# Change email
|
||||||
context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
|
context = {"lang": locale, "user": get_user_model().objects.all().order_by("?").first()}
|
||||||
email = mail_builder.change_email(test_email, context)
|
email = mail_builder.change_email(test_email, context)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Export/Import emails
|
# Export/Import emails
|
||||||
context = {
|
context = {
|
||||||
"lang": locale,
|
"lang": locale,
|
||||||
"user": User.objects.all().order_by("?").first(),
|
"user": get_user_model().objects.all().order_by("?").first(),
|
||||||
"project": Project.objects.all().order_by("?").first(),
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
"error_subject": "Error generating project dump",
|
"error_subject": "Error generating project dump",
|
||||||
"error_message": "Error generating project dump",
|
"error_message": "Error generating project dump",
|
||||||
|
@ -109,7 +109,7 @@ class Command(BaseCommand):
|
||||||
email.send()
|
email.send()
|
||||||
context = {
|
context = {
|
||||||
"lang": locale,
|
"lang": locale,
|
||||||
"user": User.objects.all().order_by("?").first(),
|
"user": get_user_model().objects.all().order_by("?").first(),
|
||||||
"error_subject": "Error importing project dump",
|
"error_subject": "Error importing project dump",
|
||||||
"error_message": "Error importing project dump",
|
"error_message": "Error importing project dump",
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ class Command(BaseCommand):
|
||||||
context = {
|
context = {
|
||||||
"lang": locale,
|
"lang": locale,
|
||||||
"url": "http://dummyurl.com",
|
"url": "http://dummyurl.com",
|
||||||
"user": User.objects.all().order_by("?").first(),
|
"user": get_user_model().objects.all().order_by("?").first(),
|
||||||
"project": Project.objects.all().order_by("?").first(),
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
"deletion_date": deletion_date,
|
"deletion_date": deletion_date,
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"lang": locale,
|
"lang": locale,
|
||||||
"user": User.objects.all().order_by("?").first(),
|
"user": get_user_model().objects.all().order_by("?").first(),
|
||||||
"project": Project.objects.all().order_by("?").first(),
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
}
|
}
|
||||||
email = mail_builder.load_dump(test_email, context)
|
email = mail_builder.load_dump(test_email, context)
|
||||||
|
@ -157,13 +157,13 @@ class Command(BaseCommand):
|
||||||
context = {
|
context = {
|
||||||
"lang": locale,
|
"lang": locale,
|
||||||
"project": Project.objects.all().order_by("?").first(),
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
"changer": User.objects.all().order_by("?").first(),
|
"changer": get_user_model().objects.all().order_by("?").first(),
|
||||||
"history_entries": HistoryEntry.objects.all().order_by("?")[0:5],
|
"history_entries": HistoryEntry.objects.all().order_by("?")[0:5],
|
||||||
"user": User.objects.all().order_by("?").first(),
|
"user": get_user_model().objects.all().order_by("?").first(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for notification_email in notification_emails:
|
for notification_email in notification_emails:
|
||||||
model = get_model(*notification_email[0].split("."))
|
model = apps.get_model(*notification_email[0].split("."))
|
||||||
snapshot = {
|
snapshot = {
|
||||||
"subject": "Tests subject",
|
"subject": "Tests subject",
|
||||||
"ref": 123123,
|
"ref": 123123,
|
||||||
|
@ -187,3 +187,38 @@ class Command(BaseCommand):
|
||||||
cls = type("InlineCSSTemplateMail", (InlineCSSTemplateMail,), {"name": notification_email[1]})
|
cls = type("InlineCSSTemplateMail", (InlineCSSTemplateMail,), {"name": notification_email[1]})
|
||||||
email = cls()
|
email = cls()
|
||||||
email.send(test_email, context)
|
email.send(test_email, context)
|
||||||
|
|
||||||
|
|
||||||
|
# Transfer Emails
|
||||||
|
context = {
|
||||||
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
|
"requester": User.objects.all().order_by("?").first(),
|
||||||
|
}
|
||||||
|
email = mail_builder.transfer_request(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
|
"receiver": User.objects.all().order_by("?").first(),
|
||||||
|
"token": "test-token",
|
||||||
|
"reason": "Test reason"
|
||||||
|
}
|
||||||
|
email = mail_builder.transfer_start(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
|
"old_owner": User.objects.all().order_by("?").first(),
|
||||||
|
"new_owner": User.objects.all().order_by("?").first(),
|
||||||
|
"reason": "Test reason"
|
||||||
|
}
|
||||||
|
email = mail_builder.transfer_accept(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project": Project.objects.all().order_by("?").first(),
|
||||||
|
"rejecter": User.objects.all().order_by("?").first(),
|
||||||
|
"reason": "Test reason"
|
||||||
|
}
|
||||||
|
email = mail_builder.transfer_reject(test_email, context)
|
||||||
|
email.send()
|
||||||
|
|
|
@ -43,9 +43,10 @@
|
||||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
"""The various HTTP responses for use in returning proper HTTP codes."""
|
"""The various HTTP responses for use in returning proper HTTP codes."""
|
||||||
|
from http.client import responses
|
||||||
|
|
||||||
from django import http
|
from django import http
|
||||||
|
|
||||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
|
||||||
from django.template.response import SimpleTemplateResponse
|
from django.template.response import SimpleTemplateResponse
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
|
@ -114,7 +115,7 @@ class Response(SimpleTemplateResponse):
|
||||||
"""
|
"""
|
||||||
# TODO: Deprecate and use a template tag instead
|
# TODO: Deprecate and use a template tag instead
|
||||||
# TODO: Status code text for RFC 6585 status codes
|
# TODO: Status code text for RFC 6585 status codes
|
||||||
return STATUS_CODE_TEXT.get(self.status_code, '')
|
return responses.get(self.status_code, '')
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.db import models, connection
|
||||||
|
from django.db.utils import DEFAULT_DB_ALIAS, ConnectionHandler
|
||||||
|
from django.db.models.signals import pre_save, post_delete
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
from django.dispatch import Signal
|
||||||
|
|
||||||
|
cleanup_pre_delete = Signal(providing_args=["file"])
|
||||||
|
cleanup_post_delete = Signal(providing_args=["file"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _find_models_with_filefield():
|
||||||
|
result = []
|
||||||
|
for model in apps.get_models():
|
||||||
|
for field in model._meta.fields:
|
||||||
|
if isinstance(field, models.FileField):
|
||||||
|
result.append(model)
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_file(file_obj):
|
||||||
|
def delete_from_storage():
|
||||||
|
try:
|
||||||
|
cleanup_pre_delete.send(sender=None, file=file_obj)
|
||||||
|
storage.delete(file_obj.name)
|
||||||
|
cleanup_post_delete.send(sender=None, file=file_obj)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Unexpected exception while attempting "
|
||||||
|
"to delete old file '%s'".format(file_obj.name))
|
||||||
|
|
||||||
|
storage = file_obj.storage
|
||||||
|
if storage and storage.exists(file_obj.name):
|
||||||
|
connection.on_commit(delete_from_storage)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_file_fields(instance):
|
||||||
|
return filter(
|
||||||
|
lambda field: isinstance(field, models.FileField),
|
||||||
|
instance._meta.fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_files_on_change(sender, instance, **kwargs):
|
||||||
|
if not instance.pk:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_instance = sender.objects.get(pk=instance.pk)
|
||||||
|
except instance.DoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
for field in _get_file_fields(instance):
|
||||||
|
old_file = getattr(old_instance, field.name)
|
||||||
|
new_file = getattr(instance, field.name)
|
||||||
|
|
||||||
|
if old_file and old_file != new_file:
|
||||||
|
_delete_file(old_file)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_files_on_delete(sender, instance, **kwargs):
|
||||||
|
for field in _get_file_fields(instance):
|
||||||
|
file_to_delete = getattr(instance, field.name)
|
||||||
|
|
||||||
|
if file_to_delete:
|
||||||
|
_delete_file(file_to_delete)
|
||||||
|
|
||||||
|
|
||||||
|
def connect_cleanup_files_signals():
|
||||||
|
connections = ConnectionHandler()
|
||||||
|
backend = connections[DEFAULT_DB_ALIAS]
|
||||||
|
|
||||||
|
for model in _find_models_with_filefield():
|
||||||
|
pre_save.connect(remove_files_on_change, sender=model)
|
||||||
|
post_delete.connect(remove_files_on_delete, sender=model)
|
|
@ -15,7 +15,7 @@
|
||||||
# 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_transactional_cleanup.signals import cleanup_post_delete
|
from .cleanup_files import cleanup_post_delete
|
||||||
from easy_thumbnails.files import get_thumbnailer
|
from easy_thumbnails.files import get_thumbnailer
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ HTTP_417_EXPECTATION_FAILED = 417
|
||||||
HTTP_428_PRECONDITION_REQUIRED = 428
|
HTTP_428_PRECONDITION_REQUIRED = 428
|
||||||
HTTP_429_TOO_MANY_REQUESTS = 429
|
HTTP_429_TOO_MANY_REQUESTS = 429
|
||||||
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
||||||
|
HTTP_451_BLOCKED = 451
|
||||||
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
||||||
HTTP_501_NOT_IMPLEMENTED = 501
|
HTTP_501_NOT_IMPLEMENTED = 501
|
||||||
HTTP_502_BAD_GATEWAY = 502
|
HTTP_502_BAD_GATEWAY = 502
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
from taiga.base import routers
|
|
||||||
|
|
||||||
router = routers.DefaultRouter(trailing_slash=False)
|
|
|
@ -19,15 +19,16 @@ import sys
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
|
|
||||||
from . import signal_handlers as handlers
|
|
||||||
|
|
||||||
|
|
||||||
def connect_events_signals():
|
def connect_events_signals():
|
||||||
|
from . import signal_handlers as handlers
|
||||||
signals.post_save.connect(handlers.on_save_any_model, dispatch_uid="events_change")
|
signals.post_save.connect(handlers.on_save_any_model, dispatch_uid="events_change")
|
||||||
signals.post_delete.connect(handlers.on_delete_any_model, dispatch_uid="events_delete")
|
signals.post_delete.connect(handlers.on_delete_any_model, dispatch_uid="events_delete")
|
||||||
|
|
||||||
|
|
||||||
def disconnect_events_signals():
|
def disconnect_events_signals():
|
||||||
|
from . import signal_handlers as handlers
|
||||||
signals.post_save.disconnect(dispatch_uid="events_change")
|
signals.post_save.disconnect(dispatch_uid="events_change")
|
||||||
signals.post_delete.disconnect(dispatch_uid="events_delete")
|
signals.post_delete.disconnect(dispatch_uid="events_delete")
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ from taiga.projects.models import Project, Membership
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.tasks.models import Task
|
from taiga.projects.tasks.models import Task
|
||||||
from taiga.projects.serializers import ProjectSerializer
|
from taiga.projects.serializers import ProjectSerializer
|
||||||
|
from taiga.users import services as users_service
|
||||||
|
|
||||||
from . import mixins
|
from . import mixins
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
@ -90,6 +91,14 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
data = request.DATA.copy()
|
data = request.DATA.copy()
|
||||||
data['owner'] = data.get('owner', request.user.email)
|
data['owner'] = data.get('owner', request.user.email)
|
||||||
|
|
||||||
|
is_private = data.get('is_private', False)
|
||||||
|
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
|
||||||
|
self.request.user,
|
||||||
|
Project(is_private=is_private, id=None)
|
||||||
|
)
|
||||||
|
if not enough_slots:
|
||||||
|
raise exc.NotEnoughSlotsForProject(is_private, 1, not_enough_slots_error)
|
||||||
|
|
||||||
# Create Project
|
# Create Project
|
||||||
project_serialized = service.store_project(data)
|
project_serialized = service.store_project(data)
|
||||||
|
|
||||||
|
@ -106,11 +115,19 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
# Create memberships
|
# Create memberships
|
||||||
if "memberships" in data:
|
if "memberships" in data:
|
||||||
|
members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]])
|
||||||
|
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
|
||||||
|
self.request.user,
|
||||||
|
Project(is_private=is_private, id=None),
|
||||||
|
members
|
||||||
|
)
|
||||||
|
if not enough_slots:
|
||||||
|
raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error)
|
||||||
service.store_memberships(project_serialized.object, data)
|
service.store_memberships(project_serialized.object, data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
owner_membership = project_serialized.object.memberships.get(user=project_serialized.object.owner)
|
owner_membership = project_serialized.object.memberships.get(user=project_serialized.object.owner)
|
||||||
owner_membership.is_owner = True
|
owner_membership.is_admin = True
|
||||||
owner_membership.save()
|
owner_membership.save()
|
||||||
except Membership.DoesNotExist:
|
except Membership.DoesNotExist:
|
||||||
Membership.objects.create(
|
Membership.objects.create(
|
||||||
|
@ -118,7 +135,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
email=project_serialized.object.owner.email,
|
email=project_serialized.object.owner.email,
|
||||||
user=project_serialized.object.owner,
|
user=project_serialized.object.owner,
|
||||||
role=project_serialized.object.roles.all().first(),
|
role=project_serialized.object.roles.all().first(),
|
||||||
is_owner=True
|
is_admin=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create project values choicess
|
# Create project values choicess
|
||||||
|
@ -202,6 +219,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dump = json.load(reader(dump))
|
dump = json.load(reader(dump))
|
||||||
|
is_private = dump.get("is_private", False)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise exc.WrongArguments(_("Invalid dump format"))
|
raise exc.WrongArguments(_("Invalid dump format"))
|
||||||
|
|
||||||
|
@ -209,11 +227,23 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
||||||
if slug is not None and Project.objects.filter(slug=slug).exists():
|
if slug is not None and Project.objects.filter(slug=slug).exists():
|
||||||
del dump['slug']
|
del dump['slug']
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
dump['owner'] = user.email
|
||||||
|
|
||||||
|
members = len([m for m in dump.get("memberships", []) if m.get("email", None) != dump["owner"]])
|
||||||
|
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
|
||||||
|
user,
|
||||||
|
Project(is_private=is_private, id=None),
|
||||||
|
members
|
||||||
|
)
|
||||||
|
if not enough_slots:
|
||||||
|
raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error)
|
||||||
|
|
||||||
if settings.CELERY_ENABLED:
|
if settings.CELERY_ENABLED:
|
||||||
task = tasks.load_project_dump.delay(request.user, dump)
|
task = tasks.load_project_dump.delay(user, dump)
|
||||||
return response.Accepted({"import_id": task.id})
|
return response.Accepted({"import_id": task.id})
|
||||||
|
|
||||||
project = dump_service.dict_to_project(dump, request.user.email)
|
project = dump_service.dict_to_project(dump, request.user)
|
||||||
response_data = ProjectSerializer(project).data
|
response_data = ProjectSerializer(project).data
|
||||||
return response.Created(response_data)
|
return response.Created(response_data)
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.projects.models import Membership
|
from taiga.projects.models import Membership, Project
|
||||||
|
from taiga.users import services as users_service
|
||||||
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import service
|
from . import service
|
||||||
|
@ -89,7 +90,15 @@ def store_tags_colors(project, data):
|
||||||
|
|
||||||
def dict_to_project(data, owner=None):
|
def dict_to_project(data, owner=None):
|
||||||
if owner:
|
if owner:
|
||||||
data["owner"] = owner
|
data["owner"] = owner.email
|
||||||
|
members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]])
|
||||||
|
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
|
||||||
|
owner,
|
||||||
|
Project(is_private=data.get("is_private", False), id=None),
|
||||||
|
members
|
||||||
|
)
|
||||||
|
if not enough_slots:
|
||||||
|
raise TaigaImportError(not_enough_slots_error)
|
||||||
|
|
||||||
project_serialized = service.store_project(data)
|
project_serialized = service.store_project(data)
|
||||||
|
|
||||||
|
@ -138,7 +147,7 @@ def dict_to_project(data, owner=None):
|
||||||
email=proj.owner.email,
|
email=proj.owner.email,
|
||||||
user=proj.owner,
|
user=proj.owner,
|
||||||
role=proj.roles.all().first(),
|
role=proj.roles.all().first(),
|
||||||
is_owner=True
|
is_admin=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if service.get_errors(clear=False):
|
if service.get_errors(clear=False):
|
||||||
|
|
|
@ -25,6 +25,7 @@ from taiga.projects.models import Project
|
||||||
from taiga.export_import.renderers import ExportRenderer
|
from taiga.export_import.renderers import ExportRenderer
|
||||||
from taiga.export_import.dump_service import dict_to_project, TaigaImportError
|
from taiga.export_import.dump_service import dict_to_project, TaigaImportError
|
||||||
from taiga.export_import.service import get_errors
|
from taiga.export_import.service import get_errors
|
||||||
|
from taiga.users.models import User
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -58,7 +59,9 @@ class Command(BaseCommand):
|
||||||
except Project.DoesNotExist:
|
except Project.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
signals.post_delete.receivers = receivers_back
|
signals.post_delete.receivers = receivers_back
|
||||||
dict_to_project(data, args[1])
|
|
||||||
|
user = User.objects.get(email=args[1])
|
||||||
|
dict_to_project(data, user)
|
||||||
except TaigaImportError as e:
|
except TaigaImportError as e:
|
||||||
print("ERROR:", end=" ")
|
print("ERROR:", end=" ")
|
||||||
print(e.message)
|
print(e.message)
|
||||||
|
|
|
@ -17,11 +17,11 @@
|
||||||
|
|
||||||
|
|
||||||
from taiga.base.api.permissions import (TaigaResourcePermission,
|
from taiga.base.api.permissions import (TaigaResourcePermission,
|
||||||
IsProjectOwner, IsAuthenticated)
|
IsProjectAdmin, IsAuthenticated)
|
||||||
|
|
||||||
|
|
||||||
class ImportExportPermission(TaigaResourcePermission):
|
class ImportExportPermission(TaigaResourcePermission):
|
||||||
import_project_perms = IsAuthenticated()
|
import_project_perms = IsAuthenticated()
|
||||||
import_item_perms = IsProjectOwner()
|
import_item_perms = IsProjectAdmin()
|
||||||
export_project_perms = IsProjectOwner()
|
export_project_perms = IsProjectAdmin()
|
||||||
load_dump_perms = IsAuthenticated()
|
load_dump_perms = IsAuthenticated()
|
||||||
|
|
|
@ -21,6 +21,7 @@ import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -29,10 +30,10 @@ from django.utils.translation import ugettext as _
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
|
||||||
from taiga import mdrender
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.fields import JsonField, PgArrayField
|
from taiga.base.fields import JsonField, PgArrayField
|
||||||
|
|
||||||
|
from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.projects import models as projects_models
|
from taiga.projects import models as projects_models
|
||||||
from taiga.projects.custom_attributes import models as custom_attributes_models
|
from taiga.projects.custom_attributes import models as custom_attributes_models
|
||||||
from taiga.projects.userstories import models as userstories_models
|
from taiga.projects.userstories import models as userstories_models
|
||||||
|
@ -154,7 +155,7 @@ class CommentField(serializers.WritableField):
|
||||||
|
|
||||||
def field_from_native(self, data, files, field_name, into):
|
def field_from_native(self, data, files, field_name, into):
|
||||||
super().field_from_native(data, files, field_name, into)
|
super().field_from_native(data, files, field_name, into)
|
||||||
into["comment_html"] = mdrender.render(self.context['project'], data.get("comment", ""))
|
into["comment_html"] = mdrender(self.context['project'], data.get("comment", ""))
|
||||||
|
|
||||||
|
|
||||||
class ProjectRelatedField(serializers.RelatedField):
|
class ProjectRelatedField(serializers.RelatedField):
|
||||||
|
@ -263,7 +264,7 @@ class WatcheableObjectModelSerializer(serializers.ModelSerializer):
|
||||||
adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
|
adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
|
||||||
removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
|
removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
|
||||||
|
|
||||||
User = apps.get_model("users", "User")
|
User = get_user_model()
|
||||||
adding_users = User.objects.filter(email__in=adding_watcher_emails)
|
adding_users = User.objects.filter(email__in=adding_watcher_emails)
|
||||||
removing_users = User.objects.filter(email__in=removing_watcher_emails)
|
removing_users = User.objects.filter(email__in=removing_watcher_emails)
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ def delete_project_dump(project_id, project_slug, task_id):
|
||||||
@app.task
|
@app.task
|
||||||
def load_project_dump(user, dump):
|
def load_project_dump(user, dump):
|
||||||
try:
|
try:
|
||||||
project = dict_to_project(dump, user.email)
|
project = dict_to_project(dump, user)
|
||||||
except Exception:
|
except Exception:
|
||||||
ctx = {
|
ctx = {
|
||||||
"user": user,
|
"user": user,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "emails/base-body-html.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% trans user=user.get_full_name()|safe, project=project.name|safe, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
|
{% trans user=user.get_full_name(), project=project.name, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
|
||||||
<h1>Project dump generated</h1>
|
<h1>Project dump generated</h1>
|
||||||
<p>Hello {{ user }},</p>
|
<p>Hello {{ user }},</p>
|
||||||
<h3>Your dump from project {{ project }} has been correctly generated.</h3>
|
<h3>Your dump from project {{ project }} has been correctly generated.</h3>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% trans user=user.get_full_name()|safe, project=project.name|safe, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
|
{% trans user=user.get_full_name(), project=project.name, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}
|
||||||
Hello {{ user }},
|
Hello {{ user }},
|
||||||
|
|
||||||
Your dump from project {{ project }} has been correctly generated. You can download it here:
|
Your dump from project {{ project }} has been correctly generated. You can download it here:
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{% trans project=project.name|safe %}[{{ project }}] Your project dump has been generated{% endtrans %}
|
{% trans project=project.name %}[{{ project }}] Your project dump has been generated{% endtrans %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "emails/base-body-html.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email"), project=project.name|safe %}
|
{% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email"), project=project.name %}
|
||||||
<h1>{{ error_message }}</h1>
|
<h1>{{ error_message }}</h1>
|
||||||
<p>Hello {{ user }},</p>
|
<p>Hello {{ user }},</p>
|
||||||
<p>Your project {{ project }} has not been exported correctly.</p>
|
<p>Your project {{ project }} has not been exported correctly.</p>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email"), project=project.name|safe %}
|
{% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email"), project=project.name %}
|
||||||
Hello {{ user }},
|
Hello {{ user }},
|
||||||
|
|
||||||
{{ error_message }}
|
{{ error_message }}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{% trans error_subject=error_subject, project=project.name|safe %}[{{ project }}] {{ error_subject }}{% endtrans %}
|
{% trans error_subject=error_subject, project=project.name %}[{{ project }}] {{ error_subject }}{% endtrans %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "emails/base-body-html.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email") %}
|
{% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email") %}
|
||||||
<h1>{{ error_message }}</h1>
|
<h1>{{ error_message }}</h1>
|
||||||
<p>Hello {{ user }},</p>
|
<p>Hello {{ user }},</p>
|
||||||
<p>Your project has not been importer correctly.</p>
|
<p>Your project has not been importer correctly.</p>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email") %}
|
{% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email") %}
|
||||||
Hello {{ user }},
|
Hello {{ user }},
|
||||||
|
|
||||||
{{ error_message }}
|
{{ error_message }}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "emails/base-body-html.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% trans user=user.get_full_name()|safe, url=resolve_front_url("project", project.slug), project=project.name|safe %}
|
{% trans user=user.get_full_name(), url=resolve_front_url("project", project.slug), project=project.name %}
|
||||||
<h1>Project dump imported</h1>
|
<h1>Project dump imported</h1>
|
||||||
<p>Hello {{ user }},</p>
|
<p>Hello {{ user }},</p>
|
||||||
<h3>Your project dump has been correctly imported.</h3>
|
<h3>Your project dump has been correctly imported.</h3>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% trans user=user.get_full_name()|safe, url=resolve_front_url("project", project.slug), project=project.name|safe %}
|
{% trans user=user.get_full_name(), url=resolve_front_url("project", project.slug), project=project.name %}
|
||||||
Hello {{ user }},
|
Hello {{ user }},
|
||||||
|
|
||||||
Your project dump has been correctly imported.
|
Your project dump has been correctly imported.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{% trans project=project.name|safe %}[{{ project }}] Your project dump has been imported{% endtrans %}
|
{% trans project=project.name %}[{{ project }}] Your project dump has been imported{% endtrans %}
|
||||||
|
|
|
@ -20,8 +20,6 @@ from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
|
||||||
from .routers import router
|
|
||||||
|
|
||||||
|
|
||||||
class FeedbackAppConfig(AppConfig):
|
class FeedbackAppConfig(AppConfig):
|
||||||
name = "taiga.feedback"
|
name = "taiga.feedback"
|
||||||
|
@ -30,4 +28,5 @@ class FeedbackAppConfig(AppConfig):
|
||||||
def ready(self):
|
def ready(self):
|
||||||
if settings.FEEDBACK_ENABLED:
|
if settings.FEEDBACK_ENABLED:
|
||||||
from taiga.urls import urlpatterns
|
from taiga.urls import urlpatterns
|
||||||
|
from .routers import router
|
||||||
urlpatterns.append(url(r'^api/v1/', include(router.urls)))
|
urlpatterns.append(url(r'^api/v1/', include(router.urls)))
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "emails/base-body-html.jinja" %}
|
{% extends "emails/base-body-html.jinja" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email %}
|
{% trans full_name=feedback_entry.full_name, email=feedback_entry.email %}
|
||||||
<h1>Feedback</h1>
|
<h1>Feedback</h1>
|
||||||
<p>Taiga has received feedback from {{ full_name }} <{{ email }}></p>
|
<p>Taiga has received feedback from {{ full_name }} <{{ email }}></p>
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email, comment=feedback_entry.comment %}---------
|
{% trans full_name=feedback_entry.full_name, email=feedback_entry.email, comment=feedback_entry.comment %}---------
|
||||||
- From: {{ full_name }} <{{ email }}>
|
- From: {{ full_name }} <{{ email }}>
|
||||||
---------
|
---------
|
||||||
- Comment:
|
- Comment:
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email %}
|
{% trans full_name=feedback_entry.full_name, email=feedback_entry.email %}
|
||||||
[Taiga] Feedback from {{ full_name }} <{{ email }}>
|
[Taiga] Feedback from {{ full_name }} <{{ email }}>
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
|
|
|
@ -32,6 +32,9 @@ class IssuesSitemap(Sitemap):
|
||||||
Q(project__is_private=True,
|
Q(project__is_private=True,
|
||||||
project__anon_permissions__contains=["view_issues"]))
|
project__anon_permissions__contains=["view_issues"]))
|
||||||
|
|
||||||
|
# Exclude blocked projects
|
||||||
|
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||||
|
|
||||||
# Project data is needed
|
# Project data is needed
|
||||||
queryset = queryset.select_related("project")
|
queryset = queryset.select_related("project")
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,9 @@ class MilestonesSitemap(Sitemap):
|
||||||
"view_us",
|
"view_us",
|
||||||
"view_tasks"]))
|
"view_tasks"]))
|
||||||
|
|
||||||
|
# Exclude blocked projects
|
||||||
|
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||||
|
|
||||||
# Project data is needed
|
# Project data is needed
|
||||||
queryset = queryset.select_related("project")
|
queryset = queryset.select_related("project")
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,9 @@ class ProjectsSitemap(Sitemap):
|
||||||
Q(is_private=True,
|
Q(is_private=True,
|
||||||
anon_permissions__contains=["view_project"]))
|
anon_permissions__contains=["view_project"]))
|
||||||
|
|
||||||
|
# Exclude blocked projects
|
||||||
|
queryset = queryset.filter(blocked_code__isnull=True)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def location(self, obj):
|
def location(self, obj):
|
||||||
|
|
|
@ -32,6 +32,9 @@ class TasksSitemap(Sitemap):
|
||||||
Q(project__is_private=True,
|
Q(project__is_private=True,
|
||||||
project__anon_permissions__contains=["view_tasks"]))
|
project__anon_permissions__contains=["view_tasks"]))
|
||||||
|
|
||||||
|
# Exclude blocked projects
|
||||||
|
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||||
|
|
||||||
# Project data is needed
|
# Project data is needed
|
||||||
queryset = queryset.select_related("project")
|
queryset = queryset.select_related("project")
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,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.apps import apps
|
from django.apps import apps
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from taiga.front.templatetags.functions import resolve
|
from taiga.front.templatetags.functions import resolve
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ from .base import Sitemap
|
||||||
|
|
||||||
class UsersSitemap(Sitemap):
|
class UsersSitemap(Sitemap):
|
||||||
def items(self):
|
def items(self):
|
||||||
user_model = apps.get_model("users", "User")
|
user_model = get_user_model()
|
||||||
|
|
||||||
# Only active users and not system users
|
# Only active users and not system users
|
||||||
queryset = user_model.objects.filter(is_active=True,
|
queryset = user_model.objects.filter(is_active=True,
|
||||||
|
|
|
@ -32,6 +32,9 @@ class UserStoriesSitemap(Sitemap):
|
||||||
Q(project__is_private=True,
|
Q(project__is_private=True,
|
||||||
project__anon_permissions__contains=["view_us"]))
|
project__anon_permissions__contains=["view_us"]))
|
||||||
|
|
||||||
|
# Exclude blocked projects
|
||||||
|
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||||
|
|
||||||
# Project data is needed
|
# Project data is needed
|
||||||
queryset = queryset.select_related("project")
|
queryset = queryset.select_related("project")
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,9 @@ class WikiPagesSitemap(Sitemap):
|
||||||
Q(project__is_private=True,
|
Q(project__is_private=True,
|
||||||
project__anon_permissions__contains=["view_wiki_pages"]))
|
project__anon_permissions__contains=["view_wiki_pages"]))
|
||||||
|
|
||||||
|
# Exclude blocked projects
|
||||||
|
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||||
|
|
||||||
# Exclude wiki pages from projects without wiki section enabled
|
# Exclude wiki pages from projects without wiki section enabled
|
||||||
queryset = queryset.exclude(project__is_wiki_activated=False)
|
queryset = queryset.exclude(project__is_wiki_activated=False)
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ urls = {
|
||||||
|
|
||||||
"team": "/project/{0}/team/", # project.slug
|
"team": "/project/{0}/team/", # project.slug
|
||||||
|
|
||||||
"project-admin": "/project/{0}/admin/project-profile/details", # project.slug
|
"project-transfer": "/project/{0}/transfer/{1}", # project.slug, project.transfer_token
|
||||||
}
|
|
||||||
|
|
||||||
|
"project-admin": "/login?next=/project/{0}/admin/project-profile/details", # project.slug
|
||||||
|
}
|
||||||
|
|
|
@ -64,6 +64,9 @@ class BaseWebhookApiViewSet(GenericViewSet):
|
||||||
if not self._validate_signature(project, request):
|
if not self._validate_signature(project, request):
|
||||||
raise exc.BadRequest(_("Bad signature"))
|
raise exc.BadRequest(_("Bad signature"))
|
||||||
|
|
||||||
|
if project.blocked_code is not None:
|
||||||
|
raise exc.Blocked(_("Blocked element"))
|
||||||
|
|
||||||
event_name = self._get_event_name(request)
|
event_name = self._get_event_name(request)
|
||||||
|
|
||||||
payload = self._get_payload(request)
|
payload = self._get_payload(request)
|
||||||
|
|
|
@ -40,19 +40,16 @@ class PushEventHook(BaseEventHook):
|
||||||
|
|
||||||
changes = self.payload.get("push", {}).get('changes', [])
|
changes = self.payload.get("push", {}).get('changes', [])
|
||||||
for change in filter(None, changes):
|
for change in filter(None, changes):
|
||||||
new = change.get("new", None)
|
commits = change.get("commits", [])
|
||||||
if not new:
|
if not commits:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
target = new.get("target", None)
|
for commit in commits:
|
||||||
if not target:
|
message = commit.get("message", None)
|
||||||
continue
|
if not message:
|
||||||
|
continue
|
||||||
|
|
||||||
message = target.get("message", None)
|
self._process_message(message, None)
|
||||||
if not message:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._process_message(message, None)
|
|
||||||
|
|
||||||
def _process_message(self, message, bitbucket_user):
|
def _process_message(self, message, bitbucket_user):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -17,10 +17,10 @@
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from taiga.users.models import User
|
|
||||||
from taiga.base.utils.urls import get_absolute_url
|
from taiga.base.utils.urls import get_absolute_url
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,4 +43,4 @@ def get_or_generate_config(project):
|
||||||
|
|
||||||
|
|
||||||
def get_bitbucket_user(user_id):
|
def get_bitbucket_user(user_id):
|
||||||
return User.objects.get(is_system=True, username__startswith="bitbucket")
|
return get_user_model().objects.get(is_system=True, username__startswith="bitbucket")
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from taiga.users.models import User
|
|
||||||
from taiga.users.models import AuthData
|
from taiga.users.models import AuthData
|
||||||
from taiga.base.utils.urls import get_absolute_url
|
from taiga.base.utils.urls import get_absolute_url
|
||||||
|
|
||||||
|
@ -49,6 +49,6 @@ def get_github_user(github_id):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
user = User.objects.get(is_system=True, username__startswith="github")
|
user = get_user_model().objects.get(is_system=True, username__startswith="github")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
|
@ -17,10 +17,10 @@
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from taiga.users.models import User
|
|
||||||
from taiga.base.utils.urls import get_absolute_url
|
from taiga.base.utils.urls import get_absolute_url
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,11 +47,11 @@ def get_gitlab_user(user_email):
|
||||||
|
|
||||||
if user_email:
|
if user_email:
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(email=user_email)
|
user = get_user_model().objects.get(email=user_email)
|
||||||
except User.DoesNotExist:
|
except get_user_model().DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
user = User.objects.get(is_system=True, username__startswith="gitlab")
|
user = get_user_model().objects.get(is_system=True, username__startswith="gitlab")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,18 +0,0 @@
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
from .service import *
|
|
|
@ -22,13 +22,12 @@
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from markdown.extensions import Extension
|
from markdown.extensions import Extension
|
||||||
from markdown.inlinepatterns import Pattern
|
from markdown.inlinepatterns import Pattern
|
||||||
from markdown.util import etree, AtomicString
|
from markdown.util import etree, AtomicString
|
||||||
|
|
||||||
from taiga.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class MentionsExtension(Extension):
|
class MentionsExtension(Extension):
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md, md_globals):
|
||||||
|
@ -43,8 +42,8 @@ class MentionsPattern(Pattern):
|
||||||
username = m.group(3)
|
username = m.group(3)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(username=username)
|
user = get_user_model().objects.get(username=username)
|
||||||
except User.DoesNotExist:
|
except get_user_model().DoesNotExist:
|
||||||
return "@{}".format(username)
|
return "@{}".format(username)
|
||||||
|
|
||||||
url = "/profile/{}".format(username)
|
url = "/profile/{}".format(username)
|
||||||
|
|
|
@ -144,4 +144,5 @@ def get_diff_of_htmls(html1, html2):
|
||||||
diffutil.diff_cleanupSemantic(diffs)
|
diffutil.diff_cleanupSemantic(diffs)
|
||||||
return diffutil.diff_pretty_html(diffs)
|
return diffutil.diff_pretty_html(diffs)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["render", "get_diff_of_htmls", "render_and_extract"]
|
__all__ = ["render", "get_diff_of_htmls", "render_and_extract"]
|
||||||
|
|
|
@ -82,7 +82,7 @@ MEMBERS_PERMISSIONS = [
|
||||||
('delete_wiki_link', _('Delete wiki link')),
|
('delete_wiki_link', _('Delete wiki link')),
|
||||||
]
|
]
|
||||||
|
|
||||||
OWNERS_PERMISSIONS = [
|
ADMINS_PERMISSIONS = [
|
||||||
('modify_project', _('Modify project')),
|
('modify_project', _('Modify project')),
|
||||||
('add_member', _('Add member')),
|
('add_member', _('Add member')),
|
||||||
('remove_member', _('Remove member')),
|
('remove_member', _('Remove member')),
|
||||||
|
|
|
@ -16,12 +16,11 @@
|
||||||
# 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 .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
|
from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
def _get_user_project_membership(user, project):
|
def _get_user_project_membership(user, project):
|
||||||
Membership = apps.get_model("projects", "Membership")
|
|
||||||
if user.is_anonymous():
|
if user.is_anonymous():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -39,10 +38,17 @@ def _get_object_project(obj):
|
||||||
|
|
||||||
|
|
||||||
def is_project_owner(user, obj):
|
def is_project_owner(user, obj):
|
||||||
"""
|
project = _get_object_project(obj)
|
||||||
The owner attribute of a project is just an historical reference
|
if project is None:
|
||||||
"""
|
return False
|
||||||
|
|
||||||
|
if user.id == project.owner.id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_project_admin(user, obj):
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -51,7 +57,7 @@ def is_project_owner(user, obj):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
membership = _get_user_project_membership(user, project)
|
membership = _get_user_project_membership(user, project)
|
||||||
if membership and membership.is_owner:
|
if membership and membership.is_admin:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -79,43 +85,41 @@ def _get_membership_permissions(membership):
|
||||||
def get_user_project_permissions(user, project):
|
def get_user_project_permissions(user, project):
|
||||||
membership = _get_user_project_membership(user, project)
|
membership = _get_user_project_membership(user, project)
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS))
|
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
|
||||||
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
|
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
|
||||||
public_permissions = list(map(lambda perm: perm[0], USER_PERMISSIONS))
|
public_permissions = list(map(lambda perm: perm[0], USER_PERMISSIONS))
|
||||||
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
|
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
|
||||||
elif membership:
|
elif membership:
|
||||||
if membership.is_owner:
|
if membership.is_admin:
|
||||||
owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS))
|
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
|
||||||
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
|
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
|
||||||
else:
|
else:
|
||||||
owner_permissions = []
|
admins_permissions = []
|
||||||
members_permissions = []
|
members_permissions = []
|
||||||
members_permissions = members_permissions + _get_membership_permissions(membership)
|
members_permissions = members_permissions + _get_membership_permissions(membership)
|
||||||
public_permissions = project.public_permissions if project.public_permissions is not None else []
|
public_permissions = project.public_permissions if project.public_permissions is not None else []
|
||||||
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
|
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
|
||||||
elif user.is_authenticated():
|
elif user.is_authenticated():
|
||||||
owner_permissions = []
|
admins_permissions = []
|
||||||
members_permissions = []
|
members_permissions = []
|
||||||
public_permissions = project.public_permissions if project.public_permissions is not None else []
|
public_permissions = project.public_permissions if project.public_permissions is not None else []
|
||||||
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
|
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
|
||||||
else:
|
else:
|
||||||
owner_permissions = []
|
admins_permissions = []
|
||||||
members_permissions = []
|
members_permissions = []
|
||||||
public_permissions = []
|
public_permissions = []
|
||||||
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
|
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
|
||||||
|
|
||||||
return set(owner_permissions + members_permissions + public_permissions + anon_permissions)
|
return set(admins_permissions + members_permissions + public_permissions + anon_permissions)
|
||||||
|
|
||||||
|
|
||||||
def set_base_permissions_for_project(project):
|
def set_base_permissions_for_project(project):
|
||||||
if project.is_private:
|
if project.is_private:
|
||||||
project.anon_permissions = []
|
project.anon_permissions = []
|
||||||
project.public_permissions = []
|
project.public_permissions = []
|
||||||
|
|
||||||
else:
|
else:
|
||||||
"""
|
# If a project is public anonymous and registered users should have at
|
||||||
If a project is public anonymous and registered users should have at least visualization permissions
|
# least visualization permissions.
|
||||||
"""
|
|
||||||
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
|
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
|
||||||
project.anon_permissions = list(set((project.anon_permissions or []) + anon_permissions))
|
project.anon_permissions = list(set((project.anon_permissions or []) + anon_permissions))
|
||||||
project.public_permissions = list(set((project.public_permissions or []) + anon_permissions))
|
project.public_permissions = list(set((project.public_permissions or []) + anon_permissions))
|
||||||
|
|
|
@ -26,6 +26,7 @@ from django.db.models.functions import Coalesce
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
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.http import Http404
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
|
@ -33,6 +34,7 @@ from taiga.base import exceptions as exc
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base.decorators import detail_route
|
from taiga.base.decorators import detail_route
|
||||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
|
from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
|
||||||
from taiga.base.api.permissions import AllowAnyPermission
|
from taiga.base.api.permissions import AllowAnyPermission
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
|
@ -50,6 +52,7 @@ from taiga.projects.tasks.models import Task
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
||||||
from taiga.permissions import service as permissions_service
|
from taiga.permissions import service as permissions_service
|
||||||
|
from taiga.users import services as users_service
|
||||||
|
|
||||||
from . import filters as project_filters
|
from . import filters as project_filters
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -61,7 +64,9 @@ from . import services
|
||||||
######################################################
|
######################################################
|
||||||
## Project
|
## Project
|
||||||
######################################################
|
######################################################
|
||||||
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet):
|
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
|
BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
|
||||||
|
|
||||||
queryset = models.Project.objects.all()
|
queryset = models.Project.objects.all()
|
||||||
serializer_class = serializers.ProjectDetailSerializer
|
serializer_class = serializers.ProjectDetailSerializer
|
||||||
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
||||||
|
@ -88,6 +93,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
"total_activity_last_month",
|
"total_activity_last_month",
|
||||||
"total_activity_last_year")
|
"total_activity_last_year")
|
||||||
|
|
||||||
|
def is_blocked(self, obj):
|
||||||
|
return obj.blocked_code is not None
|
||||||
|
|
||||||
def _get_order_by_field_name(self):
|
def _get_order_by_field_name(self):
|
||||||
order_by_query_param = project_filters.CanViewProjectObjFilterBackend.order_by_query_param
|
order_by_query_param = project_filters.CanViewProjectObjFilterBackend.order_by_query_param
|
||||||
order_by = self.request.QUERY_PARAMS.get(order_by_query_param, None)
|
order_by = self.request.QUERY_PARAMS.get(order_by_query_param, None)
|
||||||
|
@ -97,9 +105,11 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
|
|
||||||
|
qs = qs.select_related("owner")
|
||||||
# Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries)
|
# Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries)
|
||||||
# so we add some custom prefetching
|
# so we add some custom prefetching
|
||||||
qs = qs.prefetch_related("members")
|
qs = qs.prefetch_related("members")
|
||||||
|
qs = qs.prefetch_related("memberships")
|
||||||
qs = qs.prefetch_related(Prefetch("notify_policies",
|
qs = qs.prefetch_related(Prefetch("notify_policies",
|
||||||
NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
|
NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
|
||||||
|
|
||||||
|
@ -137,7 +147,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
else:
|
else:
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
|
|
||||||
if permissions_service.is_project_owner(self.request.user, project):
|
if permissions_service.is_project_admin(self.request.user, project):
|
||||||
serializer_class = self.admin_serializer_class
|
serializer_class = self.admin_serializer_class
|
||||||
|
|
||||||
return serializer_class
|
return serializer_class
|
||||||
|
@ -158,6 +168,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise exc.WrongArguments(_("Invalid image format"))
|
raise exc.WrongArguments(_("Invalid image format"))
|
||||||
|
|
||||||
|
self.pre_conditions_on_save(self.object)
|
||||||
|
|
||||||
self.object.logo = logo
|
self.object.logo = logo
|
||||||
self.object.save(update_fields=["logo"])
|
self.object.save(update_fields=["logo"])
|
||||||
|
|
||||||
|
@ -171,7 +183,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
"""
|
"""
|
||||||
self.object = get_object_or_404(self.get_queryset(), **kwargs)
|
self.object = get_object_or_404(self.get_queryset(), **kwargs)
|
||||||
self.check_permissions(request, "remove_logo", self.object)
|
self.check_permissions(request, "remove_logo", self.object)
|
||||||
|
self.pre_conditions_on_save(self.object)
|
||||||
self.object.logo = None
|
self.object.logo = None
|
||||||
self.object.save(update_fields=["logo"])
|
self.object.save(update_fields=["logo"])
|
||||||
|
|
||||||
|
@ -182,6 +194,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
def watch(self, request, pk=None):
|
def watch(self, request, pk=None):
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
self.check_permissions(request, "watch", project)
|
self.check_permissions(request, "watch", project)
|
||||||
|
self.pre_conditions_on_save(project)
|
||||||
notify_level = request.DATA.get("notify_level", NotifyLevel.involved)
|
notify_level = request.DATA.get("notify_level", NotifyLevel.involved)
|
||||||
project.add_watcher(self.request.user, notify_level=notify_level)
|
project.add_watcher(self.request.user, notify_level=notify_level)
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
@ -190,6 +203,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
def unwatch(self, request, pk=None):
|
def unwatch(self, request, pk=None):
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
self.check_permissions(request, "unwatch", project)
|
self.check_permissions(request, "unwatch", project)
|
||||||
|
self.pre_conditions_on_save(project)
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
project.remove_watcher(user)
|
project.remove_watcher(user)
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
@ -207,77 +221,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
services.update_projects_order_in_bulk(data, "user_order", request.user)
|
services.update_projects_order_in_bulk(data, "user_order", request.user)
|
||||||
return response.NoContent(data=None)
|
return response.NoContent(data=None)
|
||||||
|
|
||||||
@list_route(methods=["GET"])
|
|
||||||
def by_slug(self, request):
|
|
||||||
slug = request.QUERY_PARAMS.get("slug", None)
|
|
||||||
project = get_object_or_404(models.Project, slug=slug)
|
|
||||||
return self.retrieve(request, pk=project.pk)
|
|
||||||
|
|
||||||
@detail_route(methods=["GET", "PATCH"])
|
|
||||||
def modules(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, 'modules', project)
|
|
||||||
modules_config = services.get_modules_config(project)
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
return response.Ok(modules_config.config)
|
|
||||||
|
|
||||||
else:
|
|
||||||
modules_config.config.update(request.DATA)
|
|
||||||
modules_config.save()
|
|
||||||
return response.NoContent()
|
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
|
||||||
def stats(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, "stats", project)
|
|
||||||
return response.Ok(services.get_stats_for_project(project))
|
|
||||||
|
|
||||||
def _regenerate_csv_uuid(self, project, field):
|
|
||||||
uuid_value = uuid.uuid4().hex
|
|
||||||
setattr(project, field, uuid_value)
|
|
||||||
project.save()
|
|
||||||
return uuid_value
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
|
||||||
def regenerate_userstories_csv_uuid(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, "regenerate_userstories_csv_uuid", project)
|
|
||||||
data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")}
|
|
||||||
return response.Ok(data)
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
|
||||||
def regenerate_issues_csv_uuid(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, "regenerate_issues_csv_uuid", project)
|
|
||||||
data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")}
|
|
||||||
return response.Ok(data)
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
|
||||||
def regenerate_tasks_csv_uuid(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, "regenerate_tasks_csv_uuid", project)
|
|
||||||
data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")}
|
|
||||||
return response.Ok(data)
|
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
|
||||||
def member_stats(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, "member_stats", project)
|
|
||||||
return response.Ok(services.get_member_stats_for_project(project))
|
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
|
||||||
def issues_stats(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, "issues_stats", project)
|
|
||||||
return response.Ok(services.get_stats_for_project_issues(project))
|
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
|
||||||
def tags_colors(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, "tags_colors", project)
|
|
||||||
return response.Ok(dict(project.tags_colors))
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def create_template(self, request, **kwargs):
|
def create_template(self, request, **kwargs):
|
||||||
template_name = request.DATA.get('template_name', None)
|
template_name = request.DATA.get('template_name', None)
|
||||||
|
@ -305,13 +248,161 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
template.save()
|
template.save()
|
||||||
return response.Created(serializers.ProjectTemplateSerializer(template).data)
|
return response.Created(serializers.ProjectTemplateSerializer(template).data)
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=['POST'])
|
||||||
def leave(self, request, pk=None):
|
def leave(self, request, pk=None):
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
self.check_permissions(request, 'leave', project)
|
self.check_permissions(request, 'leave', project)
|
||||||
|
self.pre_conditions_on_save(project)
|
||||||
services.remove_user_from_project(request.user, project)
|
services.remove_user_from_project(request.user, project)
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def regenerate_userstories_csv_uuid(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "regenerate_userstories_csv_uuid", project)
|
||||||
|
self.pre_conditions_on_save(project)
|
||||||
|
data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")}
|
||||||
|
return response.Ok(data)
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def regenerate_issues_csv_uuid(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "regenerate_issues_csv_uuid", project)
|
||||||
|
self.pre_conditions_on_save(project)
|
||||||
|
data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")}
|
||||||
|
return response.Ok(data)
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def regenerate_tasks_csv_uuid(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "regenerate_tasks_csv_uuid", project)
|
||||||
|
self.pre_conditions_on_save(project)
|
||||||
|
data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")}
|
||||||
|
return response.Ok(data)
|
||||||
|
|
||||||
|
@list_route(methods=["GET"])
|
||||||
|
def by_slug(self, request):
|
||||||
|
slug = request.QUERY_PARAMS.get("slug", None)
|
||||||
|
project = get_object_or_404(models.Project, slug=slug)
|
||||||
|
return self.retrieve(request, pk=project.pk)
|
||||||
|
|
||||||
|
@detail_route(methods=["GET", "PATCH"])
|
||||||
|
def modules(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, 'modules', project)
|
||||||
|
modules_config = services.get_modules_config(project)
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
return response.Ok(modules_config.config)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.pre_conditions_on_save(project)
|
||||||
|
modules_config.config.update(request.DATA)
|
||||||
|
modules_config.save()
|
||||||
|
return response.NoContent()
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def stats(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "stats", project)
|
||||||
|
return response.Ok(services.get_stats_for_project(project))
|
||||||
|
|
||||||
|
def _regenerate_csv_uuid(self, project, field):
|
||||||
|
uuid_value = uuid.uuid4().hex
|
||||||
|
setattr(project, field, uuid_value)
|
||||||
|
project.save()
|
||||||
|
return uuid_value
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def member_stats(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "member_stats", project)
|
||||||
|
return response.Ok(services.get_member_stats_for_project(project))
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def issues_stats(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "issues_stats", project)
|
||||||
|
return response.Ok(services.get_stats_for_project_issues(project))
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def tags_colors(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "tags_colors", project)
|
||||||
|
return response.Ok(dict(project.tags_colors))
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def transfer_validate_token(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "transfer_validate_token", project)
|
||||||
|
token = request.DATA.get('token', None)
|
||||||
|
services.transfer.validate_project_transfer_token(token, project, request.user)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def transfer_request(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "transfer_request", project)
|
||||||
|
services.request_project_transfer(project, request.user)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
def transfer_start(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "transfer_start", project)
|
||||||
|
|
||||||
|
user_id = request.DATA.get('user', None)
|
||||||
|
if user_id is None:
|
||||||
|
raise exc.WrongArguments(_("Invalid user id"))
|
||||||
|
|
||||||
|
user_model = apps.get_model("users", "User")
|
||||||
|
try:
|
||||||
|
user = user_model.objects.get(id=user_id)
|
||||||
|
except user_model.DoesNotExist:
|
||||||
|
return response.BadRequest(_("The user doesn't exist"))
|
||||||
|
|
||||||
|
# Check the user is a membership from the project
|
||||||
|
if not project.memberships.filter(user=user).exists():
|
||||||
|
return response.BadRequest(_("The user must be already a project member"))
|
||||||
|
|
||||||
|
reason = request.DATA.get('reason', None)
|
||||||
|
transfer_token = services.start_project_transfer(project, user, reason)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def transfer_accept(self, request, pk=None):
|
||||||
|
token = request.DATA.get('token', None)
|
||||||
|
if token is None:
|
||||||
|
raise exc.WrongArguments(_("Invalid token"))
|
||||||
|
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "transfer_accept", project)
|
||||||
|
|
||||||
|
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
|
||||||
|
request.user,
|
||||||
|
project,
|
||||||
|
)
|
||||||
|
if not enough_slots:
|
||||||
|
members = project.memberships.count()
|
||||||
|
raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error)
|
||||||
|
|
||||||
|
reason = request.DATA.get('reason', None)
|
||||||
|
services.accept_project_transfer(project, request.user, token, reason)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def transfer_reject(self, request, pk=None):
|
||||||
|
token = request.DATA.get('token', None)
|
||||||
|
if token is None:
|
||||||
|
raise exc.WrongArguments(_("Invalid token"))
|
||||||
|
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "transfer_reject", project)
|
||||||
|
|
||||||
|
reason = request.DATA.get('reason', None)
|
||||||
|
services.reject_project_transfer(project, request.user, token, reason)
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
def _set_base_permissions(self, obj):
|
def _set_base_permissions(self, obj):
|
||||||
update_permissions = False
|
update_permissions = False
|
||||||
if not obj.id:
|
if not obj.id:
|
||||||
|
@ -329,9 +420,15 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
if not obj.id:
|
if not obj.id:
|
||||||
obj.owner = self.request.user
|
obj.owner = self.request.user
|
||||||
# TODO REFACTOR THIS
|
|
||||||
obj.template = self.request.QUERY_PARAMS.get('template', None)
|
obj.template = self.request.QUERY_PARAMS.get('template', None)
|
||||||
|
|
||||||
|
# Validate if the owner have enought slots to create or update the project
|
||||||
|
# TODO: Move to the ProjectAdminSerializer
|
||||||
|
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(obj.owner, obj)
|
||||||
|
if not enough_slots:
|
||||||
|
members = max(obj.memberships.count(), 1)
|
||||||
|
raise exc.NotEnoughSlotsForProject(obj.is_private, members, not_enough_slots_error)
|
||||||
|
|
||||||
self._set_base_permissions(obj)
|
self._set_base_permissions(obj)
|
||||||
super().pre_save(obj)
|
super().pre_save(obj)
|
||||||
|
|
||||||
|
@ -342,10 +439,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
||||||
if obj is None:
|
if obj is None:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
obj.delete_related_content()
|
|
||||||
|
|
||||||
self.pre_delete(obj)
|
self.pre_delete(obj)
|
||||||
self.pre_conditions_on_delete(obj)
|
self.pre_conditions_on_delete(obj)
|
||||||
|
obj.delete_related_content()
|
||||||
obj.delete()
|
obj.delete()
|
||||||
self.post_delete(obj)
|
self.post_delete(obj)
|
||||||
return response.NoContent()
|
return response.NoContent()
|
||||||
|
@ -365,7 +461,9 @@ class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
|
||||||
## Custom values for selectors
|
## Custom values for selectors
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
|
|
||||||
model = models.Points
|
model = models.Points
|
||||||
serializer_class = serializers.PointsSerializer
|
serializer_class = serializers.PointsSerializer
|
||||||
permission_classes = (permissions.PointsPermission,)
|
permission_classes = (permissions.PointsPermission,)
|
||||||
|
@ -379,7 +477,9 @@ class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
move_on_destroy_project_default_field = "default_points"
|
move_on_destroy_project_default_field = "default_points"
|
||||||
|
|
||||||
|
|
||||||
class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
|
|
||||||
model = models.UserStoryStatus
|
model = models.UserStoryStatus
|
||||||
serializer_class = serializers.UserStoryStatusSerializer
|
serializer_class = serializers.UserStoryStatusSerializer
|
||||||
permission_classes = (permissions.UserStoryStatusPermission,)
|
permission_classes = (permissions.UserStoryStatusPermission,)
|
||||||
|
@ -393,7 +493,9 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrd
|
||||||
move_on_destroy_project_default_field = "default_us_status"
|
move_on_destroy_project_default_field = "default_us_status"
|
||||||
|
|
||||||
|
|
||||||
class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
|
|
||||||
model = models.TaskStatus
|
model = models.TaskStatus
|
||||||
serializer_class = serializers.TaskStatusSerializer
|
serializer_class = serializers.TaskStatusSerializer
|
||||||
permission_classes = (permissions.TaskStatusPermission,)
|
permission_classes = (permissions.TaskStatusPermission,)
|
||||||
|
@ -407,7 +509,9 @@ class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMix
|
||||||
move_on_destroy_project_default_field = "default_task_status"
|
move_on_destroy_project_default_field = "default_task_status"
|
||||||
|
|
||||||
|
|
||||||
class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
|
|
||||||
model = models.Severity
|
model = models.Severity
|
||||||
serializer_class = serializers.SeveritySerializer
|
serializer_class = serializers.SeveritySerializer
|
||||||
permission_classes = (permissions.SeverityPermission,)
|
permission_classes = (permissions.SeverityPermission,)
|
||||||
|
@ -421,7 +525,8 @@ class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin
|
||||||
move_on_destroy_project_default_field = "default_severity"
|
move_on_destroy_project_default_field = "default_severity"
|
||||||
|
|
||||||
|
|
||||||
class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
model = models.Priority
|
model = models.Priority
|
||||||
serializer_class = serializers.PrioritySerializer
|
serializer_class = serializers.PrioritySerializer
|
||||||
permission_classes = (permissions.PriorityPermission,)
|
permission_classes = (permissions.PriorityPermission,)
|
||||||
|
@ -435,7 +540,8 @@ class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin
|
||||||
move_on_destroy_project_default_field = "default_priority"
|
move_on_destroy_project_default_field = "default_priority"
|
||||||
|
|
||||||
|
|
||||||
class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
model = models.IssueType
|
model = models.IssueType
|
||||||
serializer_class = serializers.IssueTypeSerializer
|
serializer_class = serializers.IssueTypeSerializer
|
||||||
permission_classes = (permissions.IssueTypePermission,)
|
permission_classes = (permissions.IssueTypePermission,)
|
||||||
|
@ -449,7 +555,8 @@ class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixi
|
||||||
move_on_destroy_project_default_field = "default_issue_type"
|
move_on_destroy_project_default_field = "default_issue_type"
|
||||||
|
|
||||||
|
|
||||||
class IssueStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
model = models.IssueStatus
|
model = models.IssueStatus
|
||||||
serializer_class = serializers.IssueStatusSerializer
|
serializer_class = serializers.IssueStatusSerializer
|
||||||
permission_classes = (permissions.IssueStatusPermission,)
|
permission_classes = (permissions.IssueStatusPermission,)
|
||||||
|
@ -480,7 +587,7 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
|
||||||
## Members & Invitations
|
## Members & Invitations
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class MembershipViewSet(ModelCrudViewSet):
|
class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.Membership
|
model = models.Membership
|
||||||
admin_serializer_class = serializers.MembershipAdminSerializer
|
admin_serializer_class = serializers.MembershipAdminSerializer
|
||||||
serializer_class = serializers.MembershipSerializer
|
serializer_class = serializers.MembershipSerializer
|
||||||
|
@ -495,12 +602,12 @@ class MembershipViewSet(ModelCrudViewSet):
|
||||||
use_admin_serializer = True
|
use_admin_serializer = True
|
||||||
|
|
||||||
if self.action == "retrieve":
|
if self.action == "retrieve":
|
||||||
use_admin_serializer = permissions_service.is_project_owner(self.request.user, self.object.project)
|
use_admin_serializer = permissions_service.is_project_admin(self.request.user, self.object.project)
|
||||||
|
|
||||||
project_id = self.request.QUERY_PARAMS.get("project", None)
|
project_id = self.request.QUERY_PARAMS.get("project", None)
|
||||||
if self.action == "list" and project_id is not None:
|
if self.action == "list" and project_id is not None:
|
||||||
project = get_object_or_404(models.Project, pk=project_id)
|
project = get_object_or_404(models.Project, pk=project_id)
|
||||||
use_admin_serializer = permissions_service.is_project_owner(self.request.user, project)
|
use_admin_serializer = permissions_service.is_project_admin(self.request.user, project)
|
||||||
|
|
||||||
if use_admin_serializer:
|
if use_admin_serializer:
|
||||||
return self.admin_serializer_class
|
return self.admin_serializer_class
|
||||||
|
@ -518,10 +625,22 @@ class MembershipViewSet(ModelCrudViewSet):
|
||||||
project = models.Project.objects.get(id=data["project_id"])
|
project = models.Project.objects.get(id=data["project_id"])
|
||||||
invitation_extra_text = data.get("invitation_extra_text", None)
|
invitation_extra_text = data.get("invitation_extra_text", None)
|
||||||
self.check_permissions(request, 'bulk_create', project)
|
self.check_permissions(request, 'bulk_create', project)
|
||||||
|
if project.blocked_code is not None:
|
||||||
|
raise exc.Blocked(_("Blocked element"))
|
||||||
|
|
||||||
# TODO: this should be moved to main exception handler instead
|
# TODO: this should be moved to main exception handler instead
|
||||||
# of handling explicit exception catchin here.
|
# of handling explicit exception catchin here.
|
||||||
|
|
||||||
|
if "bulk_memberships" in data and isinstance(data["bulk_memberships"], list):
|
||||||
|
members = len(data["bulk_memberships"])
|
||||||
|
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
|
||||||
|
project.owner,
|
||||||
|
project,
|
||||||
|
members
|
||||||
|
)
|
||||||
|
if not enough_slots:
|
||||||
|
raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
members = services.create_members_in_bulk(data["bulk_memberships"],
|
members = services.create_members_in_bulk(data["bulk_memberships"],
|
||||||
project=project,
|
project=project,
|
||||||
|
@ -539,15 +658,26 @@ class MembershipViewSet(ModelCrudViewSet):
|
||||||
invitation = self.get_object()
|
invitation = self.get_object()
|
||||||
|
|
||||||
self.check_permissions(request, 'resend_invitation', invitation.project)
|
self.check_permissions(request, 'resend_invitation', invitation.project)
|
||||||
|
self.pre_conditions_on_save(invitation)
|
||||||
|
|
||||||
services.send_invitation(invitation=invitation)
|
services.send_invitation(invitation=invitation)
|
||||||
return response.NoContent()
|
return response.NoContent()
|
||||||
|
|
||||||
def pre_delete(self, obj):
|
def pre_delete(self, obj):
|
||||||
if obj.user is not None and not services.can_user_leave_project(obj.user, obj.project):
|
if obj.user is not None and not services.can_user_leave_project(obj.user, obj.project):
|
||||||
raise exc.BadRequest(_("At least one of the user must be an active admin"))
|
raise exc.BadRequest(_("The project must have an owner and at least one of the users must be an active admin"))
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
|
if not obj.id:
|
||||||
|
members = 1
|
||||||
|
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
|
||||||
|
self.request.user,
|
||||||
|
obj.project,
|
||||||
|
members
|
||||||
|
)
|
||||||
|
if not enough_slots:
|
||||||
|
raise exc.NotEnoughSlotsForProject(obj.project.is_private, members, not_enough_slots_error)
|
||||||
|
|
||||||
if not obj.token:
|
if not obj.token:
|
||||||
obj.token = str(uuid.uuid1())
|
obj.token = str(uuid.uuid1())
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,11 @@ from django.apps import AppConfig
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
|
|
||||||
from . import signals as handlers
|
|
||||||
|
|
||||||
|
|
||||||
## Project Signals
|
## Project Signals
|
||||||
|
|
||||||
def connect_projects_signals():
|
def connect_projects_signals():
|
||||||
|
from . import signals as handlers
|
||||||
# On project object is created apply template.
|
# On project object is created apply template.
|
||||||
signals.post_save.connect(handlers.project_post_save,
|
signals.post_save.connect(handlers.project_post_save,
|
||||||
sender=apps.get_model("projects", "Project"),
|
sender=apps.get_model("projects", "Project"),
|
||||||
|
@ -51,6 +50,7 @@ def disconnect_projects_signals():
|
||||||
## Memberships Signals
|
## Memberships Signals
|
||||||
|
|
||||||
def connect_memberships_signals():
|
def connect_memberships_signals():
|
||||||
|
from . import signals as handlers
|
||||||
# On membership object is deleted, update role-points relation.
|
# On membership object is deleted, update role-points relation.
|
||||||
signals.pre_delete.connect(handlers.membership_post_delete,
|
signals.pre_delete.connect(handlers.membership_post_delete,
|
||||||
sender=apps.get_model("projects", "Membership"),
|
sender=apps.get_model("projects", "Membership"),
|
||||||
|
@ -71,6 +71,7 @@ def disconnect_memberships_signals():
|
||||||
## US Statuses Signals
|
## US Statuses Signals
|
||||||
|
|
||||||
def connect_us_status_signals():
|
def connect_us_status_signals():
|
||||||
|
from . import signals as handlers
|
||||||
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_us_status,
|
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_us_status,
|
||||||
sender=apps.get_model("projects", "UserStoryStatus"),
|
sender=apps.get_model("projects", "UserStoryStatus"),
|
||||||
dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status")
|
dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status")
|
||||||
|
@ -85,6 +86,7 @@ def disconnect_us_status_signals():
|
||||||
## Tasks Statuses Signals
|
## Tasks Statuses Signals
|
||||||
|
|
||||||
def connect_task_status_signals():
|
def connect_task_status_signals():
|
||||||
|
from . import signals as handlers
|
||||||
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_task_status,
|
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_task_status,
|
||||||
sender=apps.get_model("projects", "TaskStatus"),
|
sender=apps.get_model("projects", "TaskStatus"),
|
||||||
dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status")
|
dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status")
|
||||||
|
|
|
@ -16,7 +16,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.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.contenttypes import generic
|
from django.contrib.contenttypes.admin import GenericTabularInline
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class AttachmentAdmin(admin.ModelAdmin):
|
||||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AttachmentInline(generic.GenericTabularInline):
|
class AttachmentInline(GenericTabularInline):
|
||||||
model = models.Attachment
|
model = models.Attachment
|
||||||
fields = ("attached_file", "owner")
|
fields = ("attached_file", "owner")
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
|
@ -25,6 +25,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
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.api import ModelCrudViewSet
|
from taiga.base.api import ModelCrudViewSet
|
||||||
|
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||||
|
@ -35,7 +36,9 @@ from . import serializers
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
|
class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
|
BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
|
|
||||||
model = models.Attachment
|
model = models.Attachment
|
||||||
serializer_class = serializers.AttachmentSerializer
|
serializer_class = serializers.AttachmentSerializer
|
||||||
filter_fields = ["project", "object_id"]
|
filter_fields = ["project", "object_id"]
|
||||||
|
|
|
@ -20,7 +20,7 @@ import hashlib
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes import generic
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.text import get_valid_filename
|
from django.utils.text import get_valid_filename
|
||||||
|
@ -42,7 +42,7 @@ class Attachment(models.Model):
|
||||||
verbose_name=_("content type"))
|
verbose_name=_("content type"))
|
||||||
object_id = models.PositiveIntegerField(null=False, blank=False,
|
object_id = models.PositiveIntegerField(null=False, blank=False,
|
||||||
verbose_name=_("object id"))
|
verbose_name=_("object id"))
|
||||||
content_object = generic.GenericForeignKey("content_type", "object_id")
|
content_object = GenericForeignKey("content_type", "object_id")
|
||||||
created_date = models.DateTimeField(null=False, blank=False,
|
created_date = models.DateTimeField(null=False, blank=False,
|
||||||
verbose_name=_("created date"),
|
verbose_name=_("created date"),
|
||||||
default=timezone.now)
|
default=timezone.now)
|
||||||
|
|
|
@ -24,3 +24,12 @@ VIDEOCONFERENCES_CHOICES = (
|
||||||
("custom", _("Custom")),
|
("custom", _("Custom")),
|
||||||
("talky", _("Talky")),
|
("talky", _("Talky")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
BLOCKED_BY_NONPAYMENT = "blocked-by-nonpayment"
|
||||||
|
BLOCKED_BY_STAFF = "blocked-by-staff"
|
||||||
|
BLOCKED_BY_OWNER_LEAVING = "blocked-by-owner-leaving"
|
||||||
|
BLOCKING_CODES = [
|
||||||
|
(BLOCKED_BY_NONPAYMENT, _("This project is blocked due to payment failure")),
|
||||||
|
(BLOCKED_BY_STAFF, _("This project is blocked by admin staff")),
|
||||||
|
(BLOCKED_BY_OWNER_LEAVING, _("This project is blocked because the owner left"))
|
||||||
|
]
|
||||||
|
|
|
@ -19,6 +19,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from taiga.base.api import ModelCrudViewSet
|
from taiga.base.api import ModelCrudViewSet
|
||||||
from taiga.base.api import ModelUpdateRetrieveViewSet
|
from taiga.base.api import ModelUpdateRetrieveViewSet
|
||||||
|
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
|
@ -38,7 +39,7 @@ from . import services
|
||||||
# Custom Attribute ViewSets
|
# Custom Attribute ViewSets
|
||||||
#######################################################
|
#######################################################
|
||||||
|
|
||||||
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.UserStoryCustomAttribute
|
model = models.UserStoryCustomAttribute
|
||||||
serializer_class = serializers.UserStoryCustomAttributeSerializer
|
serializer_class = serializers.UserStoryCustomAttributeSerializer
|
||||||
permission_classes = (permissions.UserStoryCustomAttributePermission,)
|
permission_classes = (permissions.UserStoryCustomAttributePermission,)
|
||||||
|
@ -49,7 +50,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
||||||
bulk_update_order_action = services.bulk_update_userstory_custom_attribute_order
|
bulk_update_order_action = services.bulk_update_userstory_custom_attribute_order
|
||||||
|
|
||||||
|
|
||||||
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.TaskCustomAttribute
|
model = models.TaskCustomAttribute
|
||||||
serializer_class = serializers.TaskCustomAttributeSerializer
|
serializer_class = serializers.TaskCustomAttributeSerializer
|
||||||
permission_classes = (permissions.TaskCustomAttributePermission,)
|
permission_classes = (permissions.TaskCustomAttributePermission,)
|
||||||
|
@ -60,7 +61,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
||||||
bulk_update_order_action = services.bulk_update_task_custom_attribute_order
|
bulk_update_order_action = services.bulk_update_task_custom_attribute_order
|
||||||
|
|
||||||
|
|
||||||
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.IssueCustomAttribute
|
model = models.IssueCustomAttribute
|
||||||
serializer_class = serializers.IssueCustomAttributeSerializer
|
serializer_class = serializers.IssueCustomAttributeSerializer
|
||||||
permission_classes = (permissions.IssueCustomAttributePermission,)
|
permission_classes = (permissions.IssueCustomAttributePermission,)
|
||||||
|
@ -76,7 +77,7 @@ class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
||||||
#######################################################
|
#######################################################
|
||||||
|
|
||||||
class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
ModelUpdateRetrieveViewSet):
|
BlockedByProjectMixin, ModelUpdateRetrieveViewSet):
|
||||||
def get_object_for_snapshot(self, obj):
|
def get_object_for_snapshot(self, obj):
|
||||||
return getattr(obj, self.content_object)
|
return getattr(obj, self.content_object)
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,11 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
TEXT_TYPE = "text"
|
TEXT_TYPE = "text"
|
||||||
MULTILINE_TYPE = "multiline"
|
MULTILINE_TYPE = "multiline"
|
||||||
DATE_TYPE = "date"
|
DATE_TYPE = "date"
|
||||||
|
URL_TYPE = "url"
|
||||||
|
|
||||||
TYPES_CHOICES = (
|
TYPES_CHOICES = (
|
||||||
(TEXT_TYPE, _("Text")),
|
(TEXT_TYPE, _("Text")),
|
||||||
(MULTILINE_TYPE, _("Multi-Line Text")),
|
(MULTILINE_TYPE, _("Multi-Line Text")),
|
||||||
(DATE_TYPE, _("Date"))
|
(DATE_TYPE, _("Date")),
|
||||||
|
(URL_TYPE, _("Url"))
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('custom_attributes', '0006_auto_20151014_1645'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issuecustomattribute',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='taskcustomattribute',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstorycustomattribute',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
from taiga.base.api.permissions import TaigaResourcePermission
|
from taiga.base.api.permissions import TaigaResourcePermission
|
||||||
from taiga.base.api.permissions import HasProjectPerm
|
from taiga.base.api.permissions import HasProjectPerm
|
||||||
from taiga.base.api.permissions import IsProjectOwner
|
from taiga.base.api.permissions import IsProjectAdmin
|
||||||
from taiga.base.api.permissions import AllowAny
|
from taiga.base.api.permissions import AllowAny
|
||||||
from taiga.base.api.permissions import IsSuperUser
|
from taiga.base.api.permissions import IsSuperUser
|
||||||
|
|
||||||
|
@ -27,39 +27,39 @@ from taiga.base.api.permissions import IsSuperUser
|
||||||
#######################################################
|
#######################################################
|
||||||
|
|
||||||
class UserStoryCustomAttributePermission(TaigaResourcePermission):
|
class UserStoryCustomAttributePermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_project')
|
retrieve_perms = HasProjectPerm('view_project')
|
||||||
create_perms = IsProjectOwner()
|
create_perms = IsProjectAdmin()
|
||||||
update_perms = IsProjectOwner()
|
update_perms = IsProjectAdmin()
|
||||||
partial_update_perms = IsProjectOwner()
|
partial_update_perms = IsProjectAdmin()
|
||||||
destroy_perms = IsProjectOwner()
|
destroy_perms = IsProjectAdmin()
|
||||||
list_perms = AllowAny()
|
list_perms = AllowAny()
|
||||||
bulk_update_order_perms = IsProjectOwner()
|
bulk_update_order_perms = IsProjectAdmin()
|
||||||
|
|
||||||
|
|
||||||
class TaskCustomAttributePermission(TaigaResourcePermission):
|
class TaskCustomAttributePermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_project')
|
retrieve_perms = HasProjectPerm('view_project')
|
||||||
create_perms = IsProjectOwner()
|
create_perms = IsProjectAdmin()
|
||||||
update_perms = IsProjectOwner()
|
update_perms = IsProjectAdmin()
|
||||||
partial_update_perms = IsProjectOwner()
|
partial_update_perms = IsProjectAdmin()
|
||||||
destroy_perms = IsProjectOwner()
|
destroy_perms = IsProjectAdmin()
|
||||||
list_perms = AllowAny()
|
list_perms = AllowAny()
|
||||||
bulk_update_order_perms = IsProjectOwner()
|
bulk_update_order_perms = IsProjectAdmin()
|
||||||
|
|
||||||
|
|
||||||
class IssueCustomAttributePermission(TaigaResourcePermission):
|
class IssueCustomAttributePermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_project')
|
retrieve_perms = HasProjectPerm('view_project')
|
||||||
create_perms = IsProjectOwner()
|
create_perms = IsProjectAdmin()
|
||||||
update_perms = IsProjectOwner()
|
update_perms = IsProjectAdmin()
|
||||||
partial_update_perms = IsProjectOwner()
|
partial_update_perms = IsProjectAdmin()
|
||||||
destroy_perms = IsProjectOwner()
|
destroy_perms = IsProjectAdmin()
|
||||||
list_perms = AllowAny()
|
list_perms = AllowAny()
|
||||||
bulk_update_order_perms = IsProjectOwner()
|
bulk_update_order_perms = IsProjectAdmin()
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
|
@ -67,7 +67,7 @@ class IssueCustomAttributePermission(TaigaResourcePermission):
|
||||||
#######################################################
|
#######################################################
|
||||||
|
|
||||||
class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
|
class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_us')
|
retrieve_perms = HasProjectPerm('view_us')
|
||||||
update_perms = HasProjectPerm('modify_us')
|
update_perms = HasProjectPerm('modify_us')
|
||||||
|
@ -75,7 +75,7 @@ class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
|
||||||
|
|
||||||
|
|
||||||
class TaskCustomAttributesValuesPermission(TaigaResourcePermission):
|
class TaskCustomAttributesValuesPermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_tasks')
|
retrieve_perms = HasProjectPerm('view_tasks')
|
||||||
update_perms = HasProjectPerm('modify_task')
|
update_perms = HasProjectPerm('modify_task')
|
||||||
|
@ -83,7 +83,7 @@ class TaskCustomAttributesValuesPermission(TaigaResourcePermission):
|
||||||
|
|
||||||
|
|
||||||
class IssueCustomAttributesValuesPermission(TaigaResourcePermission):
|
class IssueCustomAttributesValuesPermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_issues')
|
retrieve_perms = HasProjectPerm('view_issues')
|
||||||
update_perms = HasProjectPerm('modify_issue')
|
update_perms = HasProjectPerm('modify_issue')
|
||||||
|
|
|
@ -37,7 +37,8 @@ class DiscoverModeFilterBackend(FilterBackend):
|
||||||
|
|
||||||
if discover_mode:
|
if discover_mode:
|
||||||
# discover_mode enabled
|
# discover_mode enabled
|
||||||
qs = qs.filter(anon_permissions__contains=["view_project"])
|
qs = qs.filter(anon_permissions__contains=["view_project"],
|
||||||
|
blocked_code__isnull=True)
|
||||||
|
|
||||||
return super().filter_queryset(request, qs.distinct(), view)
|
return super().filter_queryset(request, qs.distinct(), view)
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
|
||||||
if project_id:
|
if project_id:
|
||||||
memberships_qs = memberships_qs.filter(project_id=project_id)
|
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||||
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=['view_project']) |
|
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=['view_project']) |
|
||||||
Q(is_owner=True))
|
Q(is_admin=True))
|
||||||
|
|
||||||
projects_list = [membership.project_id for membership in memberships_qs]
|
projects_list = [membership.project_id for membership in memberships_qs]
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,10 @@ from contextlib import suppress
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from taiga.base.utils.urls import get_absolute_url
|
|
||||||
from taiga.base.utils.iterators import as_tuple
|
from taiga.base.utils.iterators import as_tuple
|
||||||
from taiga.base.utils.iterators import as_dict
|
from taiga.base.utils.iterators import as_dict
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
@ -49,7 +49,7 @@ def _get_generic_values(ids:tuple, *, typename=None, attr:str="name") -> tuple:
|
||||||
|
|
||||||
@as_dict
|
@as_dict
|
||||||
def _get_users_values(ids:set) -> dict:
|
def _get_users_values(ids:set) -> dict:
|
||||||
user_model = apps.get_model("users", "User")
|
user_model = get_user_model()
|
||||||
ids = filter(lambda x: x is not None, ids)
|
ids = filter(lambda x: x is not None, ids)
|
||||||
qs = user_model.objects.filter(pk__in=tuple(ids))
|
qs = user_model.objects.filter(pk__in=tuple(ids))
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,10 @@
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.apps import apps
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.conf import settings
|
|
||||||
from django_pgjson.fields import JsonField
|
from django_pgjson.fields import JsonField
|
||||||
|
|
||||||
from taiga.mdrender.service import get_diff_of_htmls
|
from taiga.mdrender.service import get_diff_of_htmls
|
||||||
|
@ -96,7 +94,7 @@ class HistoryEntry(models.Model):
|
||||||
@cached_property
|
@cached_property
|
||||||
def owner(self):
|
def owner(self):
|
||||||
pk = self.user["pk"]
|
pk = self.user["pk"]
|
||||||
model = apps.get_model("users", "User")
|
model = get_user_model()
|
||||||
try:
|
try:
|
||||||
return model.objects.get(pk=pk)
|
return model.objects.get(pk=pk)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
|
|
|
@ -16,10 +16,10 @@
|
||||||
# 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 taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
|
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
|
||||||
IsProjectOwner, AllowAny,
|
IsProjectAdmin, AllowAny,
|
||||||
IsObjectOwner, PermissionComponent)
|
IsObjectOwner, PermissionComponent)
|
||||||
|
|
||||||
from taiga.permissions.service import is_project_owner
|
from taiga.permissions.service import is_project_admin
|
||||||
from taiga.projects.history.services import get_model_from_key, get_pk_from_key
|
from taiga.projects.history.services import get_model_from_key, get_pk_from_key
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class IsCommentProjectOwner(PermissionComponent):
|
||||||
model = get_model_from_key(obj.key)
|
model = get_model_from_key(obj.key)
|
||||||
pk = get_pk_from_key(obj.key)
|
pk = get_pk_from_key(obj.key)
|
||||||
project = model.objects.get(pk=pk)
|
project = model.objects.get(pk=pk)
|
||||||
return is_project_owner(request.user, project)
|
return is_project_admin(request.user, project)
|
||||||
|
|
||||||
class UserStoryHistoryPermission(TaigaResourcePermission):
|
class UserStoryHistoryPermission(TaigaResourcePermission):
|
||||||
retrieve_perms = HasProjectPerm('view_project')
|
retrieve_perms = HasProjectPerm('view_project')
|
||||||
|
|
|
@ -16,24 +16,21 @@
|
||||||
# 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.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.db.models import Q
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
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
|
||||||
from taiga.base.decorators import detail_route, list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
|
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
||||||
from taiga.users.models import User
|
|
||||||
|
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
from taiga.projects.occ import OCCResourceMixin
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
|
|
||||||
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
|
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
|
||||||
from taiga.projects.milestones.models import Milestone
|
|
||||||
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -43,7 +40,7 @@ from . import serializers
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
ModelCrudViewSet):
|
BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
queryset = models.Issue.objects.all()
|
queryset = models.Issue.objects.all()
|
||||||
permission_classes = (permissions.IssuePermission, )
|
permission_classes = (permissions.IssuePermission, )
|
||||||
filter_backends = (filters.CanViewIssuesFilterBackend,
|
filter_backends = (filters.CanViewIssuesFilterBackend,
|
||||||
|
@ -157,8 +154,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
super().pre_save(obj)
|
super().pre_save(obj)
|
||||||
|
|
||||||
def pre_conditions_on_save(self, obj):
|
def pre_conditions_on_save(self, obj):
|
||||||
super().pre_conditions_on_save(obj)
|
|
||||||
|
|
||||||
if obj.milestone and obj.milestone.project != obj.project:
|
if obj.milestone and obj.milestone.project != obj.project:
|
||||||
raise exc.PermissionDenied(_("You don't have permissions to set this sprint "
|
raise exc.PermissionDenied(_("You don't have permissions to set this sprint "
|
||||||
"to this issue."))
|
"to this issue."))
|
||||||
|
@ -179,6 +174,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
raise exc.PermissionDenied(_("You don't have permissions to set this type "
|
raise exc.PermissionDenied(_("You don't have permissions to set this type "
|
||||||
"to this issue."))
|
"to this issue."))
|
||||||
|
|
||||||
|
super().pre_conditions_on_save(obj)
|
||||||
|
|
||||||
@list_route(methods=["GET"])
|
@list_route(methods=["GET"])
|
||||||
def by_ref(self, request):
|
def by_ref(self, request):
|
||||||
ref = request.QUERY_PARAMS.get("ref", None)
|
ref = request.QUERY_PARAMS.get("ref", None)
|
||||||
|
@ -232,6 +229,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
project = Project.objects.get(pk=data["project_id"])
|
project = Project.objects.get(pk=data["project_id"])
|
||||||
self.check_permissions(request, 'bulk_create', project)
|
self.check_permissions(request, 'bulk_create', project)
|
||||||
|
if project.blocked_code is not None:
|
||||||
|
raise exc.Blocked(_("Blocked element"))
|
||||||
|
|
||||||
issues = services.create_issues_in_bulk(
|
issues = services.create_issues_in_bulk(
|
||||||
data["bulk_issues"], project=project, owner=request.user,
|
data["bulk_issues"], project=project, owner=request.user,
|
||||||
status=project.default_issue_status, severity=project.default_severity,
|
status=project.default_issue_status, severity=project.default_severity,
|
||||||
|
|
|
@ -19,12 +19,11 @@ from django.apps import AppConfig
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
|
|
||||||
from taiga.projects import signals as generic_handlers
|
|
||||||
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
|
|
||||||
from . import signals as handlers
|
|
||||||
|
|
||||||
|
|
||||||
def connect_issues_signals():
|
def connect_issues_signals():
|
||||||
|
from taiga.projects import signals as generic_handlers
|
||||||
|
from . import signals as handlers
|
||||||
|
|
||||||
# Finished date
|
# Finished date
|
||||||
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
|
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
|
||||||
sender=apps.get_model("issues", "Issue"),
|
sender=apps.get_model("issues", "Issue"),
|
||||||
|
@ -43,6 +42,8 @@ def connect_issues_signals():
|
||||||
|
|
||||||
|
|
||||||
def connect_issues_custom_attributes_signals():
|
def connect_issues_custom_attributes_signals():
|
||||||
|
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
|
||||||
|
|
||||||
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_issue,
|
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_issue,
|
||||||
sender=apps.get_model("issues", "Issue"),
|
sender=apps.get_model("issues", "Issue"),
|
||||||
dispatch_uid="create_custom_attribute_value_when_create_issue")
|
dispatch_uid="create_custom_attribute_value_when_create_issue")
|
||||||
|
|
|
@ -16,7 +16,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.db import models
|
from django.db import models
|
||||||
from django.contrib.contenttypes import generic
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
@ -63,7 +63,7 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.
|
||||||
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||||
default=None, related_name="issues_assigned_to_me",
|
default=None, related_name="issues_assigned_to_me",
|
||||||
verbose_name=_("assigned to"))
|
verbose_name=_("assigned to"))
|
||||||
attachments = generic.GenericRelation("attachments.Attachment")
|
attachments = GenericRelation("attachments.Attachment")
|
||||||
external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
|
external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
|
||||||
_importing = None
|
_importing = None
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,12 @@
|
||||||
|
|
||||||
|
|
||||||
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
|
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
|
||||||
IsProjectOwner, PermissionComponent,
|
IsProjectAdmin, PermissionComponent,
|
||||||
AllowAny, IsAuthenticated, IsSuperUser)
|
AllowAny, IsAuthenticated, IsSuperUser)
|
||||||
|
|
||||||
|
|
||||||
class IssuePermission(TaigaResourcePermission):
|
class IssuePermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_issues')
|
retrieve_perms = HasProjectPerm('view_issues')
|
||||||
create_perms = HasProjectPerm('add_issue')
|
create_perms = HasProjectPerm('add_issue')
|
||||||
|
@ -49,14 +49,14 @@ class HasIssueIdUrlParam(PermissionComponent):
|
||||||
|
|
||||||
|
|
||||||
class IssueVotersPermission(TaigaResourcePermission):
|
class IssueVotersPermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_issues')
|
retrieve_perms = HasProjectPerm('view_issues')
|
||||||
list_perms = HasProjectPerm('view_issues')
|
list_perms = HasProjectPerm('view_issues')
|
||||||
|
|
||||||
|
|
||||||
class IssueWatchersPermission(TaigaResourcePermission):
|
class IssueWatchersPermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
global_perms = None
|
global_perms = None
|
||||||
retrieve_perms = HasProjectPerm('view_issues')
|
retrieve_perms = HasProjectPerm('view_issues')
|
||||||
list_perms = HasProjectPerm('view_issues')
|
list_perms = HasProjectPerm('view_issues')
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue