diff --git a/ipynbsrv/api/serializer.py b/ipynbsrv/api/serializer.py index 28a6cb8..cc7337f 100644 --- a/ipynbsrv/api/serializer.py +++ b/ipynbsrv/api/serializer.py @@ -199,6 +199,8 @@ class ContainerImageSerializer(serializers.ModelSerializer): Todo: write doc. """ friendly_name = serializers.CharField(read_only=True, source='get_friendly_name') + owner = UserSerializer(many=False) + access_groups = FlatCollaborationGroupSerializer(many=True, read_only=True) class Meta: model = ContainerImage diff --git a/ipynbsrv/api/urls.py b/ipynbsrv/api/urls.py index 373ba42..c5bfdd8 100644 --- a/ipynbsrv/api/urls.py +++ b/ipynbsrv/api/urls.py @@ -45,6 +45,8 @@ # /api/container/images(/)... url(r'^containers/images/?$', views.ContainerImageList.as_view(), name="images"), url(r'^containers/images/(?P[0-9]+)$', views.ContainerImageDetail.as_view(), name="image_detail"), + url(r'^containers/images/(?P[0-9]+)/add_access_groups$', views.image_add_access_groups, name="image_add_access_groups"), + url(r'^containers/images/(?P[0-9]+)/remove_access_groups$', views.image_remove_access_groups, name="image_remove_access_groups"), # /api/container/snapshots(/)... url(r'^containers/snapshots/?$', views.ContainerSnapshotList.as_view(), name="snapshot"), diff --git a/ipynbsrv/api/views.py b/ipynbsrv/api/views.py index f4ad5b2..297b967 100644 --- a/ipynbsrv/api/views.py +++ b/ipynbsrv/api/views.py @@ -49,7 +49,13 @@ def api_root(request, format=None): } available_endpoints['containers'] = { '': 'Get a list of all containers available to your user.', - 'images': 'Get a list of all container images available to your user.', + 'images': { + '': 'Get a list of all container images available to your user.', + '{id}': { + 'add_access_groups': 'Add access_groups to the share.', + 'remove_access_groups': 'Remove access_groups from the share.' + } + }, 'snapshots': 'Get a list of all container snapshots available to your user.', '{id}': { '': 'Get details about a container.', @@ -727,6 +733,95 @@ class ContainerImageDetail(generics.RetrieveUpdateDestroyAPIView): queryset = ContainerImage.objects.all() +@api_view(['POST']) +def image_add_access_groups(request, pk): + """ + Add a list of collaboration groups to the image. + Todo: show params on OPTIONS call. + Todo: permissions + :param pk pk of the collaboration group + """ + required_params = ["access_groups"] + params = validate_request_params(required_params, request) + + obj = ContainerImage.objects.filter(id=pk) + if not obj: + return Response({"error": "Image not found!", "data": request.data}) + image = obj.first() + + # validate permissions + # validate_object_permission(ShareDetailPermissions, request, share) + + # validate all the access_groups first before adding them + access_groups = [] + for access_group_id in params.get("access_groups"): + obj = CollaborationGroup.objects.filter(id=access_group_id) + if not obj: + return Response( + {"error": "CollaborationGroup not found!", "data": access_group_id}, + status=status.HTTP_404_NOT_FOUND + ) + access_groups.append(obj.first()) + + added_groups = [] + # add the access groups to the share + for access_group in access_groups: + if image.add_access_group(access_group): + added_groups.append((access_group.id, access_group.name)) + + return Response({ + "detail": "Groups added successfully", + "groups": added_groups, + "count": len(added_groups) + }, + status=status.HTTP_200_OK + ) + + +@api_view(['POST']) +def image_remove_access_groups(request, pk): + """ + Remove a list of collaboration groups from the image. + Todo: show params on OPTIONS call. + Todo: permissions + :param pk pk of the collaboration group + """ + required_params = ["access_groups"] + params = validate_request_params(required_params, request) + obj = ContainerImage.objects.filter(id=pk) + if not obj: + return Response({"error": "Image not found!", "data": request.data}) + image = obj.first() + + # validate permissions + # validate_object_permission(ShareDetailPermissions, request, share) + + # validate all the access_groups first before adding them + access_groups = [] + for access_group_id in params.get("access_groups"): + obj = CollaborationGroup.objects.filter(id=access_group_id) + if not obj: + return Response( + {"error": "CollaborationGroup not found!", "data": access_group_id}, + status=status.HTTP_404_NOT_FOUND + ) + access_groups.append(obj.first()) + + removed_groups = [] + # add the access groups to the share + for access_group in access_groups: + if image.remove_access_group(access_group): + removed_groups.append((access_group.id, access_group.name)) + + return Response({ + "detail": "Groups removed successfully", + "groups": removed_groups, + "count": len(removed_groups) + }, + status=status.HTTP_200_OK + ) + + class ContainerSnapshotsList(generics.ListAPIView): """ Get a list of all snapshots for a specific container. @@ -860,8 +955,6 @@ def share_add_access_groups(request, pk): """ required_params = ["access_groups"] params = validate_request_params(required_params, request) - print("add access groups") - print(params) obj = Share.objects.filter(id=pk) if not obj: @@ -880,15 +973,21 @@ def share_add_access_groups(request, pk): {"error": "CollaborationGroup not found!", "data": access_group_id}, status=status.HTTP_404_NOT_FOUND ) - print(obj.first()) access_groups.append(obj.first()) + added_groups = [] # add the access groups to the share for access_group in access_groups: - share.add_access_group(access_group) + if share.add_access_group(access_group): + added_groups.append((access_group.id, access_group.name)) - serializer = NestedShareSerializer(share) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response({ + "detail": "Groups added successfully", + "groups": added_groups, + "count": len(added_groups) + }, + status=status.HTTP_200_OK + ) @api_view(['POST']) @@ -899,10 +998,9 @@ def share_remove_access_groups(request, pk): Todo: permissions :param pk pk of the collaboration group """ - print("remove access groups") required_params = ["access_groups"] params = validate_request_params(required_params, request) - print(params) + obj = Share.objects.filter(id=pk) if not obj: return Response({"error": "Share not found!", "data": request.data}) @@ -922,15 +1020,19 @@ def share_remove_access_groups(request, pk): ) access_groups.append(obj.first()) + removed_groups = [] # add the access groups to the share for access_group in access_groups: - share.remove_access_group(access_group) - print("after remove from model") - - print("after all") - - serializer = NestedShareSerializer(share) - return Response(serializer.data, status=status.HTTP_201_CREATED) + if share.remove_access_group(access_group): + removed_groups.append((access_group.id, access_group.name)) + + return Response({ + "detail": "Groups removed successfully", + "groups": removed_groups, + "count": len(removed_groups) + }, + status=status.HTTP_200_OK + ) class TagList(generics.ListCreateAPIView): diff --git a/ipynbsrv/core/models.py b/ipynbsrv/core/models.py index 2eb3827..9b06316 100644 --- a/ipynbsrv/core/models.py +++ b/ipynbsrv/core/models.py @@ -789,7 +789,7 @@ class ContainerImage(models.Model): help_text='A short description of the container image.' ) description = models.TextField( - blank=True, + blank=True, null=True, help_text='Detailed information on how to use this image.' ) @@ -799,7 +799,6 @@ class ContainerImage(models.Model): related_name='images', help_text='The groups having access to that image.' ) - command = models.CharField( blank=True, null=True, @@ -828,6 +827,24 @@ class ContainerImage(models.Model): is_internal = models.BooleanField(default=False) is_public = models.BooleanField(default=False) + def add_access_group(self, group): + """ + Add access group to image. + """ + if group not in self.access_groups.all(): + self.access_groups.add(group) + return True + return False + + def remove_access_group(self, group): + """ + Remove access group from image. + """ + if group in self.access_groups.all(): + self.access_groups.remove(group) + return True + return False + def get_friendly_name(self): """ Return the humen-friendly name of this image. diff --git a/ipynbsrv/web/templates/web/images/index.html b/ipynbsrv/web/templates/web/images/index.html index 8a565d6..105f200 100644 --- a/ipynbsrv/web/templates/web/images/index.html +++ b/ipynbsrv/web/templates/web/images/index.html @@ -24,7 +24,8 @@

