Explorar el Código

[doc2] Add API endpoint to get shared documents

Jenny Kim hace 9 años
padre
commit
e5aabb3bff

+ 99 - 25
desktop/core/src/desktop/api2.py

@@ -82,38 +82,61 @@ def get_documents(request):
   # Check if user has read permissions
   document.can_read_or_exception(request.user)
 
-  # Get querystring filters if any
-  page = int(request.GET.get('page', 1))
-  limit = int(request.GET.get('limit', 0))
-  type_filters = request.GET.getlist('type', None)
-  sort = request.GET.get('sort', '-last_modified')
-  search_text = request.GET.get('text', None)
+  response = {
+    'document': document.to_dict(),
+    'parent': document.parent_directory.to_dict() if document.parent_directory else None,
+    'children': [],
+  }
 
   # Get children documents if this is a directory
-  children = None
-  count = 0
   if document.is_directory:
     directory = Directory.objects.get(id=document.id)
-    children = directory.documents(types=type_filters, search_text=search_text, order_by=sort)
-    count = children.count()
+    children = directory.get_children_documents()
+    # Refine results
+    response.update(_refine_documents(request, queryset=children, key="children"))
 
   # Paginate
-  if children and limit > 0:
-    offset = (page - 1) * limit
-    last = offset + limit
-    children = children.all()[offset:last]
+  response.update(_paginate(request, queryset=response['children'], key="children"))
 
-  return JsonResponse({
-      'document': document.to_dict(),
-      'parent': document.parent_directory.to_dict() if document.parent_directory else None,
-      'children': [doc.to_dict() for doc in children] if children else [],
-      'page': page,
-      'limit': limit,
-      'count': count,
-      'types': type_filters,
-      'sort': sort,
-      'text': search_text
-  })
+  # Serialize Results
+  if response['children']:
+    response['children'] = [doc.to_dict() for doc in response['children']]
+
+  return JsonResponse(response)
+
+
+@api_error_handler
+def get_shared_documents(request):
+  """
+  Returns the directories and documents that are shared with the current user, grouped by top-level directory.
+  Optional params:
+    page=<n>    - Controls pagination. Defaults to 1.
+    limit=<n>   - Controls limit per page. Defaults to all.
+    type=<type> - Show documents of given type(s) (directory, query-hive, query-impala, query-mysql, etc). Default to all.
+    sort=<key>  - Sort by the attribute <key>, which is one of:
+                    "name", "type", "owner", "last_modified"
+                  Accepts the form "-last_modified", which sorts in descending order.
+                  Default to "-last_modified".
+    text=<frag> - Search for fragment "frag" in names and descriptions.
+  """
+
+  response = {
+    'documents': [],
+  }
+
+  documents = Document2.objects.get_shared_documents(request.user, flatten=False)
+
+  # Refine results
+  response.update(_refine_documents(request, queryset=documents))
+
+  # Paginate
+  response.update(_paginate(request, queryset=response['documents']))
+
+  # Serialize results
+  if response['documents']:
+    response['documents'] = [doc.to_dict() for doc in response['documents']]
+
+  return JsonResponse(response)
 
 
 @api_error_handler
@@ -331,6 +354,57 @@ def import_documents(request):
     return JsonResponse({'message': stdout.getvalue()})
 
 
+def _refine_documents(request, queryset, key="documents"):
+  """
+  Given optional querystring params extracted from the request, filter the given queryset of documents and return a
+    dictionary with the refined queryset and filter params
+  :param request: request object with params
+  :param queryset: Document2 queryset
+  :param key: name for key of refined document set
+  """
+  documents = []
+  count = 0
+  type_filters = request.GET.getlist('type', None)
+  sort = request.GET.get('sort', '-last_modified')
+  search_text = request.GET.get('text', None)
+
+  if queryset:
+    documents = Document2.objects.refine_documents(documents=queryset, types=type_filters, search_text=search_text,
+                                                   order_by=sort)
+    count = documents.count()
+
+  return {
+    key: documents,
+    'count': count,
+    'types': type_filters,
+    'text': search_text,
+    'sort': sort
+  }
+
+
+def _paginate(request, queryset, key="documents"):
+  """
+  Given optional querystring params extracted from the request, slice the given queryset of documents for the given page
+    and limit, and return the updated queryset along with pagination params used.
+  :param request: request object with params
+  :param queryset: queryset
+  :param key: name for key of refined document set
+  """
+  page = int(request.GET.get('page', 1))
+  limit = int(request.GET.get('limit', 0))
+
+  if queryset and limit > 0:
+    offset = (page - 1) * limit
+    last = offset + limit
+    queryset = queryset.all()[offset:last]
+
+  return {
+    key: queryset,
+    'page': page,
+    'limit': limit
+  }
+
+
 def _convert_documents(user):
   """
   Given a user, converts any existing Document objects to Document2 objects

+ 37 - 17
desktop/core/src/desktop/models.py

@@ -766,10 +766,44 @@ class Document2Manager(models.Manager):
     return Document2.objects.filter(
         Q(owner=user) |
         Q(document2permission__users=user) |
-        Q(document2permission__groups__in=user.groups.all()) |
-        Q(document2permission__all=True)
+        Q(document2permission__groups__in=user.groups.all())
     ).distinct().order_by('-last_modified')
 
+  def get_shared_documents(self, user, flatten=False):
+    """
+    Returns all documents that are shared with the user
+    :param flatten: True to return all directories and documents in a flat list,
+                    False to filter the documents to top-level directories
+    """
+    documents = Document2.objects.filter(
+        Q(document2permission__users=user) |
+        Q(document2permission__groups__in=user.groups.all())
+    ).distinct()
+
+    if not flatten:
+      documents = documents.exclude(parent_directory__in=documents)
+
+    return documents
+
+  def refine_documents(self, documents, types=None, search_text=None, order_by=None):
+    """
+    Refines a queryset of document objects by type filters, search_text or order_by
+    :param documents: queryset of Document2 objects
+    :param types: list of Document2 types (e.g. - query-hive, directory, etc)
+    :param search_text: text to search on in the name and description fields
+    :param order_by: order by field (e.g. -last_modified, type)
+    """
+    if types and isinstance(types, list):
+      documents = documents.filter(type__in=types)
+
+    if search_text:
+      documents = documents.filter(Q(name__icontains=search_text) | Q(description__icontains=search_text))
+
+    if order_by:  # TODO: Validate that order_by is a valid sort parameter
+      documents = documents.order_by(order_by)
+
+    return documents
+
   def get_by_natural_key(self, uuid, version, is_history):
     return self.get(uuid=uuid, version=version, is_history=is_history)
 
@@ -1134,29 +1168,15 @@ class Directory(Document2):
   class Meta:
     proxy = True
 
-  def documents(self, types=None, search_text=None, order_by=None):
+  def get_children_documents(self):
     """
     Returns the children documents for a given directory, excluding history documents
-    :param types: document types to filter on (e.g. - query-hive, link-pig, etc)
-    :param search_text: search for given text in name and description fields
-    :param order_by: order by field (e.g. -last_modified, type)
     """
     documents = self.children.filter(is_history=False)  # TODO: perms
-
-    if types and isinstance(types, list):
-      documents = documents.filter(type__in=types)
-
-    if search_text:
-      documents = documents.filter(Q(name__icontains=search_text) | Q(description__icontains=search_text))
-
-    if order_by:  # TODO: Validate that order_by is a valid sort parameter
-      documents = documents.order_by(order_by)
-
     return documents
 
   def save(self, *args, **kwargs):
     self.type = 'directory'
-
     super(Directory, self).save(*args, **kwargs)
 
 

+ 76 - 4
desktop/core/src/desktop/tests_doc2.py

@@ -453,10 +453,10 @@ class TestDocument2Permissions(object):
   def test_share_directory(self):
     # Test that updating the permissions for a directory updates all nested documents accordingly, with file structure:
     # /
-    # /test_dir
-    # /test_dir/query1.sql
-    # /test_dir/nested_dir
-    # /test_dir/nested_dir/query2.sql
+    #   test_dir/
+    #     query1.sql
+    #     nested_dir/
+    #       query2.sql
 
     # All initially owned by self.user
     parent_dir = Directory.objects.create(name='test_dir', owner=self.user, parent_directory=self.home_dir)
@@ -493,3 +493,75 @@ class TestDocument2Permissions(object):
       assert_true(doc.can_write(self.user))
       assert_true(doc.can_read(self.user_not_me))
       assert_true(doc.can_write(self.user_not_me))
+
+
+  def test_get_shared_documents(self):
+    not_shared = Document2.objects.create(name='query1.sql', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+    shared_1 = Document2.objects.create(name='query2.sql', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+    shared_2 = Document2.objects.create(name='query3.sql', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+
+    shared_1.share(user=self.user, name='read', users=[self.user_not_me], groups=[])
+    shared_2.share(user=self.user, name='read', users=[self.user_not_me], groups=[])
+
+    # 2 shared docs should appear in the other user's shared documents response
+    response = self.client_not_me.get('/desktop/api2/docs/shared')
+    data = json.loads(response.content)
+    assert_true('documents' in data)
+    assert_equal(2, data['count'])
+    doc_names = [doc['name'] for doc in data['documents']]
+    assert_true('query2.sql' in doc_names)
+    assert_true('query3.sql' in doc_names)
+    assert_false('query1.sql' in doc_names)
+
+    # they should not appear in the other user's regular get_documents response
+    response = self.client_not_me.get('/desktop/api2/docs/')
+    data = json.loads(response.content)
+    doc_names = [doc['name'] for doc in data['children']]
+    assert_false('query2.sql' in doc_names)
+    assert_false('query3.sql' in doc_names)
+
+
+  def test_get_shared_directories(self):
+    # Tests that when fetching the shared documents for a user, they are grouped by top-level directory when possible
+    # /
+    #   dir1/
+    #     query1.sql
+    #   dir2/
+    #     dir3/
+    #       query2.sql
+    #   query3.sql
+
+    dir1 = Directory.objects.create(name='dir1', owner=self.user, parent_directory=self.home_dir)
+    doc1 = Document2.objects.create(name='query1.sql', type='query-hive', owner=self.user, data={}, parent_directory=dir1)
+    dir2 = Directory.objects.create(name='dir2', owner=self.user, parent_directory=self.home_dir)
+    dir3 = Directory.objects.create(name='dir3', owner=self.user, parent_directory=dir2)
+    doc2 = Document2.objects.create(name='query2.sql', type='query-hive', owner=self.user, data={}, parent_directory=dir3)
+    doc3 = Document2.objects.create(name='query3.sql', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+
+    dir1.share(user=self.user, name='read', users=[], groups=[self.default_group])
+    dir3.share(user=self.user, name='read', users=[], groups=[self.default_group])
+    doc3.share(user=self.user, name='read', users=[], groups=[self.default_group])
+
+    # 3 shared docs should appear, due to directory rollup
+    response = self.client_not_me.get('/desktop/api2/docs/shared')
+    data = json.loads(response.content)
+    assert_true('documents' in data)
+    assert_equal(3, data['count'], data)
+    doc_names = [doc['name'] for doc in data['documents']]
+    assert_true('dir1' in doc_names)
+    assert_true('dir3' in doc_names)
+    assert_true('query3.sql' in doc_names)
+    assert_false('dir2' in doc_names)
+
+    # nested documents should not appear
+    assert_false('query1.sql' in doc_names)
+    assert_false('query2.sql' in doc_names)
+
+    # but nested documents should still be shared/viewable by group
+    response = self.client_not_me.get('/desktop/api2/doc/get', {'uuid': doc1.uuid})
+    data = json.loads(response.content)
+    assert_equal(doc1.uuid, data['uuid'], data)
+
+    response = self.client_not_me.get('/desktop/api2/doc/get', {'uuid': doc2.uuid})
+    data = json.loads(response.content)
+    assert_equal(doc2.uuid, data['uuid'], data)

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

@@ -117,8 +117,9 @@ dynamic_patterns += patterns('desktop.api',
 
 dynamic_patterns += patterns('desktop.api2',
   (r'^desktop/api2/docs/?$', 'get_documents'),
-  (r'^desktop/api2/doc/get$', 'get_document'),
+  (r'^desktop/api2/docs/shared?$', 'get_shared_documents'),
 
+  (r'^desktop/api2/doc/get$', 'get_document'),
   (r'^desktop/api2/doc/move$', 'move_document'),
   (r'^desktop/api2/doc/mkdir$', 'create_directory'),
   (r'^desktop/api2/doc/delete$', 'delete_document'),