Gitlab CSE Unil

Commit 0da00dee authored by Julien Furrer's avatar Julien Furrer
Browse files

Added support for multiple owners of an AnObj

Note: there is no user interface for editing the owners list.
It is supposed to be done through the API (to be done)
parent c3ac5f4f
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('adim', '0002_auto_20150417_1302'),
]
operations = [
migrations.AddField(
model_name='anobj',
name='owners',
field=models.ManyToManyField(related_name='owned_anobjs', verbose_name='owners', to=settings.AUTH_USER_MODEL, blank=True),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
def init_owners(apps, schema_editor):
"""
Initialize new `owners` field by adding the current `owner` to it
:param apps:
:param schema_editor:
:return:
"""
AnObj = apps.get_model("adim", "AnObj")
for anobj in AnObj.objects.all():
if len(anobj.owners.all()) == 0:
anobj.owners.add(anobj.owner)
class Migration(migrations.Migration):
dependencies = [
('adim', '0003_anobj_owners'),
]
operations = [
migrations.RunPython(init_owners),
]
...@@ -16,6 +16,8 @@ from jsonfield import JSONField ...@@ -16,6 +16,8 @@ from jsonfield import JSONField
from eav.models import BaseEntity, BaseSchema, BaseAttribute, BaseChoice from eav.models import BaseEntity, BaseSchema, BaseAttribute, BaseChoice
from adim_project.utils.decorators import cache
__all__ = ('AOType', 'AOSchema', 'AOChoice', 'AOAttribute', 'AnObj', 'AnObjMembership') __all__ = ('AOType', 'AOSchema', 'AOChoice', 'AOAttribute', 'AnObj', 'AnObjMembership')
# code from from uuid._random_getnode() # code from from uuid._random_getnode()
...@@ -93,6 +95,7 @@ class AnObj(models.Model): ...@@ -93,6 +95,7 @@ class AnObj(models.Model):
name = models.CharField(max_length=125) name = models.CharField(max_length=125)
owner = models.ForeignKey(User, verbose_name=_("owner"), related_name='anobjs') owner = models.ForeignKey(User, verbose_name=_("owner"), related_name='anobjs')
owners = models.ManyToManyField(User, verbose_name=_("owners"), related_name='owned_anobjs', blank=True)
locked = models.BooleanField(verbose_name=_("locked"), default=False) locked = models.BooleanField(verbose_name=_("locked"), default=False)
sharing_mode = models.IntegerField(verbose_name=_("sharing mode"), default=0, blank=True) sharing_mode = models.IntegerField(verbose_name=_("sharing mode"), default=0, blank=True)
...@@ -117,21 +120,35 @@ class AnObj(models.Model): ...@@ -117,21 +120,35 @@ class AnObj(models.Model):
ordering = ["-id"] ordering = ["-id"]
def __unicode__(self): def __unicode__(self):
return u"{}".format( return u"{}".format(self.name)
self.name
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""
Override save method to add actions on creation:
caluclate the `uuid`
append to `owner` to the `owners` list
:param args:
:param kwargs:
:return:
"""
new_anobj = False
# set the uuid upon object creation
if not self.uuid: if not self.uuid:
self.set_uuid(no_save=True) self.set_uuid(no_save=True)
new_anobj = True
super(AnObj, self).save(*args, **kwargs) super(AnObj, self).save(*args, **kwargs)
if new_anobj:
# if not self._thumb_url: if len(self.owners.all()) == 0:
# self._thumb_url = self._create_thumbnail() self.owners.add(self.owner)
# self.save()
def set_uuid(self, no_save=False): def set_uuid(self, no_save=False):
"""
Calculate and set the UUID, only if not already set
:param no_save:
:return:
"""
if not self.uuid: if not self.uuid:
# Using the random node initialized at module level # Using the random node initialized at module level
# to avoid compromising network address # to avoid compromising network address
...@@ -158,6 +175,15 @@ class AnObj(models.Model): ...@@ -158,6 +175,15 @@ class AnObj(models.Model):
""" """
AnObjMembership.objects.filter(anobj=self, user__in=args).delete() AnObjMembership.objects.filter(anobj=self, user__in=args).delete()
@cache(seconds=10)
def is_owned(self, user_id):
"""
Return True if the User if an owner of the anobj
This is used as function, so the result can be cached
"""
return user_id in self.owners.all().values_list('id', flat=True)
# def _create_thumbnail(self): # def _create_thumbnail(self):
# if not self.image: # if not self.image:
# return # return
......
...@@ -53,7 +53,9 @@ class PermissionClass(object): ...@@ -53,7 +53,9 @@ class PermissionClass(object):
raise PermissionDenied() raise PermissionDenied()
# Owner can always access # Owner can always access
if anobj.owner == user: # if anobj.owner == user:
# if user in anobj.owners.all():
if anobj.is_owned(user.id):
return True return True
members = anobj.members.all() members = anobj.members.all()
...@@ -178,7 +180,9 @@ def check_anobj_permission(request, anobj): ...@@ -178,7 +180,9 @@ def check_anobj_permission(request, anobj):
p = get_permission_class(anobj.sharing_mode) p = get_permission_class(anobj.sharing_mode)
if p is not None: if p is not None:
p.check_permission(request, anobj) p.check_permission(request, anobj)
elif request.user != anobj.owner: # elif request.user != anobj.owner:
# elif request.user not in anobj.owners.all():
elif not anobj.is_owned(request.user.id):
raise PermissionDenied() raise PermissionDenied()
...@@ -192,4 +196,6 @@ def has_anobj_access(request, anobj): ...@@ -192,4 +196,6 @@ def has_anobj_access(request, anobj):
:param anobj: :param anobj:
:return: boolean :return: boolean
""" """
return request.user == anobj.owner or request.user in anobj.members.all() # return request.user == anobj.owner or request.user in anobj.members.all()
\ No newline at end of file # return request.user in anobj.owners.all() or request.user in anobj.members.all()
return anobj.is_owned(request.user.id) or request.user in anobj.members.all()
\ No newline at end of file
...@@ -43,7 +43,8 @@ class AnnotationSerializer(serializers.ModelSerializer): ...@@ -43,7 +43,8 @@ class AnnotationSerializer(serializers.ModelSerializer):
class BaseAnObjSerializer(serializers.ModelSerializer): class BaseAnObjSerializer(serializers.ModelSerializer):
owner_name = serializers.ReadOnlyField(source='owner.username') #owner_name = serializers.ReadOnlyField(source='owner.username')
owner_name = serializers.SerializerMethodField()
annotations = serializers.SerializerMethodField() annotations = serializers.SerializerMethodField()
# members = serializers.SerializerMethodField('get_members') # members = serializers.SerializerMethodField('get_members')
# members = UserSerializer(many=True, read_only=True) # members = UserSerializer(many=True, read_only=True)
...@@ -57,7 +58,7 @@ class BaseAnObjSerializer(serializers.ModelSerializer): ...@@ -57,7 +58,7 @@ class BaseAnObjSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = AnObj model = AnObj
fields = ('id', 'uuid', 'name', 'owner', 'owner_name', 'annotations', 'sharing_mode', 'locked') fields = ('id', 'uuid', 'name', 'owners', 'owner', 'owner_name', 'annotations', 'sharing_mode', 'locked')
read_only_fields = ('members',) read_only_fields = ('members',)
def get_annotations(self, anobj): def get_annotations(self, anobj):
...@@ -66,6 +67,9 @@ class BaseAnObjSerializer(serializers.ModelSerializer): ...@@ -66,6 +67,9 @@ class BaseAnObjSerializer(serializers.ModelSerializer):
context=self.context) context=self.context)
return annot_serializer.data return annot_serializer.data
def get_owner_name(self, anobj):
return anobj.owners.all()[0].username
# def get_sharing_opts(self, anobj): # def get_sharing_opts(self, anobj):
# request = self.context.get('request') # request = self.context.get('request')
# if request is not None and request.user == anobj.owner: # if request is not None and request.user == anobj.owner:
...@@ -104,7 +108,7 @@ class AnObjSerializer(BaseAnObjSerializer): ...@@ -104,7 +108,7 @@ class AnObjSerializer(BaseAnObjSerializer):
class Meta: class Meta:
model = AnObj model = AnObj
fields = ('id', 'uuid', 'name', 'owner', 'owner_name', 'annotations', 'members', fields = ('id', 'uuid', 'name', 'owners', 'owner', 'owner_name', 'annotations', 'members',
'sharing_mode', 'sharing_opts', 'locked', 'allow_public_publishing') 'sharing_mode', 'sharing_opts', 'locked', 'allow_public_publishing')
...@@ -112,11 +116,11 @@ class SharedAnObjSerializer(BaseAnObjSerializer): ...@@ -112,11 +116,11 @@ class SharedAnObjSerializer(BaseAnObjSerializer):
class Meta: class Meta:
model = AnObj model = AnObj
fields = ('id', 'uuid', 'name', 'owner', 'owner_name', 'annotations', 'locked', fields = ('id', 'uuid', 'name', 'owners', 'owner', 'owner_name', 'annotations', 'locked',
'sharing_mode', 'allow_public_publishing') 'sharing_mode', 'allow_public_publishing')
class AnObjListSerializer(BaseAnObjSerializer): class AnObjListSerializer(BaseAnObjSerializer):
class Meta: class Meta:
model = AnObj model = AnObj
fields = ('id', 'uuid', 'name', 'owner', 'locked', 'sharing_mode') fields = ('id', 'uuid', 'name', 'owners', 'owner', 'locked', 'sharing_mode')
\ No newline at end of file \ No newline at end of file
...@@ -45,7 +45,8 @@ class AnObjViewSet(viewsets.ModelViewSet): ...@@ -45,7 +45,8 @@ class AnObjViewSet(viewsets.ModelViewSet):
return also shared anobjs to guests for read actions return also shared anobjs to guests for read actions
""" """
user = self.request.user user = self.request.user
q = Q(owner=user) # q = Q(owner=user)
q = Q(owners=user)
if self.action == 'list': if self.action == 'list':
q = q | Q(members=user) q = q | Q(members=user)
...@@ -56,7 +57,7 @@ class AnObjViewSet(viewsets.ModelViewSet): ...@@ -56,7 +57,7 @@ class AnObjViewSet(viewsets.ModelViewSet):
anobj = self.get_object() anobj = self.get_object()
if request.method != 'GET': if request.method != 'GET':
user_model = get_model(*settings.AUTH_USER_MODEL.split('.')) user_model = get_user_model()
members = [] members = []
for member_data in request.DATA.get('members', []): for member_data in request.DATA.get('members', []):
try: try:
...@@ -100,13 +101,16 @@ class AnObjViewSet(viewsets.ModelViewSet): ...@@ -100,13 +101,16 @@ class AnObjViewSet(viewsets.ModelViewSet):
try: try:
membership = AnObjMembership.objects.get(anobj=anobj, user=user) membership = AnObjMembership.objects.get(anobj=anobj, user=user)
except AnObjMembership.DoesNotExist: except AnObjMembership.DoesNotExist:
if user == anobj.owner: # if user == anobj.owner:
# if user in anobj.owners.all():
if anobj.is_owned(user.id):
membership = AnObjMembership.objects.create(anobj=anobj, user=user, publish_mode=new_publish_mode) membership = AnObjMembership.objects.create(anobj=anobj, user=user, publish_mode=new_publish_mode)
else: else:
raise Http404() raise Http404()
if new_publish_mode == 2 and not ( if new_publish_mode == 2 and not (
anobj.allow_public_publishing or user == anobj.owner): anobj.allow_public_publishing or anobj.is_owned(user.id)):
# anobj.allow_public_publishing or user in anobj.owners.all()):
raise PermissionDenied raise PermissionDenied
if membership.publish_mode != new_publish_mode: if membership.publish_mode != new_publish_mode:
...@@ -141,11 +145,12 @@ class SharedAnObjViewSet(AnObjViewSet): ...@@ -141,11 +145,12 @@ class SharedAnObjViewSet(AnObjViewSet):
# return AnObj.objects.filter(members=self.request.user) # return AnObj.objects.filter(members=self.request.user)
user = self.request.user user = self.request.user
q = Q(owner=user) # q = Q(owner=user)
q = Q(owners=user)
if self.action in ('list', 'retrieve', 'members'): if self.action in ('list', 'retrieve', 'members'):
q = q | Q(members=user) q = q | Q(members=user)
return AnObj.objects.filter(q) return AnObj.objects.filter(q).distinct()
@detail_route(methods=['get']) @detail_route(methods=['get'])
def members(self, request, pk=None): def members(self, request, pk=None):
...@@ -177,6 +182,7 @@ class SharedAnObjViewSet(AnObjViewSet): ...@@ -177,6 +182,7 @@ class SharedAnObjViewSet(AnObjViewSet):
# return Response({'publish_mode': membership.publish_mode}) # return Response({'publish_mode': membership.publish_mode})
# #
class UserViewSet(viewsets.ReadOnlyModelViewSet): class UserViewSet(viewsets.ReadOnlyModelViewSet):
""" """
List of registered users for an AnObj. List of registered users for an AnObj.
...@@ -260,7 +266,8 @@ class SharedAnnotationViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -260,7 +266,8 @@ class SharedAnnotationViewSet(viewsets.ReadOnlyModelViewSet):
anots = anobj.annotations.filter(owner__anobjmembership__publish_mode__gte=<publish_level>, owner__anobjmembership__anobj=anobj).select_related('owner') anots = anobj.annotations.filter(owner__anobjmembership__publish_mode__gte=<publish_level>, owner__anobjmembership__anobj=anobj).select_related('owner')
""" """
anobj = get_object_or_404(AnObj, pk=self.kwargs.get('anobjs_pk', -1)) anobj = get_object_or_404(AnObj, pk=self.kwargs.get('anobjs_pk', -1))
publish_level = 1 if self.request.user == anobj.owner else 2 # publish_level = 1 if self.request.user in anobj.owners.all() else 2
publish_level = 1 if anobj.is_owned(self.request.user.id) else 2
q = ( q = (
Q(annotable=anobj) & Q(annotable=anobj) &
......
...@@ -98,7 +98,7 @@ define([ ...@@ -98,7 +98,7 @@ define([
// config: config // config: config
//}; //};
// //
//// window[config.adim_global_varname] = ADIM; // <-- FIXME: should be removed, it could be a security issue //// window[config.adim_global_varname] = ADIM;
// //
//if (config.ready && typeof config.ready === 'function') { //if (config.ready && typeof config.ready === 'function') {
// config.ready(ADIM); // config.ready(ADIM);
......
...@@ -989,10 +989,11 @@ function($, _, Signal, paper, config, view, io, tools, attributes, Users, export ...@@ -989,10 +989,11 @@ function($, _, Signal, paper, config, view, io, tools, attributes, Users, export
if (!config.ui.show_users_results) return; if (!config.ui.show_users_results) return;
$.each(view.getAllUsersLayers(), function(i, layer) { $.each(view.getAllUsersLayers(), function(i, layer) {
console.log( layer.data );
if (annotList.find("li[data-layer-id=" + layer.id + "]").length == 0) if (annotList.find("li[data-layer-id=" + layer.id + "]").length == 0)
$(layerListItemTpl({ $(layerListItemTpl({
id:layer.id, id: layer.id,
visibleClass:visibleClass, visibleClass: visibleClass,
name: layer.data.owner || layer.name name: layer.data.owner || layer.name
})) }))
.prependTo(annotList); .prependTo(annotList);
......
...@@ -28,7 +28,9 @@ define([ ...@@ -28,7 +28,9 @@ define([
function(_, Backbone, Config){ function(_, Backbone, Config){
return Backbone.Model.extend({ return Backbone.Model.extend({
initialize: function(){ initialize: function(){
this.owned = Config && Config.user ? (this.get('owner') === Config.user.id) : false; //this.owned = Config && Config.user ? (this.get('owner') === Config.user.id) : false;
this.owned = Config && Config.users ? (this.get('owners').indexOf(Config.user.id) > -1) : false;
this.set('owned', this.owned);
}, },
url: function() { url: function() {
......
...@@ -81,7 +81,7 @@ function($, _, Backbone, Config, AnObjCollection, AnObjListItemView, AnObjProper ...@@ -81,7 +81,7 @@ function($, _, Backbone, Config, AnObjCollection, AnObjListItemView, AnObjProper
var view = new AnObjListItemView({ var view = new AnObjListItemView({
model: anobj model: anobj
}); });
var targetCtnr = (anobj.get('owner') === Config.user.id) ? this.anobjListCtnr : this.sharedAnobjListCtnr; var targetCtnr = anobj.get('owned') ? this.anobjListCtnr : this.sharedAnobjListCtnr;
targetCtnr.append(view.render().el); targetCtnr.append(view.render().el);
// this.listenTo(view, "editName", this.editAnobjName); // this.listenTo(view, "editName", this.editAnobjName);
}, },
......
...@@ -137,7 +137,6 @@ define([ ...@@ -137,7 +137,6 @@ define([
.toggleClass("aom-prop-unlocked", !locked); .toggleClass("aom-prop-unlocked", !locked);
var sharingModeSelect = this.sharingPropEl.find("[name=aom-prop-shared]"); var sharingModeSelect = this.sharingPropEl.find("[name=aom-prop-shared]");
console.log("sharingMode", sharingMode);
if (_.isUndefined(sharingMode) || sharingMode === "" ) { if (_.isUndefined(sharingMode) || sharingMode === "" ) {
sharingModeSelect.attr("disabled", "disabled"); sharingModeSelect.attr("disabled", "disabled");
} else { } else {
...@@ -185,10 +184,11 @@ define([ ...@@ -185,10 +184,11 @@ define([
} }
// see http://underscorejs.org/#chain // see http://underscorejs.org/#chain
var anobjOwners = model.get('owners');
$el.html(_.chain(members) $el.html(_.chain(members)
// Filter out owner from members list // Filter out owners of the anobj from the members list
// (needed because of the AnObjMembership used to store the publishing status of the owner) // (needed because of the AnObjMembership used to store the publishing status of the owner)
.filter(function(m){ return m.id !== config.user.id; }) .filter(function(m){ return anobjOwners.indexOf(m.id) === -1; })
// Ordering by username // Ordering by username
.sortBy('username') .sortBy('username')
// Apply template // Apply template
......
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
{% block navbar_content %} {% block navbar_content %}
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a href="#" class="adim-image-name {% if anobj.owner.id != user.id %}read-only{% endif %}"> <li><a href="#" class="adim-image-name {{ is_owner|yesno:",read-only" }}">
<span class="text">{{ anobj.name }}</span> <span class="text">{{ anobj.name }}</span>
{# &nbsp;<span class="glyphicon glyphicon-pencil" style="font-size: 12px;"></span>#}
</a></li> </a></li>
</ul> </ul>
{{ block.super }} {{ block.super }}
...@@ -45,7 +44,7 @@ window.ADIM_CONFIG = { ...@@ -45,7 +44,7 @@ window.ADIM_CONFIG = {
api: { api: {
baseUrl: "{% url "api-root" %}", baseUrl: "{% url "api-root" %}",
annotables: "{% if anobj.owner == user %}{% url "anobjs-list" %}{% else %}{% url "shared-anobjs-list" %}{% endif %}", annotables: "{% if is_owner %}{% url "anobjs-list" %}{% else %}{% url "shared-anobjs-list" %}{% endif %}",
annotations: "{% url "annotations-list" %}" annotations: "{% url "annotations-list" %}"
}, },
...@@ -70,7 +69,7 @@ window.ADIM_CONFIG = { ...@@ -70,7 +69,7 @@ window.ADIM_CONFIG = {
{% if membership %} {% if membership %}
,membership: { publish_mode: {{ membership.publish_mode }} } ,membership: { publish_mode: {{ membership.publish_mode }} }
{% endif %} {% endif %}
,guest: {% if anobj.owner.id != user.id %}true{% else %}false{% endif %} ,guest: {{ is_owner|yesno:"false,true" }}
,shibboleth: true ,shibboleth: true
}, },
......
...@@ -120,34 +120,28 @@ ...@@ -120,34 +120,28 @@
<label> <label>
<input name="publish-mode" value="0" type="radio"/> <input name="publish-mode" value="0" type="radio"/>
<span class="icon-home"></span> <span class="icon-home"></span>
{# <span class="lead"><span class="label label-info">#}
<strong>privé</strong> <strong>privé</strong>
{# </span></span>#}
<span class="help-block">Les annotations sont accessibles pour vous seulement.</span> <span class="help-block">Les annotations sont accessibles pour vous seulement.</span>
</label> </label>
</div> </div>
{% if anobj.owner != user %}
<div class="radio"> <div class="radio">
<label> <label>
<input name="publish-mode" value="1" type="radio"/> <input name="publish-mode" value="1" type="radio"/>
<span class="icon-user"></span> <span class="icon-user"></span>
{# <span class="lead"><span class="label label-warning">#}
<strong>propriétaire</strong> <strong>propriétaire</strong>
{# </span></span>#}
<span class="help-block">Les annotations peuvent être vues par le/la propriétaire de l'image uniquement. <span class="help-block">Les annotations peuvent être vues par le/la propriétaire de l'image uniquement.
{% if anobj %}Pour cette image il s'agit de: <strong>«{{ anobj.owner }}»</strong> {% endif %} {% if anobj %}Pour cette image il s'agit de: <strong>«{{ anobj.owner }}»</strong> {% endif %}
</span> </span>
</label> </label>
</div> </div>
{% endif %} {% if anobj.allow_public_publishing or is_owner %}
{% if anobj.allow_public_publishing or anobj.owner == user %}
<div class="radio"> <div class="radio">
<label> <label>
<input name="publish-mode" value="2" type="radio"/> <input name="publish-mode" value="2" type="radio"/>
<span class="icon-groups-friends"></span><strong>public</strong> <span class="icon-groups-friends"></span><strong>public</strong>
<span class="help-block"> <span class="help-block">
Les annotations peuvent être vues par toutes les personnes qui ont un accès à l'image. Les annotations peuvent être vues par toutes les personnes qui ont un accès à l'image.
{% if anobj.owner != user %}Pour connaitre précisément la liste de ces personnes, {% if not is_owner %}Pour connaitre précisément la liste de ces personnes,
addressez vous à la/le propriétaire de l'image.{% endif %} addressez vous à la/le propriétaire de l'image.{% endif %}
</span> </span>
</label> </label>
...@@ -224,7 +218,7 @@ ...@@ -224,7 +218,7 @@
<% if (locked) { %><span class="glyphicon glyphicon-lock" title="Vérouillé"></span><% } %> <% if (locked) { %><span class="glyphicon glyphicon-lock" title="Vérouillé"></span><% } %>
</div> </div>
<% if (owner == {{ user.id }}) { %><button class="close" title="Supprimer l'image"> <% if (owned) { %><button class="close" title="Supprimer l'image">
<span class="glyphicon glyphicon-remove-circle"></span> <span class="glyphicon glyphicon-remove-circle"></span>
</button><% } %> </button><% } %>
<a href="{{ annotate_url }}<%= uuid %>/" title="Editer l'image"> <a href="{{ annotate_url }}<%= uuid %>/" title="Editer l'image">
......
...@@ -149,7 +149,9 @@ def annotate(request, anobj_uuid=None): ...@@ -149,7 +149,9 @@ def annotate(request, anobj_uuid=None):
if len(anobj_uuid) < 32: if len(anobj_uuid) < 32:
return HttpResponseRedirect(reverse('adim_app:annotate', kwargs={'anobj_uuid': anobj.uuid})) return HttpResponseRedirect(reverse('adim_app:annotate', kwargs={'anobj_uuid': anobj.uuid}))
is_owner = request.user == anobj.owner