Merge branch 'master' into stable
commit
bffdb71dad
|
@ -20,12 +20,18 @@ answer newbie questions, and generally made taiga that much better:
|
|||
- Andrea Stagi <stagi.andrea@gmail.com>
|
||||
- Andrés Moya <andres.moya@kaleidos.net>
|
||||
- Andrey Alekseenko <al42and@gmail.com>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
- Brett Profitt <brett.profitt@gmail.com>
|
||||
>>>>>>> master
|
||||
- Bruno Clermont <bruno@robotinfra.com>
|
||||
- Chris Wilson <chris.wilson@aridhia.com>
|
||||
- David Burke <david@burkesoftware.com>
|
||||
- Hector Colina <hcolina@gmail.com>
|
||||
- Joe Letts
|
||||
- Julien Palard
|
||||
- luyikei <luyikei.qmltu@gmail.com>
|
||||
- Motius GmbH <mail@motius.de>
|
||||
- Ricky Posner <e@eposner.com>
|
||||
- Yamila Moreno <yamila.moreno@kaleidos.net>
|
||||
- Brett Profitt <brett.profitt@gmail.com>
|
||||
- Yaser Alraddadi <yaser@yr.sa>
|
||||
|
|
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,6 +1,19 @@
|
|||
# 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)
|
||||
|
||||
### Features
|
||||
|
@ -10,7 +23,7 @@
|
|||
- Filter projects list by
|
||||
- is_looking_for_people
|
||||
- is_featured
|
||||
- is_backlog_activated
|
||||
- is_backlog_activated
|
||||
- is_kanban_activated
|
||||
- Search projects by text query (order by ranking name > tags > description)
|
||||
- Order projects list:
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
-r requirements.txt
|
||||
|
||||
factory_boy==2.6.0
|
||||
factory_boy==2.6.1
|
||||
py==1.4.31
|
||||
pytest==2.8.5
|
||||
pytest==2.8.7
|
||||
pytest-django==2.9.1
|
||||
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
|
||||
django-picklefield==0.3.2
|
||||
django-sampledatahelper==0.3.0
|
||||
gunicorn==19.3.0
|
||||
django-sampledatahelper==0.4.0
|
||||
gunicorn==19.4.5
|
||||
psycopg2==2.6.1
|
||||
Pillow==2.9.0
|
||||
Pillow==3.1.1
|
||||
pytz==2015.7
|
||||
six==1.10.0
|
||||
amqp==1.4.7
|
||||
djmail==0.11
|
||||
amqp==1.4.9
|
||||
djmail==0.12.0.post1
|
||||
django-pgjson==0.3.1
|
||||
djorm-pgarray==1.2
|
||||
django-jinja==2.1.1
|
||||
django-jinja==2.1.2
|
||||
jinja2==2.8
|
||||
pygments==2.0.2
|
||||
django-sites==0.8
|
||||
django-sites==0.9
|
||||
Markdown==2.6.5
|
||||
fn==0.4.3
|
||||
diff-match-patch==20121119
|
||||
requests==2.8.1
|
||||
requests==2.9.1
|
||||
django-sr==0.0.4
|
||||
easy-thumbnails==2.2.1
|
||||
celery==3.1.19
|
||||
easy-thumbnails==2.3
|
||||
celery==3.1.20
|
||||
redis==2.10.5
|
||||
Unidecode==0.04.18
|
||||
raven==5.9.2
|
||||
Unidecode==0.04.19
|
||||
raven==5.10.2
|
||||
bleach==1.4.2
|
||||
django-ipware==1.1.2
|
||||
premailer==2.9.6
|
||||
django-ipware==1.1.3
|
||||
premailer==2.9.7
|
||||
cssutils==1.0.1 # Compatible with python 3.5
|
||||
django-transactional-cleanup==0.1.15
|
||||
lxml==3.5.0
|
||||
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
|
||||
pyjwkest==1.0.9
|
||||
pyjwkest==1.1.5
|
||||
python-dateutil==2.4.2
|
||||
netaddr==0.7.18
|
||||
|
|
|
@ -30,7 +30,7 @@ DEBUG = False
|
|||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "transaction_hooks.backends.postgresql_psycopg2",
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": "taiga",
|
||||
}
|
||||
}
|
||||
|
@ -320,7 +320,6 @@ INSTALLED_APPS = [
|
|||
"sr",
|
||||
"easy_thumbnails",
|
||||
"raven.contrib.django.raven_compat",
|
||||
"django_transactional_cleanup",
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "taiga.wsgi.application"
|
||||
|
@ -347,7 +346,7 @@ LOGGING = {
|
|||
"handlers": {
|
||||
"null": {
|
||||
"level":"DEBUG",
|
||||
"class":"django.utils.log.NullHandler",
|
||||
"class":"logging.NullHandler",
|
||||
},
|
||||
"console":{
|
||||
"level":"DEBUG",
|
||||
|
@ -434,7 +433,9 @@ REST_FRAMEWORK = {
|
|||
# Extra expose header related to Taiga APP (see taiga.base.middleware.cors=)
|
||||
APP_EXTRA_EXPOSE_HEADERS = [
|
||||
"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"
|
||||
|
@ -522,6 +523,12 @@ WEBHOOKS_ENABLED = False
|
|||
FRONT_SITEMAP_ENABLED = False
|
||||
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 *
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# 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
|
||||
|
@ -24,7 +25,7 @@ from .development import *
|
|||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2',
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'taiga',
|
||||
'USER': 'taiga',
|
||||
'PASSWORD': 'changeme',
|
||||
|
|
|
@ -25,6 +25,7 @@ not uses clasess and uses simple functions.
|
|||
"""
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction as tx
|
||||
from django.db import IntegrityError
|
||||
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
|
||||
"""
|
||||
|
||||
user_model = apps.get_model("users", "User")
|
||||
user_model = get_user_model()
|
||||
if user_model.objects.filter(username=username):
|
||||
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:
|
||||
raise exc.WrongArguments(reason)
|
||||
|
||||
user_model = apps.get_model("users", "User")
|
||||
user_model = get_user_model()
|
||||
user = user_model(username=username,
|
||||
email=email,
|
||||
full_name=full_name)
|
||||
|
@ -159,7 +160,7 @@ def private_register_for_new_user(token:str, username:str, email:str,
|
|||
if is_registered:
|
||||
raise exc.WrongArguments(reason)
|
||||
|
||||
user_model = apps.get_model("users", "User")
|
||||
user_model = get_user_model()
|
||||
user = user_model(username=username,
|
||||
email=email,
|
||||
full_name=full_name)
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
#
|
||||
# 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.contrib.auth import get_user_model
|
||||
from taiga.base import exceptions as exc
|
||||
|
||||
from django.apps import apps
|
||||
|
@ -47,7 +47,7 @@ def get_user_for_token(token, scope, max_age=None):
|
|||
except signing.BadSignature:
|
||||
raise exc.NotAuthenticated(_("Invalid token"))
|
||||
|
||||
model_cls = apps.get_model("users", "User")
|
||||
model_cls = get_user_model()
|
||||
|
||||
try:
|
||||
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.translation import ugettext
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.datastructures import SortedDict
|
||||
|
||||
from . import ISO_8601
|
||||
from .settings import api_settings
|
||||
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal, DecimalException
|
||||
import copy
|
||||
import datetime
|
||||
import inspect
|
||||
import re
|
||||
import warnings
|
||||
from decimal import Decimal, DecimalException
|
||||
|
||||
|
||||
def is_non_str_iterable(obj):
|
||||
|
@ -255,7 +255,7 @@ class Field(object):
|
|||
return [self.to_native(item) for item in value]
|
||||
elif isinstance(value, dict):
|
||||
# Make sure we preserve field ordering, if it exists
|
||||
ret = SortedDict()
|
||||
ret = OrderedDict()
|
||||
for key, val in value.items():
|
||||
ret[key] = self.to_native(val)
|
||||
return ret
|
||||
|
@ -270,7 +270,7 @@ class Field(object):
|
|||
return {}
|
||||
|
||||
def metadata(self):
|
||||
metadata = SortedDict()
|
||||
metadata = OrderedDict()
|
||||
metadata["type"] = self.type_label
|
||||
metadata["required"] = getattr(self, "required", False)
|
||||
optional_attrs = ["read_only", "label", "help_text",
|
||||
|
|
|
@ -53,9 +53,9 @@ from taiga.base import response
|
|||
from .settings import api_settings
|
||||
from .utils import get_object_or_404
|
||||
|
||||
from .. import exceptions as exc
|
||||
from ..decorators import model_pk_lock
|
||||
|
||||
|
||||
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
|
||||
"""
|
||||
Given a model instance, and an optional pk and slug field,
|
||||
|
@ -243,3 +243,32 @@ class DestroyModelMixin:
|
|||
obj.delete()
|
||||
self.post_delete(obj)
|
||||
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 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.utils.translation import ugettext as _
|
||||
|
@ -206,9 +206,9 @@ class HasMandatoryParam(PermissionComponent):
|
|||
return False
|
||||
|
||||
|
||||
class IsProjectOwner(PermissionComponent):
|
||||
class IsProjectAdmin(PermissionComponent):
|
||||
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):
|
||||
|
|
|
@ -59,11 +59,11 @@ from django.core.paginator import Page
|
|||
from django.db import models
|
||||
from django.forms import widgets
|
||||
from django.utils import six
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from .settings import api_settings
|
||||
|
||||
from collections import OrderedDict
|
||||
import copy
|
||||
import datetime
|
||||
import inspect
|
||||
|
@ -148,7 +148,7 @@ class DictWithMetadata(dict):
|
|||
return dict(self)
|
||||
|
||||
|
||||
class SortedDictWithMetadata(SortedDict):
|
||||
class OrderedDictWithMetadata(OrderedDict):
|
||||
"""
|
||||
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
|
||||
pickle and may in some instances be unpickleable.
|
||||
"""
|
||||
return SortedDict(self).__dict__
|
||||
return OrderedDict(self).__dict__
|
||||
|
||||
|
||||
def _is_protected_type(obj):
|
||||
|
@ -194,7 +194,7 @@ def _get_declared_fields(bases, attrs):
|
|||
if hasattr(base, "base_fields"):
|
||||
fields = list(base.base_fields.items()) + fields
|
||||
|
||||
return SortedDict(fields)
|
||||
return OrderedDict(fields)
|
||||
|
||||
|
||||
class SerializerMetaclass(type):
|
||||
|
@ -222,7 +222,7 @@ class BaseSerializer(WritableField):
|
|||
pass
|
||||
|
||||
_options_class = SerializerOptions
|
||||
_dict_class = SortedDictWithMetadata
|
||||
_dict_class = OrderedDictWithMetadata
|
||||
|
||||
def __init__(self, instance=None, data=None, files=None,
|
||||
context=None, partial=False, many=None,
|
||||
|
@ -268,7 +268,7 @@ class BaseSerializer(WritableField):
|
|||
This will be the set of any explicitly declared fields,
|
||||
plus the set of fields returned by get_default_fields().
|
||||
"""
|
||||
ret = SortedDict()
|
||||
ret = OrderedDict()
|
||||
|
||||
# Get the explicitly declared 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 self.opts.fields:
|
||||
assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple"
|
||||
new = SortedDict()
|
||||
new = OrderedDict()
|
||||
for key in self.opts.fields:
|
||||
new[key] = ret[key]
|
||||
ret = new
|
||||
|
@ -458,7 +458,10 @@ class BaseSerializer(WritableField):
|
|||
many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type))
|
||||
|
||||
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)
|
||||
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
|
@ -610,7 +613,10 @@ class BaseSerializer(WritableField):
|
|||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
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:
|
||||
self._data = self.to_native(obj)
|
||||
|
||||
|
@ -645,7 +651,7 @@ class BaseSerializer(WritableField):
|
|||
Useful for things like responding to OPTIONS requests, or generating
|
||||
API schemas for auto-documentation.
|
||||
"""
|
||||
return SortedDict(
|
||||
return OrderedDict(
|
||||
[(field_name, field.metadata())
|
||||
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, \
|
||||
"Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__
|
||||
opts = cls._meta.concrete_model._meta
|
||||
ret = SortedDict()
|
||||
ret = OrderedDict()
|
||||
nested = bool(self.opts.depth)
|
||||
|
||||
# Deal with adding the primary key field
|
||||
|
|
|
@ -62,9 +62,10 @@ back to the defaults.
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import importlib
|
||||
from django.utils import six
|
||||
|
||||
import importlib
|
||||
|
||||
from . import ISO_8601
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
{% load url from future %}
|
||||
{% load api %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
{% load url from future %}
|
||||
{% load api %}
|
||||
<html>
|
||||
|
||||
|
|
|
@ -45,13 +45,10 @@
|
|||
Helper classes for parsers.
|
||||
"""
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.functional import Promise
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from taiga.base.api.serializers import DictWithMetadata, SortedDictWithMetadata
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
import types
|
||||
|
|
|
@ -43,6 +43,8 @@
|
|||
|
||||
import json
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
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.defaults import server_error
|
||||
from django.views.generic import View
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.encoding import smart_text
|
||||
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
|
||||
# generic views override this implementation and add additional
|
||||
# information for POST and PUT methods, based on the serializer.
|
||||
ret = SortedDict()
|
||||
ret = OrderedDict()
|
||||
ret['name'] = self.get_view_name()
|
||||
ret['description'] = self.get_view_description()
|
||||
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
||||
|
|
|
@ -187,11 +187,13 @@ class ModelListViewSet(mixins.RetrieveModelMixin,
|
|||
GenericViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class ModelUpdateRetrieveViewSet(mixins.UpdateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
GenericViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class ModelRetrieveViewSet(mixins.RetrieveModelMixin,
|
||||
GenericViewSet):
|
||||
pass
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from .signals.thumbnails import connect_thumbnail_signals
|
||||
|
||||
|
||||
class BaseAppConfig(AppConfig):
|
||||
name = "taiga.base"
|
||||
verbose_name = "Base App Config"
|
||||
|
||||
def ready(self):
|
||||
from .signals.thumbnails import connect_thumbnail_signals
|
||||
from .signals.cleanup_files import connect_cleanup_files_signals
|
||||
|
||||
connect_thumbnail_signals()
|
||||
connect_cleanup_files_signals()
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
from django_pglocks import advisory_lock
|
||||
|
||||
|
||||
def detail_route(methods=['get'], **kwargs):
|
||||
"""
|
||||
Used to mark a method on a ViewSet that should be routed for detail requests.
|
||||
|
|
|
@ -201,6 +201,28 @@ class NotAuthenticated(NotAuthenticated):
|
|||
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):
|
||||
if isinstance(exc.detail, (dict, list, tuple,)):
|
||||
detail = exc.detail
|
||||
|
@ -232,6 +254,9 @@ def exception_handler(exc):
|
|||
headers["WWW-Authenticate"] = exc.auth_header
|
||||
if getattr(exc, "wait", None):
|
||||
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)
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
|
@ -141,7 +142,7 @@ class PermissionBasedFilterBackend(FilterBackend):
|
|||
if project_id:
|
||||
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||
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]
|
||||
|
||||
|
@ -242,7 +243,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend):
|
|||
if project_id:
|
||||
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||
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]
|
||||
|
||||
|
@ -286,7 +287,7 @@ class BaseIsProjectAdminFilterBackend(object):
|
|||
return []
|
||||
|
||||
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:
|
||||
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ import datetime
|
|||
|
||||
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.utils import timezone
|
||||
|
||||
|
@ -28,7 +29,6 @@ from taiga.base.mails import mail_builder
|
|||
from taiga.projects.models import Project, Membership
|
||||
from taiga.projects.history.models import HistoryEntry
|
||||
from taiga.projects.history.services import get_history_queryset_by_model_instance
|
||||
from taiga.users.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -50,7 +50,7 @@ class Command(BaseCommand):
|
|||
|
||||
# Register email
|
||||
context = {"lang": locale,
|
||||
"user": User.objects.all().order_by("?").first(),
|
||||
"user": get_user_model().objects.all().order_by("?").first(),
|
||||
"cancel_token": "cancel-token"}
|
||||
|
||||
email = mail_builder.registered_user(test_email, context)
|
||||
|
@ -58,7 +58,7 @@ class Command(BaseCommand):
|
|||
|
||||
# Membership invitation
|
||||
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"
|
||||
|
||||
context = {"lang": locale, "membership": membership}
|
||||
|
@ -88,19 +88,19 @@ class Command(BaseCommand):
|
|||
email.send()
|
||||
|
||||
# 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.send()
|
||||
|
||||
# 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.send()
|
||||
|
||||
# Export/Import emails
|
||||
context = {
|
||||
"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(),
|
||||
"error_subject": "Error generating project dump",
|
||||
"error_message": "Error generating project dump",
|
||||
|
@ -109,7 +109,7 @@ class Command(BaseCommand):
|
|||
email.send()
|
||||
context = {
|
||||
"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_message": "Error importing project dump",
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ class Command(BaseCommand):
|
|||
context = {
|
||||
"lang": locale,
|
||||
"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(),
|
||||
"deletion_date": deletion_date,
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ class Command(BaseCommand):
|
|||
|
||||
context = {
|
||||
"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(),
|
||||
}
|
||||
email = mail_builder.load_dump(test_email, context)
|
||||
|
@ -157,13 +157,13 @@ class Command(BaseCommand):
|
|||
context = {
|
||||
"lang": locale,
|
||||
"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],
|
||||
"user": User.objects.all().order_by("?").first(),
|
||||
"user": get_user_model().objects.all().order_by("?").first(),
|
||||
}
|
||||
|
||||
for notification_email in notification_emails:
|
||||
model = get_model(*notification_email[0].split("."))
|
||||
model = apps.get_model(*notification_email[0].split("."))
|
||||
snapshot = {
|
||||
"subject": "Tests subject",
|
||||
"ref": 123123,
|
||||
|
@ -187,3 +187,38 @@ class Command(BaseCommand):
|
|||
cls = type("InlineCSSTemplateMail", (InlineCSSTemplateMail,), {"name": notification_email[1]})
|
||||
email = cls()
|
||||
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.
|
||||
|
||||
"""The various HTTP responses for use in returning proper HTTP codes."""
|
||||
from http.client import responses
|
||||
|
||||
from django import http
|
||||
|
||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||
from django.template.response import SimpleTemplateResponse
|
||||
from django.utils import six
|
||||
|
||||
|
@ -114,7 +115,7 @@ class Response(SimpleTemplateResponse):
|
|||
"""
|
||||
# TODO: Deprecate and use a template tag instead
|
||||
# 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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
# 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
|
||||
|
||||
|
||||
|
|
|
@ -104,6 +104,7 @@ HTTP_417_EXPECTATION_FAILED = 417
|
|||
HTTP_428_PRECONDITION_REQUIRED = 428
|
||||
HTTP_429_TOO_MANY_REQUESTS = 429
|
||||
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
||||
HTTP_451_BLOCKED = 451
|
||||
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
||||
HTTP_501_NOT_IMPLEMENTED = 501
|
||||
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.db.models import signals
|
||||
|
||||
from . import signal_handlers as handlers
|
||||
|
||||
|
||||
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_delete.connect(handlers.on_delete_any_model, dispatch_uid="events_delete")
|
||||
|
||||
|
||||
def disconnect_events_signals():
|
||||
from . import signal_handlers as handlers
|
||||
signals.post_save.disconnect(dispatch_uid="events_change")
|
||||
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.tasks.models import Task
|
||||
from taiga.projects.serializers import ProjectSerializer
|
||||
from taiga.users import services as users_service
|
||||
|
||||
from . import mixins
|
||||
from . import serializers
|
||||
|
@ -90,6 +91,14 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
|||
data = request.DATA.copy()
|
||||
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
|
||||
project_serialized = service.store_project(data)
|
||||
|
||||
|
@ -106,11 +115,19 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
|||
|
||||
# Create memberships
|
||||
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)
|
||||
|
||||
try:
|
||||
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()
|
||||
except Membership.DoesNotExist:
|
||||
Membership.objects.create(
|
||||
|
@ -118,7 +135,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
|||
email=project_serialized.object.owner.email,
|
||||
user=project_serialized.object.owner,
|
||||
role=project_serialized.object.roles.all().first(),
|
||||
is_owner=True
|
||||
is_admin=True
|
||||
)
|
||||
|
||||
# Create project values choicess
|
||||
|
@ -202,6 +219,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
|
|||
|
||||
try:
|
||||
dump = json.load(reader(dump))
|
||||
is_private = dump.get("is_private", False)
|
||||
except Exception:
|
||||
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():
|
||||
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:
|
||||
task = tasks.load_project_dump.delay(request.user, dump)
|
||||
task = tasks.load_project_dump.delay(user, dump)
|
||||
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
|
||||
return response.Created(response_data)
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
|
||||
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 service
|
||||
|
@ -89,7 +90,15 @@ def store_tags_colors(project, data):
|
|||
|
||||
def dict_to_project(data, owner=None):
|
||||
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)
|
||||
|
||||
|
@ -138,7 +147,7 @@ def dict_to_project(data, owner=None):
|
|||
email=proj.owner.email,
|
||||
user=proj.owner,
|
||||
role=proj.roles.all().first(),
|
||||
is_owner=True
|
||||
is_admin=True
|
||||
)
|
||||
|
||||
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.dump_service import dict_to_project, TaigaImportError
|
||||
from taiga.export_import.service import get_errors
|
||||
from taiga.users.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -58,7 +59,9 @@ class Command(BaseCommand):
|
|||
except Project.DoesNotExist:
|
||||
pass
|
||||
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:
|
||||
print("ERROR:", end=" ")
|
||||
print(e.message)
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
|
||||
|
||||
from taiga.base.api.permissions import (TaigaResourcePermission,
|
||||
IsProjectOwner, IsAuthenticated)
|
||||
IsProjectAdmin, IsAuthenticated)
|
||||
|
||||
|
||||
class ImportExportPermission(TaigaResourcePermission):
|
||||
import_project_perms = IsAuthenticated()
|
||||
import_item_perms = IsProjectOwner()
|
||||
export_project_perms = IsProjectOwner()
|
||||
import_item_perms = IsProjectAdmin()
|
||||
export_project_perms = IsProjectAdmin()
|
||||
load_dump_perms = IsAuthenticated()
|
||||
|
|
|
@ -21,6 +21,7 @@ import os
|
|||
from collections import OrderedDict
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
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 taiga import mdrender
|
||||
from taiga.base.api import serializers
|
||||
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.custom_attributes import models as custom_attributes_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):
|
||||
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):
|
||||
|
@ -263,7 +264,7 @@ class WatcheableObjectModelSerializer(serializers.ModelSerializer):
|
|||
adding_watcher_emails = list(new_watcher_emails.difference(old_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)
|
||||
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
|
||||
def load_project_dump(user, dump):
|
||||
try:
|
||||
project = dict_to_project(dump, user.email)
|
||||
project = dict_to_project(dump, user)
|
||||
except Exception:
|
||||
ctx = {
|
||||
"user": user,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "emails/base-body-html.jinja" %}
|
||||
|
||||
{% 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>
|
||||
<p>Hello {{ user }},</p>
|
||||
<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 }},
|
||||
|
||||
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" %}
|
||||
|
||||
{% 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>
|
||||
<p>Hello {{ user }},</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 }},
|
||||
|
||||
{{ 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" %}
|
||||
|
||||
{% 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>
|
||||
<p>Hello {{ user }},</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 }},
|
||||
|
||||
{{ error_message }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "emails/base-body-html.jinja" %}
|
||||
|
||||
{% 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>
|
||||
<p>Hello {{ user }},</p>
|
||||
<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 }},
|
||||
|
||||
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.urls import include, url
|
||||
|
||||
from .routers import router
|
||||
|
||||
|
||||
class FeedbackAppConfig(AppConfig):
|
||||
name = "taiga.feedback"
|
||||
|
@ -30,4 +28,5 @@ class FeedbackAppConfig(AppConfig):
|
|||
def ready(self):
|
||||
if settings.FEEDBACK_ENABLED:
|
||||
from taiga.urls import urlpatterns
|
||||
from .routers import router
|
||||
urlpatterns.append(url(r'^api/v1/', include(router.urls)))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "emails/base-body-html.jinja" %}
|
||||
|
||||
{% 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>
|
||||
<p>Taiga has received feedback from {{ full_name }} <{{ email }}></p>
|
||||
{% 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 }}>
|
||||
---------
|
||||
- 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 }}>
|
||||
{% endtrans %}
|
||||
|
|
|
@ -32,6 +32,9 @@ class IssuesSitemap(Sitemap):
|
|||
Q(project__is_private=True,
|
||||
project__anon_permissions__contains=["view_issues"]))
|
||||
|
||||
# Exclude blocked projects
|
||||
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||
|
||||
# Project data is needed
|
||||
queryset = queryset.select_related("project")
|
||||
|
||||
|
|
|
@ -34,6 +34,9 @@ class MilestonesSitemap(Sitemap):
|
|||
"view_us",
|
||||
"view_tasks"]))
|
||||
|
||||
# Exclude blocked projects
|
||||
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||
|
||||
# Project data is needed
|
||||
queryset = queryset.select_related("project")
|
||||
|
||||
|
|
|
@ -32,6 +32,9 @@ class ProjectsSitemap(Sitemap):
|
|||
Q(is_private=True,
|
||||
anon_permissions__contains=["view_project"]))
|
||||
|
||||
# Exclude blocked projects
|
||||
queryset = queryset.filter(blocked_code__isnull=True)
|
||||
|
||||
return queryset
|
||||
|
||||
def location(self, obj):
|
||||
|
|
|
@ -32,6 +32,9 @@ class TasksSitemap(Sitemap):
|
|||
Q(project__is_private=True,
|
||||
project__anon_permissions__contains=["view_tasks"]))
|
||||
|
||||
# Exclude blocked projects
|
||||
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||
|
||||
# Project data is needed
|
||||
queryset = queryset.select_related("project")
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from taiga.front.templatetags.functions import resolve
|
||||
|
||||
|
@ -24,7 +25,7 @@ from .base import Sitemap
|
|||
|
||||
class UsersSitemap(Sitemap):
|
||||
def items(self):
|
||||
user_model = apps.get_model("users", "User")
|
||||
user_model = get_user_model()
|
||||
|
||||
# Only active users and not system users
|
||||
queryset = user_model.objects.filter(is_active=True,
|
||||
|
|
|
@ -32,6 +32,9 @@ class UserStoriesSitemap(Sitemap):
|
|||
Q(project__is_private=True,
|
||||
project__anon_permissions__contains=["view_us"]))
|
||||
|
||||
# Exclude blocked projects
|
||||
queryset = queryset.filter(project__blocked_code__isnull=True)
|
||||
|
||||
# Project data is needed
|
||||
queryset = queryset.select_related("project")
|
||||
|
||||
|
|
|
@ -32,6 +32,9 @@ class WikiPagesSitemap(Sitemap):
|
|||
Q(project__is_private=True,
|
||||
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
|
||||
queryset = queryset.exclude(project__is_wiki_activated=False)
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ urls = {
|
|||
|
||||
"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):
|
||||
raise exc.BadRequest(_("Bad signature"))
|
||||
|
||||
if project.blocked_code is not None:
|
||||
raise exc.Blocked(_("Blocked element"))
|
||||
|
||||
event_name = self._get_event_name(request)
|
||||
|
||||
payload = self._get_payload(request)
|
||||
|
|
|
@ -40,19 +40,16 @@ class PushEventHook(BaseEventHook):
|
|||
|
||||
changes = self.payload.get("push", {}).get('changes', [])
|
||||
for change in filter(None, changes):
|
||||
new = change.get("new", None)
|
||||
if not new:
|
||||
commits = change.get("commits", [])
|
||||
if not commits:
|
||||
continue
|
||||
|
||||
target = new.get("target", None)
|
||||
if not target:
|
||||
continue
|
||||
for commit in commits:
|
||||
message = commit.get("message", None)
|
||||
if not message:
|
||||
continue
|
||||
|
||||
message = target.get("message", None)
|
||||
if not message:
|
||||
continue
|
||||
|
||||
self._process_message(message, None)
|
||||
self._process_message(message, None)
|
||||
|
||||
def _process_message(self, message, bitbucket_user):
|
||||
"""
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from taiga.users.models import User
|
||||
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):
|
||||
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
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from taiga.users.models import User
|
||||
from taiga.users.models import AuthData
|
||||
from taiga.base.utils.urls import get_absolute_url
|
||||
|
||||
|
@ -49,6 +49,6 @@ def get_github_user(github_id):
|
|||
pass
|
||||
|
||||
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
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from taiga.users.models import User
|
||||
from taiga.base.utils.urls import get_absolute_url
|
||||
|
||||
|
||||
|
@ -47,11 +47,11 @@ def get_gitlab_user(user_email):
|
|||
|
||||
if user_email:
|
||||
try:
|
||||
user = User.objects.get(email=user_email)
|
||||
except User.DoesNotExist:
|
||||
user = get_user_model().objects.get(email=user_email)
|
||||
except get_user_model().DoesNotExist:
|
||||
pass
|
||||
|
||||
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
|
||||
|
|
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,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.inlinepatterns import Pattern
|
||||
from markdown.util import etree, AtomicString
|
||||
|
||||
from taiga.users.models import User
|
||||
|
||||
|
||||
class MentionsExtension(Extension):
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
|
@ -43,8 +42,8 @@ class MentionsPattern(Pattern):
|
|||
username = m.group(3)
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
user = get_user_model().objects.get(username=username)
|
||||
except get_user_model().DoesNotExist:
|
||||
return "@{}".format(username)
|
||||
|
||||
url = "/profile/{}".format(username)
|
||||
|
|
|
@ -7,85 +7,85 @@
|
|||
# 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 markdown import Extension
|
||||
from markdown.inlinepatterns import Pattern
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
|
||||
from markdown.util import etree
|
||||
|
||||
from taiga.front.templatetags.functions import resolve
|
||||
from taiga.base.utils.slug import slugify
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class WikiLinkExtension(Extension):
|
||||
def __init__(self, project, *args, **kwargs):
|
||||
self.project = project
|
||||
return super().__init__(*args, **kwargs)
|
||||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]"
|
||||
md.inlinePatterns.add("wikilinks",
|
||||
WikiLinksPattern(md, WIKILINK_RE, self.project),
|
||||
"<not_strong")
|
||||
md.treeprocessors.add("relative_to_absolute_links",
|
||||
RelativeLinksTreeprocessor(md, self.project),
|
||||
"<prettify")
|
||||
|
||||
|
||||
class WikiLinksPattern(Pattern):
|
||||
def __init__(self, md, pattern, project):
|
||||
self.project = project
|
||||
self.md = md
|
||||
super().__init__(pattern)
|
||||
|
||||
def handleMatch(self, m):
|
||||
label = m.group(2).strip()
|
||||
url = resolve("wiki", self.project.slug, slugify(label))
|
||||
|
||||
if m.group(3):
|
||||
title = m.group(3).strip()[1:]
|
||||
else:
|
||||
title = label
|
||||
|
||||
a = etree.Element("a")
|
||||
a.text = title
|
||||
a.set("href", url)
|
||||
a.set("title", title)
|
||||
a.set("class", "reference wiki")
|
||||
return a
|
||||
|
||||
|
||||
SLUG_RE = re.compile(r"^[-a-zA-Z0-9_]+$")
|
||||
|
||||
|
||||
class RelativeLinksTreeprocessor(Treeprocessor):
|
||||
def __init__(self, md, project):
|
||||
self.project = project
|
||||
super().__init__(md)
|
||||
|
||||
def run(self, root):
|
||||
links = root.getiterator("a")
|
||||
for a in links:
|
||||
href = a.get("href", "")
|
||||
|
||||
if SLUG_RE.search(href):
|
||||
# [wiki](wiki_page) -> <a href="FRONT_HOST/.../wiki/wiki_page" ...
|
||||
url = resolve("wiki", self.project.slug, href)
|
||||
a.set("href", url)
|
||||
a.set("class", "reference wiki")
|
||||
|
||||
elif href and href[0] == "/":
|
||||
# [some link](/some/link) -> <a href="FRONT_HOST/some/link" ...
|
||||
url = "{}{}".format(resolve("home"), href[1:])
|
||||
a.set("href", url)
|
||||
|
||||
from markdown import Extension
|
||||
from markdown.inlinepatterns import Pattern
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
|
||||
from markdown.util import etree
|
||||
|
||||
from taiga.front.templatetags.functions import resolve
|
||||
from taiga.base.utils.slug import slugify
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class WikiLinkExtension(Extension):
|
||||
def __init__(self, project, *args, **kwargs):
|
||||
self.project = project
|
||||
return super().__init__(*args, **kwargs)
|
||||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]"
|
||||
md.inlinePatterns.add("wikilinks",
|
||||
WikiLinksPattern(md, WIKILINK_RE, self.project),
|
||||
"<not_strong")
|
||||
md.treeprocessors.add("relative_to_absolute_links",
|
||||
RelativeLinksTreeprocessor(md, self.project),
|
||||
"<prettify")
|
||||
|
||||
|
||||
class WikiLinksPattern(Pattern):
|
||||
def __init__(self, md, pattern, project):
|
||||
self.project = project
|
||||
self.md = md
|
||||
super().__init__(pattern)
|
||||
|
||||
def handleMatch(self, m):
|
||||
label = m.group(2).strip()
|
||||
url = resolve("wiki", self.project.slug, slugify(label))
|
||||
|
||||
if m.group(3):
|
||||
title = m.group(3).strip()[1:]
|
||||
else:
|
||||
title = label
|
||||
|
||||
a = etree.Element("a")
|
||||
a.text = title
|
||||
a.set("href", url)
|
||||
a.set("title", title)
|
||||
a.set("class", "reference wiki")
|
||||
return a
|
||||
|
||||
|
||||
SLUG_RE = re.compile(r"^[-a-zA-Z0-9_]+$")
|
||||
|
||||
|
||||
class RelativeLinksTreeprocessor(Treeprocessor):
|
||||
def __init__(self, md, project):
|
||||
self.project = project
|
||||
super().__init__(md)
|
||||
|
||||
def run(self, root):
|
||||
links = root.getiterator("a")
|
||||
for a in links:
|
||||
href = a.get("href", "")
|
||||
|
||||
if SLUG_RE.search(href):
|
||||
# [wiki](wiki_page) -> <a href="FRONT_HOST/.../wiki/wiki_page" ...
|
||||
url = resolve("wiki", self.project.slug, href)
|
||||
a.set("href", url)
|
||||
a.set("class", "reference wiki")
|
||||
|
||||
elif href and href[0] == "/":
|
||||
# [some link](/some/link) -> <a href="FRONT_HOST/some/link" ...
|
||||
url = "{}{}".format(resolve("home"), href[1:])
|
||||
a.set("href", url)
|
||||
|
|
|
@ -144,4 +144,5 @@ def get_diff_of_htmls(html1, html2):
|
|||
diffutil.diff_cleanupSemantic(diffs)
|
||||
return diffutil.diff_pretty_html(diffs)
|
||||
|
||||
|
||||
__all__ = ["render", "get_diff_of_htmls", "render_and_extract"]
|
||||
|
|
|
@ -82,7 +82,7 @@ MEMBERS_PERMISSIONS = [
|
|||
('delete_wiki_link', _('Delete wiki link')),
|
||||
]
|
||||
|
||||
OWNERS_PERMISSIONS = [
|
||||
ADMINS_PERMISSIONS = [
|
||||
('modify_project', _('Modify project')),
|
||||
('add_member', _('Add member')),
|
||||
('remove_member', _('Remove member')),
|
||||
|
|
|
@ -16,12 +16,11 @@
|
|||
# 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 .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
|
||||
|
||||
def _get_user_project_membership(user, project):
|
||||
Membership = apps.get_model("projects", "Membership")
|
||||
if user.is_anonymous():
|
||||
return None
|
||||
|
||||
|
@ -39,10 +38,17 @@ def _get_object_project(obj):
|
|||
|
||||
|
||||
def is_project_owner(user, obj):
|
||||
"""
|
||||
The owner attribute of a project is just an historical reference
|
||||
"""
|
||||
project = _get_object_project(obj)
|
||||
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:
|
||||
return True
|
||||
|
||||
|
@ -51,7 +57,7 @@ def is_project_owner(user, obj):
|
|||
return False
|
||||
|
||||
membership = _get_user_project_membership(user, project)
|
||||
if membership and membership.is_owner:
|
||||
if membership and membership.is_admin:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -79,43 +85,41 @@ def _get_membership_permissions(membership):
|
|||
def get_user_project_permissions(user, project):
|
||||
membership = _get_user_project_membership(user, project)
|
||||
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))
|
||||
public_permissions = list(map(lambda perm: perm[0], USER_PERMISSIONS))
|
||||
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
|
||||
elif membership:
|
||||
if membership.is_owner:
|
||||
owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS))
|
||||
if membership.is_admin:
|
||||
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
|
||||
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
|
||||
else:
|
||||
owner_permissions = []
|
||||
admins_permissions = []
|
||||
members_permissions = []
|
||||
members_permissions = members_permissions + _get_membership_permissions(membership)
|
||||
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 []
|
||||
elif user.is_authenticated():
|
||||
owner_permissions = []
|
||||
admins_permissions = []
|
||||
members_permissions = []
|
||||
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 []
|
||||
else:
|
||||
owner_permissions = []
|
||||
admins_permissions = []
|
||||
members_permissions = []
|
||||
public_permissions = []
|
||||
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):
|
||||
if project.is_private:
|
||||
project.anon_permissions = []
|
||||
project.public_permissions = []
|
||||
|
||||
else:
|
||||
"""
|
||||
If a project is public anonymous and registered users should have at least visualization permissions
|
||||
"""
|
||||
# If a project is public anonymous and registered users should have at
|
||||
# least visualization permissions.
|
||||
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
|
||||
project.anon_permissions = list(set((project.anon_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.utils.translation import ugettext as _
|
||||
from django.utils import timezone
|
||||
from django.http import Http404
|
||||
|
||||
from taiga.base import filters
|
||||
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 detail_route
|
||||
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.utils import get_object_or_404
|
||||
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.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
||||
from taiga.permissions import service as permissions_service
|
||||
from taiga.users import services as users_service
|
||||
|
||||
from . import filters as project_filters
|
||||
from . import models
|
||||
|
@ -61,7 +64,9 @@ from . import services
|
|||
######################################################
|
||||
## Project
|
||||
######################################################
|
||||
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet):
|
||||
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||
BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
|
||||
|
||||
queryset = models.Project.objects.all()
|
||||
serializer_class = serializers.ProjectDetailSerializer
|
||||
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
||||
|
@ -88,6 +93,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
"total_activity_last_month",
|
||||
"total_activity_last_year")
|
||||
|
||||
def is_blocked(self, obj):
|
||||
return obj.blocked_code is not None
|
||||
|
||||
def _get_order_by_field_name(self):
|
||||
order_by_query_param = project_filters.CanViewProjectObjFilterBackend.order_by_query_param
|
||||
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):
|
||||
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)
|
||||
# so we add some custom prefetching
|
||||
qs = qs.prefetch_related("members")
|
||||
qs = qs.prefetch_related("memberships")
|
||||
qs = qs.prefetch_related(Prefetch("notify_policies",
|
||||
NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
|
||||
|
||||
|
@ -137,7 +147,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
else:
|
||||
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
|
||||
|
||||
return serializer_class
|
||||
|
@ -158,6 +168,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
except Exception:
|
||||
raise exc.WrongArguments(_("Invalid image format"))
|
||||
|
||||
self.pre_conditions_on_save(self.object)
|
||||
|
||||
self.object.logo = 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.check_permissions(request, "remove_logo", self.object)
|
||||
|
||||
self.pre_conditions_on_save(self.object)
|
||||
self.object.logo = None
|
||||
self.object.save(update_fields=["logo"])
|
||||
|
||||
|
@ -182,6 +194,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
def watch(self, request, pk=None):
|
||||
project = self.get_object()
|
||||
self.check_permissions(request, "watch", project)
|
||||
self.pre_conditions_on_save(project)
|
||||
notify_level = request.DATA.get("notify_level", NotifyLevel.involved)
|
||||
project.add_watcher(self.request.user, notify_level=notify_level)
|
||||
return response.Ok()
|
||||
|
@ -190,6 +203,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
def unwatch(self, request, pk=None):
|
||||
project = self.get_object()
|
||||
self.check_permissions(request, "unwatch", project)
|
||||
self.pre_conditions_on_save(project)
|
||||
user = self.request.user
|
||||
project.remove_watcher(user)
|
||||
return response.Ok()
|
||||
|
@ -207,77 +221,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
services.update_projects_order_in_bulk(data, "user_order", request.user)
|
||||
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"])
|
||||
def create_template(self, request, **kwargs):
|
||||
template_name = request.DATA.get('template_name', None)
|
||||
|
@ -305,13 +248,161 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
template.save()
|
||||
return response.Created(serializers.ProjectTemplateSerializer(template).data)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=['POST'])
|
||||
def leave(self, request, pk=None):
|
||||
project = self.get_object()
|
||||
self.check_permissions(request, 'leave', project)
|
||||
self.pre_conditions_on_save(project)
|
||||
services.remove_user_from_project(request.user, project)
|
||||
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):
|
||||
update_permissions = False
|
||||
if not obj.id:
|
||||
|
@ -329,9 +420,15 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
def pre_save(self, obj):
|
||||
if not obj.id:
|
||||
obj.owner = self.request.user
|
||||
# TODO REFACTOR THIS
|
||||
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)
|
||||
super().pre_save(obj)
|
||||
|
||||
|
@ -342,10 +439,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
|
|||
if obj is None:
|
||||
raise Http404
|
||||
|
||||
obj.delete_related_content()
|
||||
|
||||
self.pre_delete(obj)
|
||||
self.pre_conditions_on_delete(obj)
|
||||
obj.delete_related_content()
|
||||
obj.delete()
|
||||
self.post_delete(obj)
|
||||
return response.NoContent()
|
||||
|
@ -365,7 +461,9 @@ class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
|
|||
## Custom values for selectors
|
||||
######################################################
|
||||
|
||||
class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
|
||||
model = models.Points
|
||||
serializer_class = serializers.PointsSerializer
|
||||
permission_classes = (permissions.PointsPermission,)
|
||||
|
@ -379,7 +477,9 @@ class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
|||
move_on_destroy_project_default_field = "default_points"
|
||||
|
||||
|
||||
class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
|
||||
model = models.UserStoryStatus
|
||||
serializer_class = serializers.UserStoryStatusSerializer
|
||||
permission_classes = (permissions.UserStoryStatusPermission,)
|
||||
|
@ -393,7 +493,9 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrd
|
|||
move_on_destroy_project_default_field = "default_us_status"
|
||||
|
||||
|
||||
class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
|
||||
model = models.TaskStatus
|
||||
serializer_class = serializers.TaskStatusSerializer
|
||||
permission_classes = (permissions.TaskStatusPermission,)
|
||||
|
@ -407,7 +509,9 @@ class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMix
|
|||
move_on_destroy_project_default_field = "default_task_status"
|
||||
|
||||
|
||||
class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
|
||||
model = models.Severity
|
||||
serializer_class = serializers.SeveritySerializer
|
||||
permission_classes = (permissions.SeverityPermission,)
|
||||
|
@ -421,7 +525,8 @@ class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin
|
|||
move_on_destroy_project_default_field = "default_severity"
|
||||
|
||||
|
||||
class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
model = models.Priority
|
||||
serializer_class = serializers.PrioritySerializer
|
||||
permission_classes = (permissions.PriorityPermission,)
|
||||
|
@ -435,7 +540,8 @@ class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin
|
|||
move_on_destroy_project_default_field = "default_priority"
|
||||
|
||||
|
||||
class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
model = models.IssueType
|
||||
serializer_class = serializers.IssueTypeSerializer
|
||||
permission_classes = (permissions.IssueTypePermission,)
|
||||
|
@ -449,7 +555,8 @@ class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixi
|
|||
move_on_destroy_project_default_field = "default_issue_type"
|
||||
|
||||
|
||||
class IssueStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||
model = models.IssueStatus
|
||||
serializer_class = serializers.IssueStatusSerializer
|
||||
permission_classes = (permissions.IssueStatusPermission,)
|
||||
|
@ -480,7 +587,7 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
|
|||
## Members & Invitations
|
||||
######################################################
|
||||
|
||||
class MembershipViewSet(ModelCrudViewSet):
|
||||
class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||
model = models.Membership
|
||||
admin_serializer_class = serializers.MembershipAdminSerializer
|
||||
serializer_class = serializers.MembershipSerializer
|
||||
|
@ -490,17 +597,17 @@ class MembershipViewSet(ModelCrudViewSet):
|
|||
|
||||
def get_serializer_class(self):
|
||||
use_admin_serializer = False
|
||||
|
||||
|
||||
if self.action == "create":
|
||||
use_admin_serializer = True
|
||||
|
||||
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)
|
||||
if self.action == "list" and project_id is not None:
|
||||
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:
|
||||
return self.admin_serializer_class
|
||||
|
@ -518,10 +625,22 @@ class MembershipViewSet(ModelCrudViewSet):
|
|||
project = models.Project.objects.get(id=data["project_id"])
|
||||
invitation_extra_text = data.get("invitation_extra_text", None)
|
||||
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
|
||||
# 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:
|
||||
members = services.create_members_in_bulk(data["bulk_memberships"],
|
||||
project=project,
|
||||
|
@ -539,15 +658,26 @@ class MembershipViewSet(ModelCrudViewSet):
|
|||
invitation = self.get_object()
|
||||
|
||||
self.check_permissions(request, 'resend_invitation', invitation.project)
|
||||
self.pre_conditions_on_save(invitation)
|
||||
|
||||
services.send_invitation(invitation=invitation)
|
||||
return response.NoContent()
|
||||
|
||||
def pre_delete(self, obj):
|
||||
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):
|
||||
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:
|
||||
obj.token = str(uuid.uuid1())
|
||||
|
||||
|
|
|
@ -19,12 +19,11 @@ from django.apps import AppConfig
|
|||
from django.apps import apps
|
||||
from django.db.models import signals
|
||||
|
||||
from . import signals as handlers
|
||||
|
||||
|
||||
## Project Signals
|
||||
|
||||
def connect_projects_signals():
|
||||
from . import signals as handlers
|
||||
# On project object is created apply template.
|
||||
signals.post_save.connect(handlers.project_post_save,
|
||||
sender=apps.get_model("projects", "Project"),
|
||||
|
@ -51,6 +50,7 @@ def disconnect_projects_signals():
|
|||
## Memberships Signals
|
||||
|
||||
def connect_memberships_signals():
|
||||
from . import signals as handlers
|
||||
# On membership object is deleted, update role-points relation.
|
||||
signals.pre_delete.connect(handlers.membership_post_delete,
|
||||
sender=apps.get_model("projects", "Membership"),
|
||||
|
@ -71,6 +71,7 @@ def disconnect_memberships_signals():
|
|||
## US Statuses 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,
|
||||
sender=apps.get_model("projects", "UserStoryStatus"),
|
||||
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
|
||||
|
||||
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,
|
||||
sender=apps.get_model("projects", "TaskStatus"),
|
||||
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/>.
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.admin import GenericTabularInline
|
||||
|
||||
from . import models
|
||||
|
||||
|
@ -38,7 +38,7 @@ class AttachmentAdmin(admin.ModelAdmin):
|
|||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
|
||||
class AttachmentInline(generic.GenericTabularInline):
|
||||
class AttachmentInline(GenericTabularInline):
|
||||
model = models.Attachment
|
||||
fields = ("attached_file", "owner")
|
||||
extra = 0
|
||||
|
|
|
@ -25,6 +25,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from taiga.base import filters
|
||||
from taiga.base import exceptions as exc
|
||||
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.projects.notifications.mixins import WatchedResourceMixin
|
||||
|
@ -35,7 +36,9 @@ from . import serializers
|
|||
from . import models
|
||||
|
||||
|
||||
class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
|
||||
class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||
BlockedByProjectMixin, ModelCrudViewSet):
|
||||
|
||||
model = models.Attachment
|
||||
serializer_class = serializers.AttachmentSerializer
|
||||
filter_fields = ["project", "object_id"]
|
||||
|
|
|
@ -20,7 +20,7 @@ import hashlib
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
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.translation import ugettext_lazy as _
|
||||
from django.utils.text import get_valid_filename
|
||||
|
@ -42,7 +42,7 @@ class Attachment(models.Model):
|
|||
verbose_name=_("content type"))
|
||||
object_id = models.PositiveIntegerField(null=False, blank=False,
|
||||
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,
|
||||
verbose_name=_("created date"),
|
||||
default=timezone.now)
|
||||
|
|
|
@ -24,3 +24,12 @@ VIDEOCONFERENCES_CHOICES = (
|
|||
("custom", _("Custom")),
|
||||
("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 ModelUpdateRetrieveViewSet
|
||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||
from taiga.base import exceptions as exc
|
||||
from taiga.base import filters
|
||||
from taiga.base import response
|
||||
|
@ -38,7 +39,7 @@ from . import services
|
|||
# Custom Attribute ViewSets
|
||||
#######################################################
|
||||
|
||||
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
||||
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
model = models.UserStoryCustomAttribute
|
||||
serializer_class = serializers.UserStoryCustomAttributeSerializer
|
||||
permission_classes = (permissions.UserStoryCustomAttributePermission,)
|
||||
|
@ -49,7 +50,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
|||
bulk_update_order_action = services.bulk_update_userstory_custom_attribute_order
|
||||
|
||||
|
||||
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
||||
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
model = models.TaskCustomAttribute
|
||||
serializer_class = serializers.TaskCustomAttributeSerializer
|
||||
permission_classes = (permissions.TaskCustomAttributePermission,)
|
||||
|
@ -60,7 +61,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
|||
bulk_update_order_action = services.bulk_update_task_custom_attribute_order
|
||||
|
||||
|
||||
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
||||
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
model = models.IssueCustomAttribute
|
||||
serializer_class = serializers.IssueCustomAttributeSerializer
|
||||
permission_classes = (permissions.IssueCustomAttributePermission,)
|
||||
|
@ -76,7 +77,7 @@ class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
|
|||
#######################################################
|
||||
|
||||
class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||
ModelUpdateRetrieveViewSet):
|
||||
BlockedByProjectMixin, ModelUpdateRetrieveViewSet):
|
||||
def get_object_for_snapshot(self, obj):
|
||||
return getattr(obj, self.content_object)
|
||||
|
||||
|
|
|
@ -21,9 +21,11 @@ from django.utils.translation import ugettext_lazy as _
|
|||
TEXT_TYPE = "text"
|
||||
MULTILINE_TYPE = "multiline"
|
||||
DATE_TYPE = "date"
|
||||
URL_TYPE = "url"
|
||||
|
||||
TYPES_CHOICES = (
|
||||
(TEXT_TYPE, _("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 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 IsSuperUser
|
||||
|
||||
|
@ -27,39 +27,39 @@ from taiga.base.api.permissions import IsSuperUser
|
|||
#######################################################
|
||||
|
||||
class UserStoryCustomAttributePermission(TaigaResourcePermission):
|
||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
||||
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||
global_perms = None
|
||||
retrieve_perms = HasProjectPerm('view_project')
|
||||
create_perms = IsProjectOwner()
|
||||
update_perms = IsProjectOwner()
|
||||
partial_update_perms = IsProjectOwner()
|
||||
destroy_perms = IsProjectOwner()
|
||||
create_perms = IsProjectAdmin()
|
||||
update_perms = IsProjectAdmin()
|
||||
partial_update_perms = IsProjectAdmin()
|
||||
destroy_perms = IsProjectAdmin()
|
||||
list_perms = AllowAny()
|
||||
bulk_update_order_perms = IsProjectOwner()
|
||||
bulk_update_order_perms = IsProjectAdmin()
|
||||
|
||||
|
||||
class TaskCustomAttributePermission(TaigaResourcePermission):
|
||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
||||
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||
global_perms = None
|
||||
retrieve_perms = HasProjectPerm('view_project')
|
||||
create_perms = IsProjectOwner()
|
||||
update_perms = IsProjectOwner()
|
||||
partial_update_perms = IsProjectOwner()
|
||||
destroy_perms = IsProjectOwner()
|
||||
create_perms = IsProjectAdmin()
|
||||
update_perms = IsProjectAdmin()
|
||||
partial_update_perms = IsProjectAdmin()
|
||||
destroy_perms = IsProjectAdmin()
|
||||
list_perms = AllowAny()
|
||||
bulk_update_order_perms = IsProjectOwner()
|
||||
bulk_update_order_perms = IsProjectAdmin()
|
||||
|
||||
|
||||
class IssueCustomAttributePermission(TaigaResourcePermission):
|
||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
||||
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||
global_perms = None
|
||||
retrieve_perms = HasProjectPerm('view_project')
|
||||
create_perms = IsProjectOwner()
|
||||
update_perms = IsProjectOwner()
|
||||
partial_update_perms = IsProjectOwner()
|
||||
destroy_perms = IsProjectOwner()
|
||||
create_perms = IsProjectAdmin()
|
||||
update_perms = IsProjectAdmin()
|
||||
partial_update_perms = IsProjectAdmin()
|
||||
destroy_perms = IsProjectAdmin()
|
||||
list_perms = AllowAny()
|
||||
bulk_update_order_perms = IsProjectOwner()
|
||||
bulk_update_order_perms = IsProjectAdmin()
|
||||
|
||||
|
||||
######################################################
|
||||
|
@ -67,7 +67,7 @@ class IssueCustomAttributePermission(TaigaResourcePermission):
|
|||
#######################################################
|
||||
|
||||
class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
|
||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
||||
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||
global_perms = None
|
||||
retrieve_perms = HasProjectPerm('view_us')
|
||||
update_perms = HasProjectPerm('modify_us')
|
||||
|
@ -75,7 +75,7 @@ class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
|
|||
|
||||
|
||||
class TaskCustomAttributesValuesPermission(TaigaResourcePermission):
|
||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
||||
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||
global_perms = None
|
||||
retrieve_perms = HasProjectPerm('view_tasks')
|
||||
update_perms = HasProjectPerm('modify_task')
|
||||
|
@ -83,7 +83,7 @@ class TaskCustomAttributesValuesPermission(TaigaResourcePermission):
|
|||
|
||||
|
||||
class IssueCustomAttributesValuesPermission(TaigaResourcePermission):
|
||||
enought_perms = IsProjectOwner() | IsSuperUser()
|
||||
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||
global_perms = None
|
||||
retrieve_perms = HasProjectPerm('view_issues')
|
||||
update_perms = HasProjectPerm('modify_issue')
|
||||
|
|
|
@ -37,7 +37,8 @@ class DiscoverModeFilterBackend(FilterBackend):
|
|||
|
||||
if discover_mode:
|
||||
# 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)
|
||||
|
||||
|
@ -70,7 +71,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
|
|||
if project_id:
|
||||
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||
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]
|
||||
|
||||
|
|
|
@ -19,10 +19,10 @@ from contextlib import suppress
|
|||
|
||||
from functools import partial
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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_dict
|
||||
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
|
||||
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)
|
||||
qs = user_model.objects.filter(pk__in=tuple(ids))
|
||||
|
||||
|
|
|
@ -14,12 +14,10 @@
|
|||
|
||||
import uuid
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
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.conf import settings
|
||||
from django_pgjson.fields import JsonField
|
||||
|
||||
from taiga.mdrender.service import get_diff_of_htmls
|
||||
|
@ -96,7 +94,7 @@ class HistoryEntry(models.Model):
|
|||
@cached_property
|
||||
def owner(self):
|
||||
pk = self.user["pk"]
|
||||
model = apps.get_model("users", "User")
|
||||
model = get_user_model()
|
||||
try:
|
||||
return model.objects.get(pk=pk)
|
||||
except model.DoesNotExist:
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
|
||||
IsProjectOwner, AllowAny,
|
||||
IsProjectAdmin, AllowAny,
|
||||
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
|
||||
|
||||
|
||||
|
@ -38,7 +38,7 @@ class IsCommentProjectOwner(PermissionComponent):
|
|||
model = get_model_from_key(obj.key)
|
||||
pk = get_pk_from_key(obj.key)
|
||||
project = model.objects.get(pk=pk)
|
||||
return is_project_owner(request.user, project)
|
||||
return is_project_admin(request.user, project)
|
||||
|
||||
class UserStoryHistoryPermission(TaigaResourcePermission):
|
||||
retrieve_perms = HasProjectPerm('view_project')
|
||||
|
|
|
@ -16,24 +16,21 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
|
||||
from taiga.base import filters
|
||||
from taiga.base import exceptions as exc
|
||||
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.mixins import BlockedByProjectMixin
|
||||
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.occ import OCCResourceMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
|
||||
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 . import models
|
||||
|
@ -43,7 +40,7 @@ from . import serializers
|
|||
|
||||
|
||||
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||
ModelCrudViewSet):
|
||||
BlockedByProjectMixin, ModelCrudViewSet):
|
||||
queryset = models.Issue.objects.all()
|
||||
permission_classes = (permissions.IssuePermission, )
|
||||
filter_backends = (filters.CanViewIssuesFilterBackend,
|
||||
|
@ -157,8 +154,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
|||
super().pre_save(obj)
|
||||
|
||||
def pre_conditions_on_save(self, obj):
|
||||
super().pre_conditions_on_save(obj)
|
||||
|
||||
if obj.milestone and obj.milestone.project != obj.project:
|
||||
raise exc.PermissionDenied(_("You don't have permissions to set this sprint "
|
||||
"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 "
|
||||
"to this issue."))
|
||||
|
||||
super().pre_conditions_on_save(obj)
|
||||
|
||||
@list_route(methods=["GET"])
|
||||
def by_ref(self, request):
|
||||
ref = request.QUERY_PARAMS.get("ref", None)
|
||||
|
@ -232,6 +229,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
|||
data = serializer.data
|
||||
project = Project.objects.get(pk=data["project_id"])
|
||||
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(
|
||||
data["bulk_issues"], project=project, owner=request.user,
|
||||
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.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():
|
||||
from taiga.projects import signals as generic_handlers
|
||||
from . import signals as handlers
|
||||
|
||||
# Finished date
|
||||
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
|
||||
sender=apps.get_model("issues", "Issue"),
|
||||
|
@ -43,6 +42,8 @@ def connect_issues_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,
|
||||
sender=apps.get_model("issues", "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/>.
|
||||
|
||||
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.utils import timezone
|
||||
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,
|
||||
default=None, related_name="issues_assigned_to_me",
|
||||
verbose_name=_("assigned to"))
|
||||
attachments = generic.GenericRelation("attachments.Attachment")
|
||||
attachments = GenericRelation("attachments.Attachment")
|
||||
external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
|
||||
_importing = None
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue