瀏覽代碼

[doc2] Add sharing popup permission

Romain Rigaux 10 年之前
父節點
當前提交
aeb673e

+ 33 - 10
desktop/core/src/desktop/api2.py

@@ -22,6 +22,7 @@ import time
 import StringIO
 import zipfile
 
+from django.contrib.auth.models import Group, User
 from django.core import management
 from django.http import HttpResponse
 from django.shortcuts import redirect
@@ -88,6 +89,18 @@ def get_documents2(request):
   })
 
 
+@api_error_handler
+def get_document(request):
+  if request.GET.get('id'):
+    doc = Document2.objects.get(id=request.GET['id'])
+  else:
+    doc = Document2.objects.get(uuid=request.GET['uuid'])
+
+#   response = _massage_doc_for_json(doc, request.user, with_data=request.GET.get('with_data'))
+
+  return JsonResponse(doc.to_dict())
+
+
 @api_error_handler
 @require_POST
 def move_document(request):
@@ -140,20 +153,30 @@ def delete_document(request):
 
 @api_error_handler
 @require_POST
-def update_permissions(requests):
-  pass
+def share_document(request):
+  """
+  Set who else or which other group can interact with the document.
 
+  Example of input: {'read': {'user_ids': [1, 2, 3], 'group_ids': [1, 2, 3]}}
+  """
+  perms_dict = json.loads(request.POST['perms_dict'])
+  doc_id = json.loads(request.POST['doc_id'])
 
-# TODO security + permissions
-def get_document(request):
-  if request.GET.get('id'):
-    doc = Document2.objects.get(id=request.GET['id'])
-  else:
-    doc = Document2.objects.get(uuid=request.GET['uuid'])
+  doc = Document2.objects.document(request.user, doc_id)
+
+  for name, perm in perms_dict.iteritems():
+    users = groups = None
+    if perm.get('user_ids'):
+      users = User.objects.in_bulk(perm.get('user_ids'))
+    else:
+      users = []
 
-  response = _massage_doc_for_json(doc, request.user, with_data=request.GET.get('with_data'))
+    if perm.get('group_ids'):
+      groups = Group.objects.in_bulk(perm.get('group_ids'))
+    else:
+      groups = []
 
-  return JsonResponse(response)
+    doc.share(request.user, name=name, users=users, groups=groups)
 
 
 def _massage_doc_for_json(document, user, with_data=False):

+ 18 - 0
desktop/core/src/desktop/models.py

@@ -903,6 +903,24 @@ class Document2(models.Model):
     destination_directory = self.documents(self.user).get(type='directory', uuid=self.uuid)
     destination_directory.dependencies.remove(self)
 
+  def share(self, user, name='read', users=None, groups=None):
+    # TODO check in settings if user can sync, re-share, which perms...
+
+    perm, created = Document2Permission.objects.get_or_create(doc=self, perms=name)
+
+    if users is not None:
+      perm.users = []
+      perm.users = users
+      perm.save()
+
+    if groups is not None:
+      perm.groups = []
+      perm.groups = groups
+      perm.save()
+
+    if not users and not groups:
+      perm.delete()
+
 
 class Directory(Document2):
   # e.g. name = '/' or '/dir1/dir2/f3'

+ 2 - 1
desktop/core/src/desktop/static/desktop/js/home2.vm.js

@@ -22,6 +22,7 @@ function HomeViewModel(data) {
   self.path = ko.mapping.fromJS('/');
   self.mkdirFormPath = ko.observable('');
   self.deleteFormPath = ko.observable('');
+  self.shareFormDocId = ko.observable('');
 
   self.page = ko.observable(1);
   self.documentsPerPage = ko.observable(50);
@@ -92,7 +93,7 @@ function HomeViewModel(data) {
         self.mkdirFormPath('');
       }
       else {
-          $(document).trigger("error", data.message);
+        $(document).trigger("error", data.message);
       }
     }).fail(function (xhr) {
       $(document).trigger("error", xhr.responseText);

+ 257 - 0
desktop/core/src/desktop/static/desktop/js/share2.vm.js

@@ -0,0 +1,257 @@
+var shareViewModel;
+
+function ShareViewModel(updateDocF) {
+  var self = this;
+
+  self.hasInitBeenCalled = false;
+  self.hasSetupBeenCalled = false;
+
+  self.selectedPerm = ko.observable('read');
+  self.selectedPermLabel = ko.computed(function() {
+    if (self.selectedPerm() == 'write') {
+      return 'Modify';
+    } else {
+      return 'Read';
+    }
+  });
+
+  self.selectedDoc = ko.observable(ko.mapping.fromJS({
+    perms: {
+      read: {
+        users: [],
+        groups: []
+      },
+      write: {
+        users: [],
+        groups: []
+      }
+    }
+  }))
+
+  self.updateDoc = updateDocF
+
+  self.setDocId = function(docId) {
+    if (docId == -1) { return false; }
+    $.get('/desktop/api2/doc/get', { id : docId }, function (data) {
+      shareViewModel.selectedDoc(data)
+    }).fail(function (response) {
+      $(document).trigger("error", "There was an error processing your action: " + response.responseText);
+    });
+  }
+}
+
+
+function openShareModal() {
+  $("#documentShareModal").modal("show");
+  setupSharing(function(){
+    $("#documentShareAddBtn").removeClass("disabled");
+    $("#documentShareCaret").removeClass("disabled");
+    $("#documentShareTypeahead").removeAttr("disabled");
+    $(".notpretty").each(function(){
+      $(this).text(prettifyUsername($(this).attr("data-id")));
+    });
+  });
+}
+
+function isShared() {
+  if (!shareViewModel) { return false; }
+  read = shareViewModel.selectedDoc().perms.read;
+  return read.users.length + read.groups.length > 0
+}
+
+function prettifyUsername(userId) {
+  var _user = null;
+  if (self.hasSetupBeenCalled) {
+    for (var i = 0; i < JSON_USERS_GROUPS.users.length; i++) {
+      if (JSON_USERS_GROUPS.users[i].id == userId) {
+        _user = JSON_USERS_GROUPS.users[i];
+      }
+    }
+    if (_user != null) {
+      return (_user.first_name != "" ? _user.first_name + " " : "") + (_user.last_name != "" ? _user.last_name + " " : "") + ((_user.first_name != "" || _user.last_name != "") ? "(" : "") + _user.username + ((_user.first_name != "" || _user.last_name != "") ? ")" : "");
+    }
+  }
+  return "";
+}
+
+function initSharing(id, updateFunc) {
+  if(! updateFunc) {
+    updateFunc = function () {}
+  }
+  shareViewModel = new ShareViewModel(updateFunc);
+  ko.applyBindings(shareViewModel, $(id)[0]);
+  shareViewModel.hasInitBeenCalled = true;
+  return shareViewModel;
+}
+
+function setupSharing(id, updateFunc) {
+  if (shareViewModel == null || !shareViewModel.hasInitBeenCalled) {
+    shareViewModel = initSharing(id, updateFunc);
+  }
+
+  if (! self.hasSetupBeenCalled){
+    $.getJSON('/desktop/api/users/autocomplete', function (data) {
+      self.hasSetupBeenCalled = true;
+      JSON_USERS_GROUPS = data;
+      dropdown = [];
+      usermap = {};
+      groupmap = {};
+
+      $.each(JSON_USERS_GROUPS.users, function (i, user) {
+        var _display = prettifyUsername(user.id);
+        usermap[_display] = user;
+        dropdown.push(_display);
+      });
+
+      $.each(JSON_USERS_GROUPS.groups, function (i, group) {
+        groupmap[group.name] = group;
+        dropdown.push(group.name);
+      });
+
+      $("#documentShareTypeahead").typeahead({
+        source: function (query, process) {
+          process(dropdown);
+        },
+        matcher: function (item) {
+          if (item.toLowerCase().indexOf(this.query.trim().toLowerCase()) != -1) {
+            return true;
+          }
+        },
+        sorter: function (items) {
+          return items.sort();
+        },
+        highlighter: function (item) {
+          var _icon = "fa";
+          if (usermap[item]) {
+            _icon += " fa-user";
+          }
+          else {
+            _icon += " fa-users";
+          }
+          var regex = new RegExp('(' + this.query + ')', 'gi');
+          return "<i class='" + _icon + "'></i> " + item.replace(regex, "<strong>$1</strong>");
+        },
+        updater: function (item) {
+          selectedUserOrGroup = usermap[item] ? usermap[item] : groupmap[item];
+          return item;
+        }
+      });
+
+      $("#documentShareTypeahead").on("keyup", function (e) {
+        var _code = (e.keyCode ? e.keyCode : e.which);
+        if (_code == 13) {
+          handleTypeaheadSelection();
+        }
+      });
+
+      if (typeof id == "function"){
+        id();
+      }
+    }).fail(function (response) {
+      $(document).trigger("error", "There was an error processing your action: " + response.responseText);
+    });
+
+    $("#documentShareAddBtn").on("click", function () {
+      if (! $(this).hasClass("disabled")){
+        handleTypeaheadSelection();  
+      }
+    });
+  }
+  else {
+    if (typeof id == "function"){
+      id();
+    }
+  }
+
+  return shareViewModel;
+}
+
+function updateSharePerm(perms, user) {
+  $(perms).each(function (cnt, item) {
+    if (item.id == user.id) {
+      perms.splice(cnt, 1);
+    }
+  });
+  shareViewModel.selectedDoc.valueHasMutated();
+  shareDocFinal();
+}
+
+function removeUserReadShare(user) {
+  updateSharePerm(shareViewModel.selectedDoc().perms.read.users, user);
+}
+
+function removeUserWriteShare(user) {
+  updateSharePerm(shareViewModel.selectedDoc().perms.write.users, user);
+}
+
+function removeGroupReadShare(group) {
+  updateSharePerm(shareViewModel.selectedDoc().perms.read.groups, group);
+}
+
+function removeGroupWriteShare(group) {
+  updateSharePerm(shareViewModel.selectedDoc().perms.write.groups, group);
+}
+
+function changeDocumentSharePerm(perm) {
+  shareViewModel.selectedPerm(perm);
+}
+
+function handleTypeaheadSelection() {
+  if (selectedUserOrGroup != null) {
+    if (selectedUserOrGroup.hasOwnProperty("username")) {
+      shareViewModel.selectedDoc().perms[shareViewModel.selectedPerm()].users.push(selectedUserOrGroup);
+    }
+    else {
+      shareViewModel.selectedDoc().perms[shareViewModel.selectedPerm()].groups.push(selectedUserOrGroup);
+    }
+    shareViewModel.selectedDoc.valueHasMutated();
+    shareDocFinal();
+  }
+  selectedUserOrGroup = null;
+  $("#documentShareTypeahead").val("");
+}
+
+function shareDocFinal() {
+  var _postPerms = {
+    read: {
+      user_ids: [],
+      group_ids: []
+    },
+    write: {
+      user_ids: [],
+      group_ids: []
+    }
+  }
+
+  $(shareViewModel.selectedDoc().perms.read.users).each(function (cnt, item) {
+    _postPerms.read.user_ids.push(item.id);
+  });
+
+  $(shareViewModel.selectedDoc().perms.read.groups).each(function (cnt, item) {
+    _postPerms.read.group_ids.push(item.id);
+  });
+
+  $(shareViewModel.selectedDoc().perms.write.users).each(function (cnt, item) {
+    _postPerms.write.user_ids.push(item.id);
+  });
+
+  $(shareViewModel.selectedDoc().perms.write.groups).each(function (cnt, item) {
+    _postPerms.write.group_ids.push(item.id);
+  });
+
+  $.post("/desktop/api2/doc/share", {
+    doc_id: shareViewModel.selectedDoc().id,
+    data: JSON.stringify(_postPerms)
+  }, function (response) {
+    if (response != null) {
+      if (response.status != 0) {
+        $(document).trigger("error", "There was an error processing your action: " + response.message);
+      }
+      else {
+        shareViewModel.updateDoc(response.doc);
+      }
+    }
+  }).fail(function (response) {
+    $(document).trigger("error", "There was an error processing your action: " + response.responseText);
+  });
+}

+ 72 - 0
desktop/core/src/desktop/templates/common_share2.mako

@@ -0,0 +1,72 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+<%!
+from django.utils.translation import ugettext as _
+%>
+
+<div id="documentShareModal" class="modal hide fade">
+  <!-- ko if: selectedDoc() -->
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3>${_('Sharing settings')}</h3>
+  </div>
+  <div class="modal-body">
+    <div class="row-fluid">
+      <div class="span6">
+        <h4 class="muted" style="margin-top:0px">${_('Read')}</h4>
+        <div data-bind="visible: (selectedDoc().perms.read.users.length == 0 && selectedDoc().perms.read.groups.length == 0)">${_('The document is not shared for read.')}</div>
+        <ul class="unstyled airy" data-bind="foreach: selectedDoc().perms.read.users">
+          <li><span class="badge badge-info badge-left"><i class="fa fa-user"></i> <span data-bind="text: prettifyUsername(id), css:{'notpretty': prettifyUsername(id) == ''}, attr:{'data-id': id}"></span></span><span class="badge badge-right trash-share" data-bind="click: removeUserReadShare"> <i class="fa fa-times"></i></li>
+        </ul>
+        <ul class="unstyled airy" data-bind="foreach: selectedDoc().perms.read.groups">
+          <li><span class="badge badge-info badge-left"><i class="fa fa-users"></i> ${ _('Group') } &quot;<span data-bind="text: name"></span>&quot;</span><span class="badge badge-right trash-share" data-bind="click: removeGroupReadShare"> <i class="fa fa-times"></i></li>
+        </ul>
+      </div>
+
+      <div class="span6">
+        <h4 class="muted" style="margin-top:0px">${_('Read and Modify')}</h4>
+        <div data-bind="visible: (selectedDoc().perms.write.users.length == 0 && selectedDoc().perms.write.groups.length == 0)">${_('The document is not shared for read and modify.')}</div>
+        <ul class="unstyled airy" data-bind="foreach: selectedDoc().perms.write.users">
+          <li><span class="badge badge-info badge-left"><i class="fa fa-user"></i> <span data-bind="text: prettifyUsername(id), css:{'notpretty': prettifyUsername(id) == ''}, attr:{'data-id': id}"></span></span><span class="badge badge-right trash-share" data-bind="click: removeUserWriteShare"> <i class="fa fa-times"></i></li>
+        </ul>
+        <ul class="unstyled airy" data-bind="foreach: selectedDoc().perms.write.groups">
+          <li><span class="badge badge-info badge-left"><i class="fa fa-users"></i> ${ _('Group') } &quot;<span data-bind="text: name"></span>&quot;</span><span class="badge badge-right trash-share" data-bind="click: removeGroupWriteShare"> <i class="fa fa-times"></i></li>
+        </ul>
+      </div>
+
+    </div>
+    <div class="clearfix"></div>
+    <div style="margin-top: 20px">
+      <div class="input-append">
+        <input id="documentShareTypeahead" type="text" style="width: 460px" placeholder="${_('Type a username or a group name')}" disabled="disabled">
+        <div class="btn-group">
+          <a id="documentShareAddBtn" class="btn disabled"><i class="fa fa-plus-circle"></i> <span data-bind="text: selectedPermLabel"></span></a>
+          <a id="documentShareCaret" class="btn dropdown-toggle disabled" data-toggle="dropdown">
+            <span class="caret"></span>
+          </a>
+          <ul class="dropdown-menu">
+            <li><a data-bind="click: changeDocumentSharePerm.bind(null, 'read')" href="javascript:void(0)">${ _('Read') }</a></li>
+            <li><a data-bind="click: changeDocumentSharePerm.bind(null, 'write')" href="javascript:void(0)">${ _('Read and Modify') }</a></li>
+          </ul>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <a href="#" data-dismiss="modal" class="btn btn-primary disable-feedback disable-enter">${_('Done')}</a>
+  </div>
+  <!-- /ko -->
+</div>

+ 23 - 4
desktop/core/src/desktop/templates/home2.mako

@@ -14,7 +14,7 @@
 ## See the License for the specific language governing permissions and
 ## limitations under the License.
 <%!
-  from desktop.views import commonheader, commonfooter, commonshare, _ko
+  from desktop.views import commonheader, commonfooter, commonshare2, _ko
   from django.utils.translation import ugettext as _
 %>
 
@@ -142,9 +142,16 @@ ${ commonheader(_('Welcome Home'), "home", user) | n,unicode }
         <span data-bind="text: path"></span>
         <br/>
         <input data-bind="value: mkdirFormPath" placeholder="dir name, e.g. projects"></input>
-        <a href="javascript(0);" class="btn" data-bind="click: mkdir"><i class="fa fa-plus-circle"></i> ${ _('Create Directory') }</a>
+        <a href="javascript:void(0);" class="btn" data-bind="click: mkdir"><i class="fa fa-plus-circle"></i> ${ _('Create Directory') }</a>
+
         <input data-bind="value: deleteFormPath" placeholder="doc id, e.g. 50491"></input>
-        <a href="javascript(0);" class="btn" data-bind="click: deleteDocument"><i class="fa fa-times"></i> ${ _('Delete') }</a>
+        <a href="javascript:void(0);" class="btn" data-bind="click: deleteDocument"><i class="fa fa-times"></i> ${ _('Delete') }</a>
+
+        <input data-bind="value: shareFormDocId" placeholder="doc id, e.g. 50491"></input>
+        <a class="share-link btn" rel="tooltip" data-placement="bottom" style="margin-left:20px" data-bind="click: function(e){ prepareShareModal(e) },
+          attr: {'data-original-title': '${ _ko("Share") } ' + name}">
+          <i class="fa fa-users"></i> ${ _('Share') }
+        </a>
 
         <div class="card-body">
           <p>
@@ -186,7 +193,7 @@ ${ commonheader(_('Welcome Home'), "home", user) | n,unicode }
     </div>
 
   </div>
-
+</div>
 
 
 <script type="text/html" id="document-template">
@@ -203,10 +210,14 @@ ${ commonheader(_('Welcome Home'), "home", user) | n,unicode }
 </script>
 
 
+${ commonshare2() | n,unicode }
+
+
 <script src="${ static('desktop/ext/js/datatables-paging-0.1.js') }" type="text/javascript" charset="utf-8"></script>
 <script src="${ static('desktop/ext/js/knockout.min.js') }" type="text/javascript" charset="utf-8"></script>
 <script src="${ static('desktop/ext/js/knockout-mapping.min.js') }" type="text/javascript" charset="utf-8"></script>
 <script src="${ static('desktop/js/home2.vm.js') }"></script>
+<script src="${ static('desktop/js/share2.vm.js') }"></script>
 
 
 <script type="text/javascript" charset="utf-8">
@@ -216,7 +227,15 @@ ${ commonheader(_('Welcome Home'), "home", user) | n,unicode }
     viewModel = new HomeViewModel();
     ko.applyBindings(viewModel, $('#documentList')[0]);
 
+    shareViewModel = initSharing("#documentShareModal");
+    shareViewModel.setDocId(-1);
+
     viewModel.loadDocuments(location.getParameter('path') ? location.getParameter('path') : '/');
+    
+    prepareShareModal = function(job) {
+      shareViewModel.setDocId(viewModel.shareFormDocId());
+      openShareModal();
+    };
   });
 </script>
 

+ 1 - 0
desktop/core/src/desktop/urls.py

@@ -106,6 +106,7 @@ dynamic_patterns += patterns('desktop.api2',
   (r'^desktop/api2/doc/move$', 'move_document'),
   (r'^desktop/api2/doc/mkdir$', 'create_directory'),
   (r'^desktop/api2/doc/delete$', 'delete_document'),
+  (r'^desktop/api2/doc/share$', 'share_document'),
 
   (r'^desktop/api2/doc/export$', 'export_documents'),
   (r'^desktop/api2/doc/import$', 'import_documents'),

+ 3 - 0
desktop/core/src/desktop/views.py

@@ -396,6 +396,9 @@ def commonheader(title, section, user, padding="90px", skip_topbar=False):
 def commonshare():
   return django_mako.render_to_string("common_share.mako", {})
 
+def commonshare2():
+  return django_mako.render_to_string("common_share2.mako", {})
+
 def commonimportexport(request):
   return django_mako.render_to_string("common_import_export.mako", {'request': request})