diff --git a/settings/common.py b/settings/common.py index e207f0578..c871c54c7 100644 --- a/settings/common.py +++ b/settings/common.py @@ -433,7 +433,9 @@ # 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" diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py index f81c14244..75b15bd2b 100644 --- a/taiga/base/exceptions.py +++ b/taiga/base/exceptions.py @@ -202,10 +202,27 @@ class NotAuthenticated(NotAuthenticated): 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 = _("Not enough slots for project.") + + 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 @@ -237,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) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index 9eadf5a2a..d74662763 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -97,7 +97,7 @@ def create(self, request, *args, **kwargs): project=Project(is_private=is_private, id=None) ) if not enough_slots: - raise exc.BadRequest(not_enough_slots_error) + raise exc.NotEnoughSlotsForProject(is_private, 1, not_enough_slots_error) # Create Project project_serialized = service.store_project(data) @@ -122,7 +122,7 @@ def create(self, request, *args, **kwargs): members=max(members, 1) ) if not enough_slots: - raise exc.BadRequest(not_enough_slots_error) + raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error) service.store_memberships(project_serialized.object, data) try: @@ -224,13 +224,6 @@ def load_dump(self, request): raise exc.WrongArguments(_("Invalid dump format")) user = request.user - (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( - user, - project=Project(is_private=is_private, id=None) - ) - if not enough_slots: - raise exc.BadRequest(not_enough_slots_error) - slug = dump.get('slug', None) if slug is not None and Project.objects.filter(slug=slug).exists(): del dump['slug'] @@ -242,7 +235,7 @@ def load_dump(self, request): members=max(members, 1) ) if not enough_slots: - raise exc.BadRequest(not_enough_slots_error) + raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error) if settings.CELERY_ENABLED: task = tasks.load_project_dump.delay(user, dump) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 6d057807c..955fec3e3 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -378,7 +378,7 @@ def transfer_accept(self, request, pk=None): members=members ) if not enough_slots: - raise exc.BadRequest(not_enough_slots_error) + 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) @@ -414,8 +414,9 @@ def _set_base_permissions(self, obj): def pre_save(self, obj): user = self.request.user (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(user, project=obj) + members = max(obj.memberships.count(), 1) if not enough_slots: - raise exc.BadRequest(not_enough_slots_error) + raise exc.NotEnoughSlotsForProject(obj.is_private, members, not_enough_slots_error) if not obj.id: obj.owner = user @@ -624,13 +625,14 @@ def bulk_create(self, request, **kwargs): # 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( request.user, project=project, - members=len(data["bulk_memberships"]) + members=members ) if not enough_slots: - raise exc.BadRequest(not_enough_slots_error) + raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error) try: members = services.create_members_in_bulk(data["bulk_memberships"], @@ -660,13 +662,14 @@ def pre_delete(self, obj): def pre_save(self, obj): if not obj.id: + members = 1 (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( self.request.user, project=obj.project, - members=1 + members=members ) if not enough_slots: - raise exc.BadRequest(not_enough_slots_error) + raise exc.NotEnoughSlotsForProject(obj.project.is_private, members, not_enough_slots_error) if not obj.token: obj.token = str(uuid.uuid1()) diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 5d598de8c..90fa70dd1 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -91,6 +91,8 @@ def test_valid_project_without_enough_public_projects_slots(client): assert response.status_code == 400 assert "can't have more public projects" in response.data["_error_message"] assert Project.objects.filter(slug="public-project-without-slots").count() == 0 + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "False" def test_valid_project_without_enough_private_projects_slots(client): @@ -110,6 +112,8 @@ def test_valid_project_without_enough_private_projects_slots(client): assert response.status_code == 400 assert "can't have more private projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "True" assert Project.objects.filter(slug="private-project-without-slots").count() == 0 @@ -1249,6 +1253,8 @@ def test_valid_dump_import_without_enough_public_projects_slots(client): response = client.post(url, {'dump': data}) assert response.status_code == 400 assert "can't have more public projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "False" assert Project.objects.filter(slug="public-project-without-slots").count() == 0 @@ -1269,6 +1275,8 @@ def test_valid_dump_import_without_enough_private_projects_slots(client): response = client.post(url, {'dump': data}) assert response.status_code == 400 assert "can't have more private projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "True" assert Project.objects.filter(slug="private-project-without-slots").count() == 0 diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 3c8f19cc0..182190553 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -70,6 +70,8 @@ def test_create_private_project_without_enough_private_projects_slots(client): assert response.status_code == 400 assert "can't have more private projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "True" def test_create_public_project_without_enough_public_projects_slots(client): @@ -86,6 +88,8 @@ def test_create_public_project_without_enough_public_projects_slots(client): assert response.status_code == 400 assert "can't have more public projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "False" def test_change_project_from_private_to_public_without_enough_public_projects_slots(client): @@ -102,6 +106,8 @@ def test_change_project_from_private_to_public_without_enough_public_projects_sl assert response.status_code == 400 assert "can't have more public projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "False" def test_change_project_from_public_to_private_without_enough_private_projects_slots(client): @@ -118,6 +124,8 @@ def test_change_project_from_public_to_private_without_enough_private_projects_s assert response.status_code == 400 assert "can't have more private projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "True" def test_create_private_project_with_enough_private_projects_slots(client):