Images

Name - Short Description + Short Description + Owner Public Actions @@ -32,13 +33,14 @@

Images

{% for img in images %} - {{ img.friendly_name }} + {{ img.friendly_name }} {{ img.short_description }} + {{ img.owner.username }} - {% if img.owner == request.user.id %} + {% if img.owner.id == request.user.id %}
{% csrf_token %} diff --git a/ipynbsrv/web/templates/web/images/manage.html b/ipynbsrv/web/templates/web/images/manage.html new file mode 100644 index 0000000..9d1e9a3 --- /dev/null +++ b/ipynbsrv/web/templates/web/images/manage.html @@ -0,0 +1,137 @@ +{% extends 'web/base.html' %} + +{% load staticfiles %} + +{% block external_css %} + + + +{% endblock %} + +{% block content %} +
+
+ {% include 'web/snippets/messages.html' with messages=messages only %} + +

Image

+ {% if request.user.id == image.owner.id %} + + {% csrf_token %} + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Name{{ image.friendly_name }}
Short Description{{ image.short_description }}
Description{{ image.description }}
Public{{ image.is_public }}
Owner{{ image.owner.username }}{% if image.owner.id == request.user.id %} That's you!{% endif %}
+ + {% if request.user.id == image.owner.id %} +

Shared with...

+ +
+
+ + {% if groups|length_is:"1" %} + + {% else %} + + {% endif %} +
+
+ + + + + + + + + {% for group in image.access_groups %} + + + + + {% endfor %} + +
MemberActions
{{ group.name }} + {% if request.user.id == image.owner.id %} +
+ {% csrf_token %} + + + +
+ {% endif %} +
+ + {% include 'web/images/modal_addusers.html' with id=image.id users=users image=image origin='manage' csrf_token=csrf_token only %} + {% include 'web/images/modal_addgroups.html' with id=image.id groups=groups image=image origin='manage' csrf_token=csrf_token only %} + {% endif %} +
+
+{% endblock %} + +{% block js %} + + + + + +{% endblock %} diff --git a/ipynbsrv/web/templates/web/images/modal_addgroups.html b/ipynbsrv/web/templates/web/images/modal_addgroups.html new file mode 100644 index 0000000..89d7306 --- /dev/null +++ b/ipynbsrv/web/templates/web/images/modal_addgroups.html @@ -0,0 +1,38 @@ + diff --git a/ipynbsrv/web/templates/web/images/modal_addusers.html b/ipynbsrv/web/templates/web/images/modal_addusers.html new file mode 100644 index 0000000..7773ec2 --- /dev/null +++ b/ipynbsrv/web/templates/web/images/modal_addusers.html @@ -0,0 +1,38 @@ + diff --git a/ipynbsrv/web/templates/web/shares/modal_addgroups.html b/ipynbsrv/web/templates/web/shares/modal_addgroups.html index 2b4a39e..1dfa338 100644 --- a/ipynbsrv/web/templates/web/shares/modal_addgroups.html +++ b/ipynbsrv/web/templates/web/shares/modal_addgroups.html @@ -1,11 +1,11 @@ -