Adding max_private_projects and max_public_projects support
parent
273d94f347
commit
b8fd768d01
|
@ -524,6 +524,9 @@ FRONT_SITEMAP_CACHE_TIMEOUT = 24*60*60 # In second
|
||||||
|
|
||||||
EXTRA_BLOCKING_CODES = []
|
EXTRA_BLOCKING_CODES = []
|
||||||
|
|
||||||
|
MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit
|
||||||
|
MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit
|
||||||
|
|
||||||
from .sr import *
|
from .sr import *
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ from taiga.projects.tasks.models import Task
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
||||||
from taiga.permissions import service as permissions_service
|
from taiga.permissions import service as permissions_service
|
||||||
|
from taiga.users import services as users_service
|
||||||
|
|
||||||
from . import filters as project_filters
|
from . import filters as project_filters
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -342,9 +343,12 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
permissions_service.set_base_permissions_for_project(obj)
|
permissions_service.set_base_permissions_for_project(obj)
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
|
user = self.request.user
|
||||||
|
if not users_service.has_available_slot_for_project(user, is_private=obj.is_private):
|
||||||
|
raise exc.BadRequest(_("The user can't have more projects of this type"))
|
||||||
|
|
||||||
if not obj.id:
|
if not obj.id:
|
||||||
obj.owner = self.request.user
|
obj.owner = user
|
||||||
# TODO REFACTOR THIS
|
|
||||||
obj.template = self.request.QUERY_PARAMS.get('template', None)
|
obj.template = self.request.QUERY_PARAMS.get('template', None)
|
||||||
|
|
||||||
self._set_base_permissions(obj)
|
self._set_base_permissions(obj)
|
||||||
|
|
|
@ -50,7 +50,7 @@ class UserAdmin(DjangoUserAdmin):
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
(_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}),
|
(_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}),
|
||||||
(_('Extra info'), {'fields': ('color', 'lang', 'timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}),
|
(_('Extra info'), {'fields': ('color', 'lang', 'timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}),
|
||||||
(_('Permissions'), {'fields': ('is_active', 'is_superuser',)}),
|
(_('Permissions'), {'fields': ('is_active', 'is_superuser', 'max_private_projects', 'max_public_projects')}),
|
||||||
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||||
)
|
)
|
||||||
form = UserChangeForm
|
form = UserChangeForm
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0014_auto_20151005_1357'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='max_private_projects',
|
||||||
|
field=models.IntegerField(null=True, verbose_name='max number of private projects owned', default=None, blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='max_public_projects',
|
||||||
|
field=models.IntegerField(null=True, verbose_name='max number of public projects owned', default=None, blank=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -25,6 +25,7 @@ import uuid
|
||||||
from unidecode import unidecode
|
from unidecode import unidecode
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
@ -140,6 +141,11 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||||
new_email = models.EmailField(_('new email address'), null=True, blank=True)
|
new_email = models.EmailField(_('new email address'), null=True, blank=True)
|
||||||
|
|
||||||
is_system = models.BooleanField(null=False, blank=False, default=False)
|
is_system = models.BooleanField(null=False, blank=False, default=False)
|
||||||
|
|
||||||
|
|
||||||
|
max_private_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PRIVATE_PROJECTS_PER_USER, verbose_name='max number of private projects owned')
|
||||||
|
max_public_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PUBLIC_PROJECTS_PER_USER, verbose_name='max number of public projects owned')
|
||||||
|
|
||||||
_cached_memberships = None
|
_cached_memberships = None
|
||||||
_cached_liked_ids = None
|
_cached_liked_ids = None
|
||||||
_cached_watched_ids = None
|
_cached_watched_ids = None
|
||||||
|
|
|
@ -104,14 +104,27 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
return ContactProjectDetailSerializer(projects, many=True).data
|
return ContactProjectDetailSerializer(projects, many=True).data
|
||||||
|
|
||||||
class UserAdminSerializer(UserSerializer):
|
class UserAdminSerializer(UserSerializer):
|
||||||
|
total_private_projects = serializers.SerializerMethodField("get_total_private_projects")
|
||||||
|
total_public_projects = serializers.SerializerMethodField("get_total_public_projects")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
# IMPORTANT: Maintain the UserSerializer Meta up to date
|
# IMPORTANT: Maintain the UserSerializer Meta up to date
|
||||||
# with this info (including here the email)
|
# with this info (including here the email)
|
||||||
fields = ("id", "username", "full_name", "full_name_display", "email",
|
fields = ("id", "username", "full_name", "full_name_display", "email",
|
||||||
"color", "bio", "lang", "theme", "timezone", "is_active", "photo",
|
"color", "bio", "lang", "theme", "timezone", "is_active", "photo",
|
||||||
"big_photo")
|
"big_photo",
|
||||||
read_only_fields = ("id", "email")
|
"max_private_projects", "max_public_projects",
|
||||||
|
"total_private_projects", "total_public_projects")
|
||||||
|
|
||||||
|
read_only_fields = ("id", "email",
|
||||||
|
"max_private_projects", "max_public_projects")
|
||||||
|
|
||||||
|
def get_total_private_projects(self, user):
|
||||||
|
return user.owned_projects.filter(is_private=True).count()
|
||||||
|
|
||||||
|
def get_total_public_projects(self, user):
|
||||||
|
return user.owned_projects.filter(is_private=False).count()
|
||||||
|
|
||||||
|
|
||||||
class UserBasicInfoSerializer(UserSerializer):
|
class UserBasicInfoSerializer(UserSerializer):
|
||||||
|
|
|
@ -572,3 +572,16 @@ def get_voted_list(for_user, from_user, type=None, q=None):
|
||||||
dict(zip([col[0] for col in desc], row))
|
dict(zip([col[0] for col in desc], row))
|
||||||
for row in cursor.fetchall()
|
for row in cursor.fetchall()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def has_available_slot_for_project(user, is_private=False):
|
||||||
|
if is_private:
|
||||||
|
if user.max_private_projects is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return user.owned_projects.filter(is_private=True).count() < user.max_private_projects
|
||||||
|
|
||||||
|
if user.max_public_projects is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return user.owned_projects.filter(is_private=False).count() < user.max_public_projects
|
||||||
|
|
|
@ -43,6 +43,130 @@ def test_create_project(client):
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_private_project_without_enough_private_projects_slots(client):
|
||||||
|
user = f.create_user(max_private_projects=0)
|
||||||
|
url = reverse("projects-list")
|
||||||
|
data = {
|
||||||
|
"name": "project name",
|
||||||
|
"description": "project description",
|
||||||
|
"is_private": True
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "can't have more projects" in response.data["_error_message"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_public_project_without_enough_public_projects_slots(client):
|
||||||
|
user = f.create_user(max_public_projects=0)
|
||||||
|
url = reverse("projects-list")
|
||||||
|
data = {
|
||||||
|
"name": "project name",
|
||||||
|
"description": "project description",
|
||||||
|
"is_private": False
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "can't have more projects" in response.data["_error_message"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_project_from_private_to_public_without_enough_public_projects_slots(client):
|
||||||
|
project = f.create_project(is_private=True, owner__max_public_projects=0)
|
||||||
|
f.MembershipFactory(user=project.owner, project=project, is_owner=True)
|
||||||
|
url = reverse("projects-detail", kwargs={"pk": project.pk})
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"is_private": False
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "can't have more projects" in response.data["_error_message"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_project_from_public_to_private_without_enough_private_projects_slots(client):
|
||||||
|
project = f.create_project(is_private=False, owner__max_private_projects=0)
|
||||||
|
f.MembershipFactory(user=project.owner, project=project, is_owner=True)
|
||||||
|
url = reverse("projects-detail", kwargs={"pk": project.pk})
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"is_private": True
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "can't have more projects" in response.data["_error_message"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_private_project_with_enough_private_projects_slots(client):
|
||||||
|
user = f.create_user(max_private_projects=1)
|
||||||
|
url = reverse("projects-list")
|
||||||
|
data = {
|
||||||
|
"name": "project name",
|
||||||
|
"description": "project description",
|
||||||
|
"is_private": True
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_public_project_with_enough_public_projects_slots(client):
|
||||||
|
user = f.create_user(max_public_projects=1)
|
||||||
|
url = reverse("projects-list")
|
||||||
|
data = {
|
||||||
|
"name": "project name",
|
||||||
|
"description": "project description",
|
||||||
|
"is_private": False
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_project_from_private_to_public_with_enough_public_projects_slots(client):
|
||||||
|
project = f.create_project(is_private=True, owner__max_public_projects=1)
|
||||||
|
f.MembershipFactory(user=project.owner, project=project, is_owner=True)
|
||||||
|
url = reverse("projects-detail", kwargs={"pk": project.pk})
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"is_private": False
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_project_from_public_to_private_with_enough_private_projects_slots(client):
|
||||||
|
project = f.create_project(is_private=False, owner__max_private_projects=1)
|
||||||
|
f.MembershipFactory(user=project.owner, project=project, is_owner=True)
|
||||||
|
url = reverse("projects-detail", kwargs={"pk": project.pk})
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"is_private": True
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_partially_update_project(client):
|
def test_partially_update_project(client):
|
||||||
project = f.create_project()
|
project = f.create_project()
|
||||||
f.MembershipFactory(user=project.owner, project=project, is_owner=True)
|
f.MembershipFactory(user=project.owner, project=project, is_owner=True)
|
||||||
|
|
Loading…
Reference in New Issue