diff --git a/beat/web/team/api.py b/beat/web/team/api.py index a2d94bb5289c36bf38a970c7a0113ebc33f7a0e5..a4f4a02d89fe9748b88acf35411c8f75f96b5d58 100644 --- a/beat/web/team/api.py +++ b/beat/web/team/api.py @@ -42,7 +42,7 @@ from .serializers import TeamUpdateSerializer from .models import Team from .permissions import IsOwner, HasPrivacyLevel -from ..common.responses import BadRequestResponse +from ..common.responses import BadRequestResponse, ForbiddenResponse from ..common.mixins import CommonContextMixin @@ -117,6 +117,11 @@ class TeamDetailView(CommonContextMixin, generics.RetrieveUpdateDestroyAPIView): def delete(self, request, owner_name, team_name): team = self.get_queryset() + + # Check that the team can still be deleted + if not(team.deletable()): + return ForbiddenResponse("The team isn't deletable (it has been used to share %d objects with its members)" % team.total_shares()) + team.delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/beat/web/team/models.py b/beat/web/team/models.py index 69bd56e0ed3b536e100e7e19c50dafe63be7d236..e8e4e8ff27d4f989cc110095792d6b73f173c376 100644 --- a/beat/web/team/models.py +++ b/beat/web/team/models.py @@ -93,6 +93,57 @@ class Team(models.Model): return (self.owner.username, self.name) + def total_shares(self, user=None): + '''Count number of objects shared with this team + + If ``user`` is passed, counts objects shared with this team that the + given user has access to. + ''' + + used_at = 0 + + if user is None: + + used_at += self.shared_algorithms.count() + used_at += self.shared_databases.count() + used_at += self.shared_dataformats.count() + used_at += self.shared_environments.count() + used_at += self.shared_experiments.count() + used_at += self.shared_librarys.count() + used_at += self.shared_plotterparameters.count() + used_at += self.shared_plotters.count() + used_at += self.shared_searchs.count() + used_at += self.shared_toolchains.count() + used_at += self.usable_algorithms.count() + used_at += self.usable_librarys.count() + used_at += self.usable_plotterparameters.count() + used_at += self.usable_plotters.count() + + else: + + used_at += self.shared_algorithms.for_user(user, add_public=True).count() + used_at += self.shared_databases.for_user(user, add_public=True).count() + used_at += self.shared_dataformats.for_user(user, add_public=True).count() + used_at += self.shared_environments.for_user(user, add_public=True).count() + used_at += self.shared_experiments.for_user(user, add_public=True).count() + used_at += self.shared_librarys.for_user(user, add_public=True).count() + used_at += self.shared_plotterparameters.for_user(user, add_public=True).count() + used_at += self.shared_plotters.for_user(user, add_public=True).count() + used_at += self.shared_searchs.for_user(user, add_public=True).count() + used_at += self.shared_toolchains.for_user(user, add_public=True).count() + used_at += self.usable_algorithms.for_user(user, add_public=True).count() + used_at += self.usable_librarys.for_user(user, add_public=True).count() + used_at += self.usable_plotterparameters.for_user(user, add_public=True).count() + used_at += self.usable_plotters.for_user(user, add_public=True).count() + + return used_at + + + def deletable(self): + + return not self.total_shares() + + #_____ Overrides __________ def __unicode__(self): diff --git a/beat/web/team/templates/team/edition.html b/beat/web/team/templates/team/edition.html index fd106134a0701e1c46a067a4ce3ad3bfd923349f..433d2a319e12ad69e67502c1c940a5caf6f1fa8b 100644 --- a/beat/web/team/templates/team/edition.html +++ b/beat/web/team/templates/team/edition.html @@ -2,21 +2,21 @@ {% comment %} * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ * Contact: beat.support@idiap.ch - * + * * This file is part of the beat.web module of the BEAT platform. - * + * * Commercial License Usage * Licensees holding valid commercial BEAT licenses may use this file in * accordance with the terms contained in a written agreement between you * and Idiap. For further information contact tto@idiap.ch - * + * * Alternatively, this file may be used under the terms of the GNU Affero * Public License version 3 as published by the Free Software and appearing * in the file LICENSE.AGPL included in the packaging of this file. * The BEAT platform 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. - * + * * You should have received a copy of the GNU Affero Public License along * with the BEAT platform. If not, see http://www.gnu.org/licenses/. {% endcomment %} @@ -125,8 +125,8 @@ </div>{# column #} </div>{# row #} -<script type="text/javascript"> {% csrf_token %} +<script type="text/javascript"> jQuery(document).ready(function() { //sets-up chosen box diff --git a/beat/web/team/templates/team/panels/actions.html b/beat/web/team/templates/team/panels/actions.html index 75e9e51c80c02520f8a29b3a87b527329548679c..434d1ece2953da50516feade12ad021efb30546d 100644 --- a/beat/web/team/templates/team/panels/actions.html +++ b/beat/web/team/templates/team/panels/actions.html @@ -1,34 +1,39 @@ {% comment %} * Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/ * Contact: beat.support@idiap.ch - * + * * This file is part of the beat.web module of the BEAT platform. - * + * * Commercial License Usage * Licensees holding valid commercial BEAT licenses may use this file in * accordance with the terms contained in a written agreement between you * and Idiap. For further information contact tto@idiap.ch - * + * * Alternatively, this file may be used under the terms of the GNU Affero * Public License version 3 as published by the Free Software and appearing * in the file LICENSE.AGPL included in the packaging of this file. * The BEAT platform 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. - * + * * You should have received a copy of the GNU Affero Public License along * with the BEAT platform. If not, see http://www.gnu.org/licenses/. {% endcomment %} +{% load team_tags %} <div class="btn-group btn-group-sm action-buttons pull-right"> {% if display_count %} - <a class="btn btn-default btn-references" href="{% url 'teams:view' object.owner.username object.name %}" data-toggle="tooltip" data-placement="bottom" title="Number of members in this team"><span class="badge">{{ object.members.count }}</span></a> + <a class="btn btn-default btn-references" href="{% url 'teams:view' object.owner.username object.name %}" data-toggle="tooltip" data-placement="bottom" title="Number of objects shared through this team"><span class="badge">{% total_shares_for_user object %}</span></a> + + <a class="btn btn-default btn-references" href="{% url 'teams:view' object.owner.username object.name %}" data-toggle="tooltip" data-placement="bottom" title="Number of members in this team">{{ object.members.count }} <i class="fa fa-group"></i></a> {% endif %} {% ifequal request.user object.owner %} <!-- Delete, needs to be the owner --> + {% if object.deletable %} <a class="btn btn-default btn-delete" onclick="modal_delete('team', '{{ object.name }}', '{% url 'api_teams:user_teamlist' request.user.username %}', '{% url 'teams:list' request.user.username %}');" data-toggle="tooltip" data-placement="bottom" title="Delete"><i class="fa fa-times fa-lg"></i></a> + {% endif %} <!-- Edit --> <a class="btn btn-default btn-edit" href="{% url 'teams:edit' object.owner.username object.name %}" data-toggle="tooltip" data-placement="bottom" title="Edit"><i class="fa fa-edit fa-lg"></i></a> diff --git a/beat/web/team/templatetags/team_tags.py b/beat/web/team/templatetags/team_tags.py index 1fc422d73f0cdf126f31d8dfa5ec3079820209d6..bc8acad19942b8ef845233ce9934ae583151b8db 100644 --- a/beat/web/team/templatetags/team_tags.py +++ b/beat/web/team/templatetags/team_tags.py @@ -86,10 +86,21 @@ def team_actions(context, obj, display_count): } -register.inclusion_tag('ui/contribution_list.html') +#-------------------------------------------------- + + +@register.inclusion_tag('ui/contribution_list.html') def user_list(id): return { 'panel_id': id, 'URL_PREFIX': settings.URL_PREFIX, 'finder_placeholder': 'Find an User...', 'owner': False, } + + +#-------------------------------------------------- + + +@register.simple_tag(takes_context=True) +def total_shares_for_user(context, team): + return team.total_shares(context['request'].user) diff --git a/beat/web/team/tests.py b/beat/web/team/tests.py index 3839dcfe72a34109e0822ba6213ef01f40e64728..1d12285ea4bf3f2868ce1643381c038b0c1b1b6d 100644 --- a/beat/web/team/tests.py +++ b/beat/web/team/tests.py @@ -31,11 +31,15 @@ from rest_framework import status from rest_framework.test import APITestCase from django.contrib.auth.models import User +from django.conf import settings +import os +import shutil import simplejson as json from ..common.testutils import tearDownModule from .models import Team +from ..dataformats.models import DataFormat from .serializers import SimpleTeamSerializer @@ -57,6 +61,19 @@ class TeamTestCase(APITestCase): self.invalid_username = 'janedoe' + # object to test sharing/deletion + if os.path.exists(settings.DATAFORMATS_ROOT): + shutil.rmtree(settings.DATAFORMATS_ROOT) + + (dataformat, errors) = DataFormat.objects.create_dataformat( + author=self.johndoe, + name='data_format_shared_with_private_team', + ) + assert dataformat, errors + + dataformat.share(teams=[self.teamdoe_private]) + self.dataformat = dataformat + #---------------------------------------------------------- @@ -377,14 +394,31 @@ class TeamDeletionTestCase(TeamTestCase): options['owner_name'] = self.johndoe.username url = self.get_url(options) + self.assertEqual(self.teamdoe.total_shares(), 0) + response = self.client.delete(url, format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + def test_logged_in_user_own_team_shared(self): + self.client.login(username=self.johndoe.username, password=self.password) + + options = self.options + options['owner_name'] = self.johndoe.username + options['team_name'] = 'teamdoe_private' + url = self.get_url(options) + + self.assertEqual(self.teamdoe_private.total_shares(), 1) + + response = self.client.delete(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_logged_in_user_team_from_other(self): self.client.login(username=self.jackdoe, password=self.password) url = self.get_url(self.options) + self.assertEqual(self.teamdoe.total_shares(), 0) + response = self.client.delete(url, format='json') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -396,6 +430,8 @@ class TeamDeletionTestCase(TeamTestCase): options['owner_name'] = self.invalid_username url = self.get_url(options) + self.assertEqual(self.teamdoe.total_shares(), 0) + response = self.client.delete(url, format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)