Bladeren bron

HUE-950 [core] Upgrading to the new trash and permission API

Converting the Pig app, Beeswax, Impala and Oozie apps
Romain Rigaux 12 jaren geleden
bovenliggende
commit
c9e7ed0e1a
34 gewijzigde bestanden met toevoegingen van 755 en 322 verwijderingen
  1. 0 6
      apps/beeswax/src/beeswax/conf.py
  2. 13 3
      apps/beeswax/src/beeswax/models.py
  3. 2 2
      apps/beeswax/src/beeswax/templates/list_designs.mako
  4. 3 3
      apps/beeswax/src/beeswax/templates/list_trashed_designs.mako
  5. 1 1
      apps/beeswax/src/beeswax/templates/my_queries.mako
  6. 47 51
      apps/beeswax/src/beeswax/views.py
  7. 0 7
      apps/oozie/src/oozie/conf.py
  8. 4 3
      apps/oozie/src/oozie/forms.py
  9. 39 39
      apps/oozie/src/oozie/models.py
  10. 3 1
      apps/oozie/src/oozie/templates/editor/create_bundle.mako
  11. 2 2
      apps/oozie/src/oozie/templates/editor/create_coordinator.mako
  12. 2 2
      apps/oozie/src/oozie/templates/editor/create_workflow.mako
  13. 2 2
      apps/oozie/src/oozie/templates/editor/edit_bundle.mako
  14. 2 2
      apps/oozie/src/oozie/templates/editor/edit_coordinator.mako
  15. 3 1
      apps/oozie/src/oozie/templates/editor/edit_workflow.mako
  16. 3 3
      apps/oozie/src/oozie/templates/editor/list_bundles.mako
  17. 2 2
      apps/oozie/src/oozie/templates/editor/list_coordinators.mako
  18. 2 1
      apps/oozie/src/oozie/templates/editor/list_trashed_bundles.mako
  19. 5 4
      apps/oozie/src/oozie/templates/editor/list_trashed_coordinators.mako
  20. 8 7
      apps/oozie/src/oozie/templates/editor/list_trashed_workflows.mako
  21. 3 3
      apps/oozie/src/oozie/templates/editor/list_workflows.mako
  22. 1 1
      apps/oozie/src/oozie/urls.py
  23. 1 0
      apps/oozie/src/oozie/views/api.py
  24. 27 67
      apps/oozie/src/oozie/views/editor.py
  25. 21 9
      apps/pig/src/pig/models.py
  26. 15 7
      apps/pig/src/pig/views.py
  27. 0 8
      desktop/conf.dist/hue.ini
  28. 0 8
      desktop/conf/pseudo-distributed.ini.tmpl
  29. 30 0
      desktop/core/src/desktop/management/commands/sync_documents.py
  30. 171 0
      desktop/core/src/desktop/migrations/0007_auto__add_documentpermission__add_documenttag__add_document.py
  31. 232 0
      desktop/core/src/desktop/models.py
  32. 31 49
      desktop/core/src/desktop/templates/home.mako
  33. 35 23
      desktop/core/src/desktop/urls.py
  34. 45 5
      desktop/core/src/desktop/views.py

+ 0 - 6
apps/beeswax/src/beeswax/conf.py

@@ -65,12 +65,6 @@ BROWSE_PARTITIONED_TABLE_LIMIT = Config(
   type=int,
   type=int,
   help=_t('Set a LIMIT clause when browsing a partitioned table. A positive value will be set as the LIMIT. If 0 or negative, do not set any limit.'))
   help=_t('Set a LIMIT clause when browsing a partitioned table. A positive value will be set as the LIMIT. If 0 or negative, do not set any limit.'))
 
 
-SHARE_SAVED_QUERIES = Config(
-  key='share_saved_queries',
-  default=True,
-  type=coerce_bool,
-  help=_t('Share saved queries with all users. If set to false, saved queries are visible only to the owner and administrators.'))
-
 
 
 def config_validator(user):
 def config_validator(user):
   # dbms is dependent on beeswax.conf (this file)
   # dbms is dependent on beeswax.conf (this file)

+ 13 - 3
apps/beeswax/src/beeswax/models.py

@@ -22,11 +22,14 @@ import logging
 
 
 from django.db import models
 from django.db import models
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
+from django.contrib.contenttypes import generic
+from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 
 
 from enum import Enum
 from enum import Enum
 
 
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.exceptions_renderable import PopupException
+from desktop.models import Document
 
 
 from beeswax.design import HQLdesign, hql_query
 from beeswax.design import HQLdesign, hql_query
 from TCLIService.ttypes import TSessionHandle, THandleIdentifier,\
 from TCLIService.ttypes import TSessionHandle, THandleIdentifier,\
@@ -73,6 +76,7 @@ class QueryHistory(models.Model):
   design = models.ForeignKey('SavedQuery', to_field='id', null=True) # Some queries (like read/create table) don't have a design
   design = models.ForeignKey('SavedQuery', to_field='id', null=True) # Some queries (like read/create table) don't have a design
   notify = models.BooleanField(default=False)                        # Notify on completion
   notify = models.BooleanField(default=False)                        # Notify on completion
 
 
+
   class Meta:
   class Meta:
     ordering = ['-submission_date']
     ordering = ['-submission_date']
 
 
@@ -87,8 +91,9 @@ class QueryHistory(models.Model):
   def get(id):
   def get(id):
     return HiveServerQueryHistory.objects.get(id=id)
     return HiveServerQueryHistory.objects.get(id=id)
 
 
-  def get_type_name(self):
-    if self.query_type == 1:
+  @staticmethod
+  def get_type_name(query_type):
+    if query_type == IMPALA:
       return 'impala'
       return 'impala'
     else:
     else:
       return 'beeswax'
       return 'beeswax'
@@ -96,7 +101,7 @@ class QueryHistory(models.Model):
   def get_query_server_config(self):
   def get_query_server_config(self):
     from beeswax.server.dbms import get_query_server_config
     from beeswax.server.dbms import get_query_server_config
 
 
-    query_server = get_query_server_config(self.get_type_name())
+    query_server = get_query_server_config(QueryHistory.get_type_name(self.query_type))
     query_server.update({
     query_server.update({
         'server_name': self.server_name,
         'server_name': self.server_name,
         'server_host': self.server_host,
         'server_host': self.server_host,
@@ -213,6 +218,8 @@ class SavedQuery(models.Model):
   is_trashed = models.BooleanField(default=False, db_index=True, verbose_name=_t('Is trashed'),
   is_trashed = models.BooleanField(default=False, db_index=True, verbose_name=_t('Is trashed'),
                                    help_text=_t('If this query is trashed.'))
                                    help_text=_t('If this query is trashed.'))
 
 
+  doc = generic.GenericRelation(Document, related_name='hql_doc')
+
   class Meta:
   class Meta:
     ordering = ['-mtime']
     ordering = ['-mtime']
 
 
@@ -276,6 +283,9 @@ class SavedQuery(models.Model):
     except:
     except:
       return ""
       return ""
 
 
+  def get_absolute_url(self):
+    return reverse(QueryHistory.get_type_name(self.type) + ':execute_query', kwargs={'design_id': self.id})
+
 
 
 class SessionManager(models.Manager):
 class SessionManager(models.Manager):
   def get_session(self, user, application='beeswax'):
   def get_session(self, user, application='beeswax'):

+ 2 - 2
apps/beeswax/src/beeswax/templates/list_designs.mako

@@ -56,9 +56,9 @@ ${layout.menubar(section='saved queries')}
       </%def>
       </%def>
 
 
       <%def name="creation()">
       <%def name="creation()">
-        <div class="btn-toolbar" style="display: inline; vertical-align: middle">
-          <a class="btn" href="${ url(app_name + ':list_trashed_designs') }" title="${_('Go to the trash')}"><i class="icon-trash"></i> ${_('View trash')}</a>
+        <div class="btn-toolbar" style="display: inline; vertical-align: middle">          
           <a class="btn" href="${ url(app_name + ':execute_query') }" title="${_('Create new query')}"><i class="icon-plus-sign"></i> ${_('New query')}</a>
           <a class="btn" href="${ url(app_name + ':execute_query') }" title="${_('Create new query')}"><i class="icon-plus-sign"></i> ${_('New query')}</a>
+          <a class="btn" href="${ url(app_name + ':list_trashed_designs') }" title="${_('Go to the trash')}"><i class="icon-trash"></i> ${_('View trash')}</a>
         </div>
         </div>
       </%def>
       </%def>
     </%actionbar:render>
     </%actionbar:render>

+ 3 - 3
apps/beeswax/src/beeswax/templates/list_trashed_designs.mako

@@ -50,11 +50,11 @@ ${layout.menubar(section='saved queries')}
 
 
       <%def name="creation()">
       <%def name="creation()">
         <div class="btn-toolbar" style="display: inline; vertical-align: middle">
         <div class="btn-toolbar" style="display: inline; vertical-align: middle">
-          <button id="viewQueriesBtn" class="btn" title="${_('View queries')}">
-            <i class="icon-home"></i> ${_('View queries')}
-          </button>
           <button id="emptyTrashBtn" class="btn" title="${_('Empty trash')}" data-bind="enabled: availableSavedQueries().length > 0">
           <button id="emptyTrashBtn" class="btn" title="${_('Empty trash')}" data-bind="enabled: availableSavedQueries().length > 0">
             <i class="icon-fire"></i> ${_('Empty trash')}
             <i class="icon-fire"></i> ${_('Empty trash')}
+          </button>        
+          <button id="viewQueriesBtn" class="btn" title="${_('View queries')}">
+            <i class="icon-home"></i> ${_('Back')}
           </button>
           </button>
         </div>
         </div>
       </%def>
       </%def>

+ 1 - 1
apps/beeswax/src/beeswax/templates/my_queries.mako

@@ -102,7 +102,7 @@ ${layout.menubar(section='my queries')}
             </td>
             </td>
             <td>
             <td>
               % if design.desc:
               % if design.desc:
-              ${design.desc}
+                ${ design.desc }
               % endif
               % endif
             </td>
             </td>
             <td data-sort-value="${time.mktime(design.mtime.timetuple())}">${ timesince(design.mtime) } ${_('ago')}</td>
             <td data-sort-value="${time.mktime(design.mtime.timetuple())}">${ timesince(design.mtime) } ${_('ago')}</td>

+ 47 - 51
apps/beeswax/src/beeswax/views.py

@@ -35,6 +35,7 @@ from desktop.lib.paginator import Paginator
 from desktop.lib.django_util import copy_query_dict, format_preserving_redirect, render
 from desktop.lib.django_util import copy_query_dict, format_preserving_redirect, render
 from desktop.lib.django_util import login_notrequired, get_desktop_uri_prefix
 from desktop.lib.django_util import login_notrequired, get_desktop_uri_prefix
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.exceptions_renderable import PopupException
+from desktop.models import Document
 
 
 from jobsub.parameterization import find_variables, substitute_variables
 from jobsub.parameterization import find_variables, substitute_variables
 
 
@@ -42,7 +43,7 @@ import beeswax.forms
 import beeswax.design
 import beeswax.design
 import beeswax.management.commands.beeswax_install_examples
 import beeswax.management.commands.beeswax_install_examples
 
 
-from beeswax import common, data_export, models, conf
+from beeswax import common, data_export, models
 from beeswax.forms import QueryForm
 from beeswax.forms import QueryForm
 from beeswax.design import HQLdesign
 from beeswax.design import HQLdesign
 from beeswax.models import SavedQuery, make_query_context, QueryHistory
 from beeswax.models import SavedQuery, make_query_context, QueryHistory
@@ -58,7 +59,6 @@ LOG = logging.getLogger(__name__)
 def index(request):
 def index(request):
   return execute_query(request)
   return execute_query(request)
 
 
-
 """
 """
 Design views
 Design views
 """
 """
@@ -111,6 +111,9 @@ def save_design(request, form, type, design, explicit_save):
   design.data = new_data
   design.data = new_data
 
 
   design.save()
   design.save()
+  design.doc.update(name=design.name, description=design.desc)
+
+  Document.objects.link(design, owner=design.owner, name=design.name, extra=design.type)
 
 
   LOG.info('Saved %s design "%s" (id %s) for %s' %
   LOG.info('Saved %s design "%s" (id %s) for %s' %
            (explicit_save and '' or 'auto ', design.name, design.id, design.owner))
            (explicit_save and '' or 'auto ', design.name, design.id, design.owner))
@@ -136,6 +139,7 @@ def save_design_properties(request):
     elif field == 'description':
     elif field == 'description':
       design.desc = request.POST.get('value')
       design.desc = request.POST.get('value')
     design.save()
     design.save()
+    design.doc.update(name=design.name, description=design.desc)
     response['status'] = 0
     response['status'] = 0
   except Exception, e:
   except Exception, e:
     response['data'] = str(e)
     response['data'] = str(e)
@@ -146,17 +150,17 @@ def save_design_properties(request):
 def delete_design(request):
 def delete_design(request):
   if request.method == 'POST':
   if request.method == 'POST':
     ids = request.POST.getlist('designs_selection')
     ids = request.POST.getlist('designs_selection')
-    designs = dict([(design_id, authorized_get_design(request, design_id)) for design_id in ids])
-
+    designs = dict([(design_id, authorized_get_design(request, design_id, owner_only=True)) for design_id in ids])
+    print designs
     if None in designs.values():
     if None in designs.values():
       LOG.error('Cannot delete non-existent design(s) %s' % ','.join([key for key, name in designs.items() if name is None]))
       LOG.error('Cannot delete non-existent design(s) %s' % ','.join([key for key, name in designs.items() if name is None]))
       return list_designs(request)
       return list_designs(request)
 
 
     for design in designs.values():
     for design in designs.values():
       if request.POST.get('skipTrash', 'false') == 'false':
       if request.POST.get('skipTrash', 'false') == 'false':
-        design.is_trashed = True
-        design.save()
+        design.doc.get().send_to_trash()
       else:
       else:
+        design.doc.all().delete()
         design.delete()
         design.delete()
     return redirect(reverse(get_app_name(request) + ':list_designs'))
     return redirect(reverse(get_app_name(request) + ':list_designs'))
   else:
   else:
@@ -173,8 +177,7 @@ def restore_design(request):
       return list_designs(request)
       return list_designs(request)
 
 
     for design in designs.values():
     for design in designs.values():
-      design.is_trashed = False
-      design.save()
+      design.doc.get().restore_from_trash()
     return redirect(reverse(get_app_name(request) + ':list_designs'))
     return redirect(reverse(get_app_name(request) + ':list_designs'))
   else:
   else:
     return render('confirm.html', request, dict(url=request.path, title=_('Restore design(s)?')))
     return render('confirm.html', request, dict(url=request.path, title=_('Restore design(s)?')))
@@ -193,6 +196,7 @@ def clone_design(request, design_id):
   copy.owner = request.user
   copy.owner = request.user
   copy.save()
   copy.save()
   messages.info(request, _('Copied design: %(name)s') % {'name': design.name})
   messages.info(request, _('Copied design: %(name)s') % {'name': design.name})
+
   return format_preserving_redirect(
   return format_preserving_redirect(
       request, reverse(get_app_name(request) + ':execute_query', kwargs={'design_id': copy.id}))
       request, reverse(get_app_name(request) + ':execute_query', kwargs={'design_id': copy.id}))
 
 
@@ -218,16 +222,11 @@ def list_designs(request):
   DEFAULT_PAGE_SIZE = 20
   DEFAULT_PAGE_SIZE = 20
   app_name= get_app_name(request)
   app_name= get_app_name(request)
 
 
-  if conf.SHARE_SAVED_QUERIES.get() or request.user.is_superuser:
-    user = None
-  else:
-    user = request.user
-
   # Extract the saved query list.
   # Extract the saved query list.
   prefix = 'q-'
   prefix = 'q-'
   querydict_query = _copy_prefix(prefix, request.GET)
   querydict_query = _copy_prefix(prefix, request.GET)
   # Manually limit up the user filter.
   # Manually limit up the user filter.
-  querydict_query[ prefix + 'user' ] = user
+  querydict_query[ prefix + 'user' ] = request.user
   querydict_query[ prefix + 'type' ] = app_name
   querydict_query[ prefix + 'type' ] = app_name
   page, filter_params = _list_designs(querydict_query, DEFAULT_PAGE_SIZE, prefix)
   page, filter_params = _list_designs(querydict_query, DEFAULT_PAGE_SIZE, prefix)
 
 
@@ -276,7 +275,7 @@ def my_queries(request):
   prefix = 'h-'
   prefix = 'h-'
   querydict_history = _copy_prefix(prefix, request.GET)
   querydict_history = _copy_prefix(prefix, request.GET)
   # Manually limit up the user filter.
   # Manually limit up the user filter.
-  querydict_history[ prefix + 'user' ] = request.user.username
+  querydict_history[ prefix + 'user' ] = request.user
   querydict_history[ prefix + 'type' ] = app_name
   querydict_history[ prefix + 'type' ] = app_name
 
 
   hist_page, hist_filter = _list_query_history(request.user,
   hist_page, hist_filter = _list_query_history(request.user,
@@ -287,7 +286,7 @@ def my_queries(request):
   prefix = 'q-'
   prefix = 'q-'
   querydict_query = _copy_prefix(prefix, request.GET)
   querydict_query = _copy_prefix(prefix, request.GET)
   # Manually limit up the user filter.
   # Manually limit up the user filter.
-  querydict_query[ prefix + 'user' ] = request.user.username
+  querydict_query[ prefix + 'user' ] = request.user
   querydict_query[ prefix + 'type' ] = app_name
   querydict_query[ prefix + 'type' ] = app_name
 
 
   query_page, query_filter = _list_designs(querydict_query, DEFAULT_PAGE_SIZE, prefix)
   query_page, query_filter = _list_designs(querydict_query, DEFAULT_PAGE_SIZE, prefix)
@@ -322,7 +321,7 @@ def list_query_history(request):
   DEFAULT_PAGE_SIZE = 30
   DEFAULT_PAGE_SIZE = 30
   prefix = 'q-'
   prefix = 'q-'
 
 
-  share_queries = conf.SHARE_SAVED_QUERIES.get() or request.user.is_superuser
+  share_queries = request.user.is_superuser
 
 
   querydict_query = request.GET.copy()
   querydict_query = request.GET.copy()
   if not share_queries:
   if not share_queries:
@@ -439,7 +438,7 @@ def execute_query(request, design_id=None):
     'log': log,
     'log': log,
     'autocomplete_base_url': reverse(get_app_name(request) + ':autocomplete', kwargs={}),
     'autocomplete_base_url': reverse(get_app_name(request) + ':autocomplete', kwargs={}),
     'on_success_url': on_success_url,
     'on_success_url': on_success_url,
-    'can_edit_name': design and not design.is_auto and design.name,
+    'can_edit_name': design and not design.is_auto,
   })
   })
 
 
 
 
@@ -905,39 +904,39 @@ def autocomplete(request, database=None, table=None):
 Utils
 Utils
 """
 """
 
 
+# owner_only is deprecated
 def authorized_get_design(request, design_id, owner_only=False, must_exist=False):
 def authorized_get_design(request, design_id, owner_only=False, must_exist=False):
   if design_id is None and not must_exist:
   if design_id is None and not must_exist:
     return None
     return None
   try:
   try:
-    design = models.SavedQuery.objects.get(id=design_id)
-  except models.SavedQuery.DoesNotExist:
+    design = SavedQuery.objects.get(id=design_id)
+  except SavedQuery.DoesNotExist:
     if must_exist:
     if must_exist:
       raise PopupException(_('Design %(id)s does not exist.') % {'id': design_id})
       raise PopupException(_('Design %(id)s does not exist.') % {'id': design_id})
     else:
     else:
       return None
       return None
 
 
-  if not conf.SHARE_SAVED_QUERIES.get() and (not request.user.is_superuser or owner_only) \
-      and design.owner != request.user:
-    raise PopupException(_('Cannot access design %(id)s.') % {'id': design_id})
+  if owner_only:
+    design.doc.get().can_write_or_exception(request.user)
   else:
   else:
-    return design
+    design.doc.get().can_read_or_exception(request.user)
+
+  return design
 
 
 def authorized_get_history(request, query_history_id, owner_only=False, must_exist=False):
 def authorized_get_history(request, query_history_id, owner_only=False, must_exist=False):
   if query_history_id is None and not must_exist:
   if query_history_id is None and not must_exist:
     return None
     return None
   try:
   try:
-    query_history = models.QueryHistory.get(id=query_history_id)
-  except models.QueryHistory.DoesNotExist:
+    query_history = QueryHistory.get(id=query_history_id)
+  except QueryHistory.DoesNotExist:
     if must_exist:
     if must_exist:
       raise PopupException(_('QueryHistory %(id)s does not exist.') % {'id': query_history_id})
       raise PopupException(_('QueryHistory %(id)s does not exist.') % {'id': query_history_id})
     else:
     else:
       return None
       return None
 
 
-  if not conf.SHARE_SAVED_QUERIES.get() and (not request.user.is_superuser or owner_only) \
-      and query_history.owner != request.user:
-    raise PopupException(_('Cannot access QueryHistory %(id)s.') % {'id': query_history_id})
-  else:
-    return query_history
+  query_history.design.doc.get().can_read_or_exception(request.user)
+  
+  return query_history
 
 
 
 
 def safe_get_design(request, design_type, design_id=None):
 def safe_get_design(request, design_type, design_id=None):
@@ -949,10 +948,7 @@ def safe_get_design(request, design_type, design_id=None):
   design = None
   design = None
 
 
   if design_id is not None:
   if design_id is not None:
-    try:
-      design = models.SavedQuery.get(design_id, request.user, design_type)
-    except models.SavedQuery.DoesNotExist:
-      messages.error(request, _('Design does not exist.'))
+    design = authorized_get_design(request, design_id)
 
 
   if design is None:
   if design is None:
     design = models.SavedQuery(owner=request.user, type=design_type)
     design = models.SavedQuery(owner=request.user, type=design_type)
@@ -1126,32 +1122,30 @@ def _list_designs(querydict, page_size, prefix="", is_trashed=False):
   DEFAULT_SORT = ('-', 'date')                  # Descending date
   DEFAULT_SORT = ('-', 'date')                  # Descending date
 
 
   SORT_ATTR_TRANSLATION = dict(
   SORT_ATTR_TRANSLATION = dict(
-    date='mtime',
+    date='last_modified',
     name='name',
     name='name',
-    desc='desc',
-    type='type',
+    desc='description',
+    type='extra',
   )
   )
 
 
-  # Filtering. Only display designs explicitly saved.
-  db_queryset = models.SavedQuery.objects.filter(is_auto=False, is_trashed=is_trashed)
-
   user = querydict.get(prefix + 'user')
   user = querydict.get(prefix + 'user')
-  if user is not None:
-    db_queryset = db_queryset.filter(owner__username=user)
+
+  # Trash and security
+  # Discarding is_auto for now
+  if is_trashed:
+    db_queryset = Document.objects.trashed_docs(SavedQuery, user)
+  else:
+    db_queryset = Document.objects.available_docs(SavedQuery, user)
 
 
   # Design type
   # Design type
   d_type = querydict.get(prefix + 'type')
   d_type = querydict.get(prefix + 'type')
-  if d_type:
-    d_type = str(d_type)
-    if d_type not in SavedQuery.TYPES_MAPPING.keys():
-      LOG.warn('Bad parameter to list_designs: type=%s' % (d_type,))
-    else:
-      db_queryset = db_queryset.filter(type=SavedQuery.TYPES_MAPPING[d_type])
+  if d_type and d_type in SavedQuery.TYPES_MAPPING.keys():
+    db_queryset = db_queryset.filter(extra=str(SavedQuery.TYPES_MAPPING[d_type]))
 
 
   # Text search
   # Text search
   frag = querydict.get(prefix + 'text')
   frag = querydict.get(prefix + 'text')
   if frag:
   if frag:
-    db_queryset = db_queryset.filter(Q(name__icontains=frag) | Q(desc__icontains=frag))
+    db_queryset = db_queryset.filter(Q(name__icontains=frag) | Q(description__icontains=frag))
 
 
   # Ordering
   # Ordering
   sort_key = querydict.get(prefix + 'sort')
   sort_key = querydict.get(prefix + 'sort')
@@ -1168,8 +1162,10 @@ def _list_designs(querydict, page_size, prefix="", is_trashed=False):
     sort_dir, sort_attr = DEFAULT_SORT
     sort_dir, sort_attr = DEFAULT_SORT
   db_queryset = db_queryset.order_by(sort_dir + SORT_ATTR_TRANSLATION[sort_attr])
   db_queryset = db_queryset.order_by(sort_dir + SORT_ATTR_TRANSLATION[sort_attr])
 
 
+  designs = [job.content_object for job in db_queryset.all() if job.content_object]
+
   pagenum = int(querydict.get(prefix + 'page', 1))
   pagenum = int(querydict.get(prefix + 'page', 1))
-  paginator = Paginator(db_queryset, page_size)
+  paginator = Paginator(designs, page_size)
   page = paginator.page(pagenum)
   page = paginator.page(pagenum)
 
 
   # We need to pass the parameters back to the template to generate links
   # We need to pass the parameters back to the template to generate links

+ 0 - 7
apps/oozie/src/oozie/conf.py

@@ -49,13 +49,6 @@ REMOTE_SAMPLE_DIR = Config(
   default="/user/hue/oozie/workspaces",
   default="/user/hue/oozie/workspaces",
   help=_t("Location on HDFS where the Oozie workflows are stored."))
   help=_t("Location on HDFS where the Oozie workflows are stored."))
 
 
-SHARE_JOBS = Config(
-  key='share_jobs',
-  default=True,
-  type=coerce_bool,
-  help=_t('Share workflows, coordinators and bundles information with all users. If set to false, '
-         'they will be visible only to the owner and administrators.'))
-
 OOZIE_JOBS_COUNT = Config(
 OOZIE_JOBS_COUNT = Config(
   key='oozie_jobs_count',
   key='oozie_jobs_count',
   default=100,
   default=100,

+ 4 - 3
apps/oozie/src/oozie/forms.py

@@ -29,6 +29,7 @@ from desktop.lib.django_forms import MultiForm, SplitDateTimeWidget
 from oozie.models import Workflow, Node, Java, Mapreduce, Streaming, Coordinator,\
 from oozie.models import Workflow, Node, Java, Mapreduce, Streaming, Coordinator,\
   Dataset, DataInput, DataOutput, Pig, Link, Hive, Sqoop, Ssh, Shell, DistCp, Fs,\
   Dataset, DataInput, DataOutput, Pig, Link, Hive, Sqoop, Ssh, Shell, DistCp, Fs,\
   Email, SubWorkflow, Generic, Bundle, BundledCoordinator
   Email, SubWorkflow, Generic, Bundle, BundledCoordinator
+from desktop.models import Document
 
 
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
@@ -273,7 +274,7 @@ class SubWorkflowForm(forms.ModelForm):
     user = kwargs.pop('user')
     user = kwargs.pop('user')
     workflow = kwargs.pop('workflow')
     workflow = kwargs.pop('workflow')
     super(SubWorkflowForm, self).__init__(*args, **kwargs)
     super(SubWorkflowForm, self).__init__(*args, **kwargs)
-    choices=((wf.id, wf) for wf in Workflow.objects.available().filter(owner=user).exclude(id=workflow.id))
+    choices=((wf.id, wf) for wf in Document.objects.available(Workflow, user) if workflow.id != id)
     self.fields['sub_workflow'] = forms.ChoiceField(choices=choices, widget=forms.RadioSelect(attrs={'class':'radio'}))
     self.fields['sub_workflow'] = forms.ChoiceField(choices=choices, widget=forms.RadioSelect(attrs={'class':'radio'}))
 
 
   class Meta:
   class Meta:
@@ -350,12 +351,12 @@ class CoordinatorForm(forms.ModelForm):
     user = kwargs['user']
     user = kwargs['user']
     del kwargs['user']
     del kwargs['user']
     super(CoordinatorForm, self).__init__(*args, **kwargs)
     super(CoordinatorForm, self).__init__(*args, **kwargs)
-    qs = Workflow.objects.available().filter(Q(is_shared=True) | Q(owner=user))
+    qs = Document.objects.available(Workflow, user)
     workflows = []
     workflows = []
     for workflow in qs:
     for workflow in qs:
       if workflow.is_accessible(user):
       if workflow.is_accessible(user):
         workflows.append(workflow.id)
         workflows.append(workflow.id)
-    qs = qs.filter(id__in=workflows)
+    qs = Workflow.objects.filter(id__in=workflows)
     self.fields['workflow'].queryset = qs
     self.fields['workflow'].queryset = qs
 
 
 
 

+ 39 - 39
apps/oozie/src/oozie/models.py

@@ -28,9 +28,12 @@ from string import Template
 from itertools import chain
 from itertools import chain
 
 
 from django.db import models
 from django.db import models
+from django.db.models import Q
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.core.validators import RegexValidator
 from django.core.validators import RegexValidator
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
 from django.forms.models import inlineformset_factory
 from django.forms.models import inlineformset_factory
 from django.utils.encoding import force_unicode
 from django.utils.encoding import force_unicode
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
@@ -39,13 +42,14 @@ from desktop.log.access import access_warn
 from desktop.lib import django_mako
 from desktop.lib import django_mako
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.json_utils import JSONEncoderForHTML
 from desktop.lib.json_utils import JSONEncoderForHTML
+from desktop.models import Document
 from hadoop.fs.exceptions import WebHdfsException
 from hadoop.fs.exceptions import WebHdfsException
 
 
 from hadoop.fs.hadoopfs import Hdfs
 from hadoop.fs.hadoopfs import Hdfs
 from liboozie.submittion import Submission
 from liboozie.submittion import Submission
 from liboozie.submittion import create_directories
 from liboozie.submittion import create_directories
 
 
-from oozie.conf import REMOTE_SAMPLE_DIR, SHARE_JOBS
+from oozie.conf import REMOTE_SAMPLE_DIR
 from timezones import TIMEZONES
 from timezones import TIMEZONES
 
 
 
 
@@ -57,13 +61,6 @@ name_validator = RegexValidator(regex='^[a-zA-Z_][\-_a-zA-Z0-9]{1,39}$',
                                 message=_('Enter a valid value: combination of 2 - 40 letters and digits starting by a letter'))
                                 message=_('Enter a valid value: combination of 2 - 40 letters and digits starting by a letter'))
 
 
 
 
-class TrashManager(models.Manager):
-  def trashed(self):
-    return super(TrashManager, self).get_query_set().filter(is_trashed=True)
-
-  def available(self):
-    return super(TrashManager, self).get_query_set().filter(is_trashed=False)
-
 
 
 """
 """
 Permissions:
 Permissions:
@@ -106,14 +103,12 @@ class JobManager(models.Manager):
 
 
 class Job(models.Model):
 class Job(models.Model):
   """
   """
-  Base class for Workflows and Coordinators.
-
-  http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/index.html
+  Base class for Oozie Workflows, Coordinators and Bundles.
   """
   """
-  owner = models.ForeignKey(User, db_index=True, verbose_name=_t('Owner'), help_text=_t('Person who can modify the job.'))
-  name = models.CharField(max_length=40, blank=False, validators=[name_validator],
+  owner = models.ForeignKey(User, db_index=True, verbose_name=_t('Owner'), help_text=_t('Person who can modify the job.')) # Deprecated
+  name = models.CharField(max_length=40, blank=False, validators=[name_validator], # Deprecated
       help_text=_t('Name of the job, which must be unique per user.'), verbose_name=_t('Name'))
       help_text=_t('Name of the job, which must be unique per user.'), verbose_name=_t('Name'))
-  description = models.CharField(max_length=1024, blank=True, verbose_name=_t('Description'),
+  description = models.CharField(max_length=1024, blank=True, verbose_name=_t('Description'), # Deprecated
                                  help_text=_t('The purpose of the job.'))
                                  help_text=_t('The purpose of the job.'))
   last_modified = models.DateTimeField(auto_now=True, db_index=True, verbose_name=_t('Last modified'))
   last_modified = models.DateTimeField(auto_now=True, db_index=True, verbose_name=_t('Last modified'))
   schema_version = models.CharField(max_length=128, verbose_name=_t('Schema version'),
   schema_version = models.CharField(max_length=128, verbose_name=_t('Schema version'),
@@ -121,27 +116,27 @@ class Job(models.Model):
   deployment_dir = models.CharField(max_length=1024, blank=True, verbose_name=_t('HDFS deployment directory'),
   deployment_dir = models.CharField(max_length=1024, blank=True, verbose_name=_t('HDFS deployment directory'),
                                     help_text=_t('The path on the HDFS where all the workflows and '
                                     help_text=_t('The path on the HDFS where all the workflows and '
                                                 'dependencies must be uploaded.'))
                                                 'dependencies must be uploaded.'))
-  is_shared = models.BooleanField(default=False, db_index=True, verbose_name=_t('Is shared'),
+  is_shared = models.BooleanField(default=False, db_index=True, verbose_name=_t('Is shared'), # Deprecated
                                   help_text=_t('Enable other users to have access to this job.'))
                                   help_text=_t('Enable other users to have access to this job.'))
   parameters = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]', verbose_name=_t('Oozie parameters'),
   parameters = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]', verbose_name=_t('Oozie parameters'),
                                 help_text=_t('Parameters used at the submission time (e.g. market=US, oozie.use.system.libpath=true).'))
                                 help_text=_t('Parameters used at the submission time (e.g. market=US, oozie.use.system.libpath=true).'))
-  is_trashed = models.BooleanField(default=False, db_index=True, verbose_name=_t('Is trashed'),
+  is_trashed = models.BooleanField(default=False, db_index=True, verbose_name=_t('Is trashed'),  blank=True,# Deprecated
                                    help_text=_t('If this job is trashed.'))
                                    help_text=_t('If this job is trashed.'))
+  doc = generic.GenericRelation(Document, related_name='oozie_doc')
 
 
   objects = JobManager()
   objects = JobManager()
   unique_together = ('owner', 'name')
   unique_together = ('owner', 'name')
 
 
   def delete(self, skip_trash=False, *args, **kwargs):
   def delete(self, skip_trash=False, *args, **kwargs):
     if skip_trash:
     if skip_trash:
+      self.doc.all().delete()
       return super(Job, self).delete(*args, **kwargs)
       return super(Job, self).delete(*args, **kwargs)
     else:
     else:
-      self.is_trashed = True
-      self.save()
+      self.doc.get().send_to_trash()
       return self
       return self
 
 
   def restore(self):
   def restore(self):
-    self.is_trashed = False
-    self.save()
+    self.doc.get().restore_from_trash()
     return self
     return self
 
 
   def save(self):
   def save(self):
@@ -191,6 +186,7 @@ class Job(models.Model):
 
 
   @property
   @property
   def status(self):
   def status(self):
+    # TODO
     if self.is_shared:
     if self.is_shared:
       return _('shared')
       return _('shared')
     else:
     else:
@@ -205,14 +201,15 @@ class Job(models.Model):
     return  [{'name': name, 'value': value} for name, value in params.iteritems()]
     return  [{'name': name, 'value': value} for name, value in params.iteritems()]
 
 
   def is_accessible(self, user):
   def is_accessible(self, user):
-    return user.is_superuser or self.owner == user or (SHARE_JOBS.get() and self.is_shared)
+    return self.doc.get().is_accessible(user)
+    #return user.is_superuser or self.owner == user or (SHARE_JOBS.get() and self.is_shared)
 
 
   def is_editable(self, user):
   def is_editable(self, user):
     """Only owners or admins can modify a job."""
     """Only owners or admins can modify a job."""
     return user.is_superuser or self.owner == user
     return user.is_superuser or self.owner == user
 
 
 
 
-class WorkflowManager(TrashManager):
+class WorkflowManager(models.Manager):
   def new_workflow(self, owner):
   def new_workflow(self, owner):
     workflow = Workflow(owner=owner, schema_version='uri:oozie:workflow:0.4')
     workflow = Workflow(owner=owner, schema_version='uri:oozie:workflow:0.4')
 
 
@@ -242,6 +239,8 @@ class WorkflowManager(TrashManager):
     workflow.end = end
     workflow.end = end
     workflow.save()
     workflow.save()
 
 
+    Document.objects.link(workflow, owner=workflow.owner, name=workflow.name, description=workflow.description)
+
     self.check_workspace(workflow, fs)
     self.check_workspace(workflow, fs)
 
 
   def check_workspace(self, workflow, fs):
   def check_workspace(self, workflow, fs):
@@ -272,9 +271,6 @@ class WorkflowManager(TrashManager):
 
 
 
 
 class Workflow(Job):
 class Workflow(Job):
-  """
-  http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/WorkflowFunctionalSpec.html
-  """
   is_single = models.BooleanField(default=False)
   is_single = models.BooleanField(default=False)
   start = models.ForeignKey('Start', related_name='start_workflow', blank=True, null=True)
   start = models.ForeignKey('Start', related_name='start_workflow', blank=True, null=True)
   end  = models.ForeignKey('End', related_name='end_workflow',  blank=True, null=True)
   end  = models.ForeignKey('End', related_name='end_workflow',  blank=True, null=True)
@@ -300,6 +296,7 @@ class Workflow(Job):
   def clone(self, fs, new_owner=None):
   def clone(self, fs, new_owner=None):
     source_deployment_dir = self.deployment_dir # Needed
     source_deployment_dir = self.deployment_dir # Needed
     nodes = self.node_set.all()
     nodes = self.node_set.all()
+    copy_doc = self.doc.get().copy()    
     links = Link.objects.filter(parent__workflow=self)
     links = Link.objects.filter(parent__workflow=self)
 
 
     copy = self
     copy = self
@@ -333,6 +330,10 @@ class Workflow(Job):
     copy.end = old_nodes_mapping[self.end.id]
     copy.end = old_nodes_mapping[self.end.id]
     copy.save()
     copy.save()
 
 
+    copy_doc.name = copy.name
+    copy_doc.save()
+    copy.doc.add(copy_doc)    
+
     try:
     try:
       if copy.is_shared:
       if copy.is_shared:
         perms = 0755
         perms = 0755
@@ -415,7 +416,7 @@ class Workflow(Job):
     return 'workflow.xml'
     return 'workflow.xml'
 
 
   def get_absolute_url(self):
   def get_absolute_url(self):
-    return reverse('oozie:edit_workflow', kwargs={'workflow': self.id})
+    return reverse('oozie:edit_workflow', kwargs={'workflow': self.id}) + '#editWorkflow'
 
 
   def get_hierarchy(self):
   def get_hierarchy(self):
     node = Start.objects.get(workflow=self) # Uncached version of start.
     node = Start.objects.get(workflow=self) # Uncached version of start.
@@ -666,9 +667,6 @@ class Node(models.Model):
 
 
 
 
 class Action(Node):
 class Action(Node):
-  """
-  http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/WorkflowFunctionalSpec.html#a3.2_Workflow_Action_Nodes
-  """
   types = ()
   types = ()
 
 
   class Meta:
   class Meta:
@@ -1235,9 +1233,6 @@ DATASET_FREQUENCY = ['MINUTE', 'HOUR', 'DAY', 'MONTH', 'YEAR']
 
 
 
 
 class Coordinator(Job):
 class Coordinator(Job):
-  """
-  http://oozie.apache.org/docs/3.3.0/CoordinatorFunctionalSpec.html
-  """
   frequency_number = models.SmallIntegerField(default=1, choices=FREQUENCY_NUMBERS, verbose_name=_t('Frequency number'),
   frequency_number = models.SmallIntegerField(default=1, choices=FREQUENCY_NUMBERS, verbose_name=_t('Frequency number'),
                                               help_text=_t('The number of units of the rate at which '
                                               help_text=_t('The number of units of the rate at which '
                                                            'data is periodically created.'))
                                                            'data is periodically created.'))
@@ -1271,8 +1266,6 @@ class Coordinator(Job):
   job_properties = models.TextField(default='[]', verbose_name=_t('Workflow properties'),
   job_properties = models.TextField(default='[]', verbose_name=_t('Workflow properties'),
                                     help_text=_t('Additional properties to transmit to the workflow, e.g. limit=100, and EL functions, e.g. username=${coord:user()}'))
                                     help_text=_t('Additional properties to transmit to the workflow, e.g. limit=100, and EL functions, e.g. username=${coord:user()}'))
 
 
-  objects = TrashManager()
-
   HUE_ID = 'hue-id-c'
   HUE_ID = 'hue-id-c'
 
 
   def get_type(self):
   def get_type(self):
@@ -1285,6 +1278,7 @@ class Coordinator(Job):
     return re.sub(re.compile('\s*\n+', re.MULTILINE), '\n', django_mako.render_to_string(tmpl, {'coord': self, 'mapping': mapping})).encode('utf-8', 'xmlcharrefreplace')
     return re.sub(re.compile('\s*\n+', re.MULTILINE), '\n', django_mako.render_to_string(tmpl, {'coord': self, 'mapping': mapping})).encode('utf-8', 'xmlcharrefreplace')
 
 
   def clone(self, new_owner=None):
   def clone(self, new_owner=None):
+    copy_doc = self.doc.get()
     datasets = Dataset.objects.filter(coordinator=self)
     datasets = Dataset.objects.filter(coordinator=self)
     data_inputs = DataInput.objects.filter(coordinator=self)
     data_inputs = DataInput.objects.filter(coordinator=self)
     data_outputs = DataOutput.objects.filter(coordinator=self)
     data_outputs = DataOutput.objects.filter(coordinator=self)
@@ -1322,6 +1316,11 @@ class Coordinator(Job):
       data_output.dataset = old_dataset_mapping[data_output.dataset.id]
       data_output.dataset = old_dataset_mapping[data_output.dataset.id]
       data_output.save()
       data_output.save()
 
 
+    copy_doc.pk = None
+    copy_doc.id = None
+    copy_doc.save()
+    copy.doc.add(copy_doc)
+
     return copy
     return copy
 
 
   @classmethod
   @classmethod
@@ -1528,15 +1527,10 @@ class BundledCoordinator(models.Model):
 
 
 
 
 class Bundle(Job):
 class Bundle(Job):
-  """
-  http://oozie.apache.org/docs/3.3.0/BundleFunctionalSpec.html
-  """
   kick_off_time = models.DateTimeField(default=datetime.today(), verbose_name=_t('Start'),
   kick_off_time = models.DateTimeField(default=datetime.today(), verbose_name=_t('Start'),
                                        help_text=_t('When to start the first coordinators.'))
                                        help_text=_t('When to start the first coordinators.'))
   coordinators = models.ManyToManyField(Coordinator, through='BundledCoordinator')
   coordinators = models.ManyToManyField(Coordinator, through='BundledCoordinator')
 
 
-  objects = TrashManager()
-
   HUE_ID = 'hue-id-b'
   HUE_ID = 'hue-id-b'
 
 
   def get_type(self):
   def get_type(self):
@@ -1555,6 +1549,7 @@ class Bundle(Job):
 
 
   def clone(self, new_owner=None):
   def clone(self, new_owner=None):
     bundleds = BundledCoordinator.objects.filter(bundle=self)
     bundleds = BundledCoordinator.objects.filter(bundle=self)
+    copy_doc = self.doc.get()
 
 
     copy = self
     copy = self
     copy.pk = None
     copy.pk = None
@@ -1571,6 +1566,11 @@ class Bundle(Job):
       bundled.bundle = copy
       bundled.bundle = copy
       bundled.save()
       bundled.save()
 
 
+    copy_doc.pk = None
+    copy_doc.id = None
+    copy_doc.save()
+    copy.doc.add(copy_doc)
+
     return copy
     return copy
 
 
   @classmethod
   @classmethod

+ 3 - 1
apps/oozie/src/oozie/templates/editor/create_bundle.mako

@@ -46,7 +46,9 @@ ${ layout.menubar(section='bundles') }
             ${ utils.render_field(bundle_form['name']) }
             ${ utils.render_field(bundle_form['name']) }
             ${ utils.render_field(bundle_form['description']) }
             ${ utils.render_field(bundle_form['description']) }
             ${ utils.render_field(bundle_form['kick_off_time']) }
             ${ utils.render_field(bundle_form['kick_off_time']) }
-            ${ utils.render_field(bundle_form['is_shared']) }
+            <div class="hide">
+              ${ utils.render_field(bundle_form['is_shared']) }
+            </hide>
 
 
             ${ bundle_form['schema_version'] | n,unicode }
             ${ bundle_form['schema_version'] | n,unicode }
             ${ bundle_form['parameters'] | n,unicode }
             ${ bundle_form['parameters'] | n,unicode }

+ 2 - 2
apps/oozie/src/oozie/templates/editor/create_coordinator.mako

@@ -70,11 +70,11 @@ ${ layout.menubar(section='coordinators') }
             <div class="fieldWrapper">
             <div class="fieldWrapper">
               ${ utils.render_field_no_popover(coordinator_form['name'], extra_attrs = {'validate':'true'}) }
               ${ utils.render_field_no_popover(coordinator_form['name'], extra_attrs = {'validate':'true'}) }
               ${ utils.render_field_no_popover(coordinator_form['description']) }
               ${ utils.render_field_no_popover(coordinator_form['description']) }
-              ${ utils.render_field_no_popover(coordinator_form['workflow'], extra_attrs = {'validate':'true'}) }
-              ${ utils.render_field_no_popover(coordinator_form['is_shared']) }
+              ${ utils.render_field_no_popover(coordinator_form['workflow'], extra_attrs = {'validate':'true'}) }              
               ${ coordinator_form['parameters'] | n,unicode }
               ${ coordinator_form['parameters'] | n,unicode }
               ${ coordinator_form['job_properties'] | n,unicode }
               ${ coordinator_form['job_properties'] | n,unicode }
               <div class="hide">
               <div class="hide">
+                ${ utils.render_field_no_popover(coordinator_form['is_shared']) }
                 ${ utils.render_field(coordinator_form['timeout']) }
                 ${ utils.render_field(coordinator_form['timeout']) }
                 ${ coordinator_form['schema_version'] | n,unicode }
                 ${ coordinator_form['schema_version'] | n,unicode }
               </div>
               </div>

+ 2 - 2
apps/oozie/src/oozie/templates/editor/create_workflow.mako

@@ -46,8 +46,7 @@ ${ layout.menubar(section='workflows') }
               <form class="form-horizontal" id="workflowForm" action="${ url('oozie:create_workflow') }" method="POST">
               <form class="form-horizontal" id="workflowForm" action="${ url('oozie:create_workflow') }" method="POST">
               <fieldset>
               <fieldset>
               ${ utils.render_field(workflow_form['name']) }
               ${ utils.render_field(workflow_form['name']) }
-              ${ utils.render_field(workflow_form['description']) }
-              ${ utils.render_field(workflow_form['is_shared']) }
+              ${ utils.render_field(workflow_form['description']) }              
 
 
               <div class="control-group ">
               <div class="control-group ">
                 <label class="control-label">
                 <label class="control-label">
@@ -64,6 +63,7 @@ ${ layout.menubar(section='workflows') }
                </div>
                </div>
 
 
                <div class="hide">
                <div class="hide">
+                 ${ utils.render_field(workflow_form['is_shared']) }
                  ${ workflow_form['schema_version'] | n,unicode }
                  ${ workflow_form['schema_version'] | n,unicode }
                  ${ workflow_form['job_properties'] | n,unicode }
                  ${ workflow_form['job_properties'] | n,unicode }
                  ${ workflow_form['parameters'] | n,unicode }
                  ${ workflow_form['parameters'] | n,unicode }

+ 2 - 2
apps/oozie/src/oozie/templates/editor/edit_bundle.mako

@@ -113,10 +113,10 @@ ${ layout.menubar(section='bundles') }
                       ${ _('UTC time only. (e.g. if you want 10pm PST (UTC+8) set it 8 hours later to 6am the next day.') }
                       ${ _('UTC time only. (e.g. if you want 10pm PST (UTC+8) set it 8 hours later to 6am the next day.') }
                     </div>
                     </div>
                   </div>
                   </div>
-                </div>
-                ${ utils.render_field_no_popover(bundle_form['is_shared']) }
+                </div>                
                 ${ bundle_form['parameters'] | n,unicode }
                 ${ bundle_form['parameters'] | n,unicode }
                 <div class="hide">
                 <div class="hide">
+                  ${ utils.render_field_no_popover(bundle_form['is_shared']) }
                   ${ bundle_form['schema_version']  | n,unicode }
                   ${ bundle_form['schema_version']  | n,unicode }
                 </div>
                 </div>
               </div>
               </div>

+ 2 - 2
apps/oozie/src/oozie/templates/editor/edit_coordinator.mako

@@ -104,10 +104,10 @@ ${ layout.menubar(section='coordinators') }
             <div class="fieldWrapper">
             <div class="fieldWrapper">
               ${ utils.render_field_no_popover(coordinator_form['name'], extra_attrs = {'validate':'true'}) }
               ${ utils.render_field_no_popover(coordinator_form['name'], extra_attrs = {'validate':'true'}) }
               ${ utils.render_field_no_popover(coordinator_form['description']) }
               ${ utils.render_field_no_popover(coordinator_form['description']) }
-              ${ utils.render_field_no_popover(coordinator_form['workflow'], extra_attrs = {'validate':'true'}) }
-              ${ utils.render_field_no_popover(coordinator_form['is_shared']) }
+              ${ utils.render_field_no_popover(coordinator_form['workflow'], extra_attrs = {'validate':'true'}) }              
               ${ coordinator_form['parameters'] | n,unicode }
               ${ coordinator_form['parameters'] | n,unicode }
               <div class="hide">
               <div class="hide">
+                ${ utils.render_field_no_popover(coordinator_form['is_shared']) }
                 ${ utils.render_field(coordinator_form['timeout']) }
                 ${ utils.render_field(coordinator_form['timeout']) }
                 ${ coordinator_form['schema_version']  | n,unicode }
                 ${ coordinator_form['schema_version']  | n,unicode }
               </div>
               </div>

+ 3 - 1
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -84,7 +84,9 @@ ${ layout.menubar(section='workflows') }
             <fieldset>
             <fieldset>
         ${ utils.render_field(workflow_form['name'], extra_attrs={'data-bind': 'value: %s' % workflow_form['name'].name}) }
         ${ utils.render_field(workflow_form['name'], extra_attrs={'data-bind': 'value: %s' % workflow_form['name'].name}) }
         ${ utils.render_field(workflow_form['description'], extra_attrs={'data-bind': 'value: %s' % workflow_form['description'].name}) }
         ${ utils.render_field(workflow_form['description'], extra_attrs={'data-bind': 'value: %s' % workflow_form['description'].name}) }
-        ${ utils.render_field(workflow_form['is_shared'], extra_attrs={'data-bind': 'checked: %s' % workflow_form['is_shared'].name}) }
+        <div class="hide">
+          ${ utils.render_field(workflow_form['is_shared'], extra_attrs={'data-bind': 'checked: %s' % workflow_form['is_shared'].name}) }
+        </div>
 
 
       <%
       <%
       workflows.key_value_field(workflow_form['parameters'], {
       workflows.key_value_field(workflow_form['parameters'], {

+ 3 - 3
apps/oozie/src/oozie/templates/editor/list_bundles.mako

@@ -59,9 +59,9 @@ ${ layout.menubar(section='bundles') }
     </%def>
     </%def>
 
 
     <%def name="creation()">
     <%def name="creation()">
-        <a href="${ url('oozie:list_trashed_bundles') }" class="btn"><i class="icon-trash"></i> ${ _('View trash') }</a>
-        &nbsp;&nbsp;
-        <a href="${ url('oozie:create_bundle') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
+      <a href="${ url('oozie:create_bundle') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
+      &nbsp;&nbsp;
+      <a href="${ url('oozie:list_trashed_bundles') }" class="btn"><i class="icon-trash"></i> ${ _('View trash') }</a>
     </%def>
     </%def>
   </%actionbar:render>
   </%actionbar:render>
 
 

+ 2 - 2
apps/oozie/src/oozie/templates/editor/list_coordinators.mako

@@ -58,9 +58,9 @@ ${ layout.menubar(section='coordinators') }
     </%def>
     </%def>
 
 
     <%def name="creation()">
     <%def name="creation()">
-      <a href="${ url('oozie:list_trashed_coordinators') }" class="btn"><i class="icon-trash"></i> ${ _('View trash') }</a>
-      &nbsp;&nbsp;
       <a href="${ url('oozie:create_coordinator') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
       <a href="${ url('oozie:create_coordinator') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
+      &nbsp;&nbsp;
+      <a href="${ url('oozie:list_trashed_coordinators') }" class="btn"><i class="icon-trash"></i> ${ _('View trash') }</a>
     </%def>
     </%def>
   </%actionbar:render>
   </%actionbar:render>
 
 

+ 2 - 1
apps/oozie/src/oozie/templates/editor/list_trashed_bundles.mako

@@ -43,10 +43,11 @@ ${ layout.menubar(section='bundles') }
     </%def>
     </%def>
 
 
     <%def name="creation()">
     <%def name="creation()">
-      <a href="${ url('oozie:list_bundles') }" id="home-btn" class="btn" title="${ _('Got to bundle manager') }"><i class="icon-home"></i> ${ _('View bundles') }</a>
       <button class="btn" id="purge-btn" title="${ _('Delete all the bundles') }" data-bind="enabled: availableJobs().length > 0">
       <button class="btn" id="purge-btn" title="${ _('Delete all the bundles') }" data-bind="enabled: availableJobs().length > 0">
         <i class="icon-fire"></i> ${ _('Empty trash') }
         <i class="icon-fire"></i> ${ _('Empty trash') }
       </button>
       </button>
+      &nbsp;&nbsp;
+      <a href="${ url('oozie:list_bundles') }" id="home-btn" class="btn" title="${ _('Got to bundle manager') }"><i class="icon-home"></i> ${ _('Back') }</a>
     </%def>
     </%def>
   </%actionbar:render>
   </%actionbar:render>
 
 

+ 5 - 4
apps/oozie/src/oozie/templates/editor/list_trashed_coordinators.mako

@@ -46,10 +46,11 @@ ${ layout.menubar(section='coordinators') }
     </%def>
     </%def>
 
 
     <%def name="creation()">
     <%def name="creation()">
-        <a href="${ url('oozie:list_coordinators') }" id="home-btn" class="btn" title="${ _('Got to coordinator manager') }">
-          <i class="icon-home"></i> ${ _('View coordinators') }
-        </a>
-        <button class="btn" id="purge-btn"><i class="icon-fire"></i> ${ _('Empty trash') }</button>
+       <button class="btn" id="purge-btn"><i class="icon-fire"></i> ${ _('Empty trash') }</button>
+       &nbsp;&nbsp;
+       <a href="${ url('oozie:list_coordinators') }" id="home-btn" class="btn" title="${ _('Got to coordinator manager') }">
+         <i class="icon-home"></i> ${ _('Back') }
+       </a>        
     </%def>
     </%def>
   </%actionbar:render>
   </%actionbar:render>
 
 

+ 8 - 7
apps/oozie/src/oozie/templates/editor/list_trashed_workflows.mako

@@ -37,21 +37,21 @@ ${ layout.menubar(section='workflows') }
     </%def>
     </%def>
 
 
     <%def name="actions()">
     <%def name="actions()">
-      <button type="button" id="restore-btn" class="btn" title="${ _('Restore the selected workflows') }">
+      <button type="button" id="restore-btn" class="btn toolbarBtn" disabled="disabled" title="${ _('Restore the selected workflows') }">
         <i class="icon-cloud-upload"></i> ${ _('Restore') }
         <i class="icon-cloud-upload"></i> ${ _('Restore') }
       </button>
       </button>
-      <button type="button" id="destroy-btn" class="btn" title="${ _('Delete the selected workflows') }">
+      <button type="button" id="destroy-btn" class="btn toolbarBtn" disabled="disabled" title="${ _('Delete the selected workflows') }">
         <i class="icon-bolt"></i> ${ _('Delete forever') }
         <i class="icon-bolt"></i> ${ _('Delete forever') }
       </button>
       </button>
     </%def>
     </%def>
 
 
     <%def name="creation()">
     <%def name="creation()">
-      <a href="${ url('oozie:list_workflows') }" id="home-btn" class="btn" title="${ _('Go to workflow manager') }">
-        <i class="icon-home"></i> ${ _('View workflows') }
-      </a>
       <button type="button" id="purge-btn" class="btn" title="${ _('Delete all the workflows') }">
       <button type="button" id="purge-btn" class="btn" title="${ _('Delete all the workflows') }">
         <i class="icon-fire"></i> ${ _('Empty trash') }
         <i class="icon-fire"></i> ${ _('Empty trash') }
       </button>
       </button>
+      <a href="${ url('oozie:list_workflows') }" id="home-btn" class="btn" title="${ _('Go to workflow manager') }">
+        <i class="icon-home"></i> ${ _('Back') }
+      </a>
     </%def>
     </%def>
   </%actionbar:render>
   </%actionbar:render>
 
 
@@ -180,10 +180,11 @@ ${ layout.menubar(section='workflows') }
 
 
     function toggleActions() {
     function toggleActions() {
       $(".toolbarBtn").attr("disabled", "disabled");
       $(".toolbarBtn").attr("disabled", "disabled");
-      var selector = $(".hueCheckbox[checked='checked']");
-      var can_delete = $(".hueCheckbox[checked='checked'][data-workflow-id]");
+      var selector = $(".hueCheckbox.workflowCheck[checked='checked']");
+      var can_delete = $(".hueCheckbox.workflowCheck[checked='checked'][data-workflow-id]");
       if (can_delete.length >= 1 && can_delete.length == selector.length) {
       if (can_delete.length >= 1 && can_delete.length == selector.length) {
         $("#destroy-btn").removeAttr("disabled");
         $("#destroy-btn").removeAttr("disabled");
+        $("#restore-btn").removeAttr("disabled");
       }
       }
     }
     }
 
 

+ 3 - 3
apps/oozie/src/oozie/templates/editor/list_workflows.mako

@@ -59,10 +59,10 @@ ${ layout.menubar(section='workflows') }
     </%def>
     </%def>
 
 
     <%def name="creation()">
     <%def name="creation()">
-      <a href="${ url('oozie:list_trashed_workflows') }" class="btn"><i class="icon-trash"></i> ${ _('View trash') }</a>
-      &nbsp;&nbsp;
       <a href="${ url('oozie:create_workflow') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
       <a href="${ url('oozie:create_workflow') }" class="btn"><i class="icon-plus-sign"></i> ${ _('Create') }</a>
-      <a href="${ url('oozie:import_workflow') }" class="btn"><i class="icon-download-alt"></i> ${ _('Import') }</a>
+      <a href="${ url('oozie:import_workflow') }" class="btn"><i class="icon-download-alt"></i> ${ _('Import') }</a>      
+      &nbsp;&nbsp;
+      <a href="${ url('oozie:list_trashed_workflows') }" class="btn"><i class="icon-trash"></i> ${ _('Trash') }</a>
     </%def>
     </%def>
   </%actionbar:render>
   </%actionbar:render>
 
 

+ 1 - 1
apps/oozie/src/oozie/urls.py

@@ -36,7 +36,7 @@ urlpatterns = patterns(
   url(r'^import_workflow/$', 'import_workflow', name='import_workflow'),
   url(r'^import_workflow/$', 'import_workflow', name='import_workflow'),
 
 
   url(r'^list_coordinators/(?P<workflow_id>[-\w]+)?$', 'list_coordinators', name='list_coordinators'),
   url(r'^list_coordinators/(?P<workflow_id>[-\w]+)?$', 'list_coordinators', name='list_coordinators'),
-  url(r'^list_trashed_coordinators/(?P<workflow_id>[-\w]+)?$', 'list_trashed_coordinators', name='list_trashed_coordinators'),
+  url(r'^list_trashed_coordinators/$', 'list_trashed_coordinators', name='list_trashed_coordinators'),
   url(r'^create_coordinator/(?P<workflow>[-\w]+)?$', 'create_coordinator', name='create_coordinator'),
   url(r'^create_coordinator/(?P<workflow>[-\w]+)?$', 'create_coordinator', name='create_coordinator'),
   url(r'^edit_coordinator/(?P<coordinator>[-\w]+)$', 'edit_coordinator', name='edit_coordinator'),
   url(r'^edit_coordinator/(?P<coordinator>[-\w]+)$', 'edit_coordinator', name='edit_coordinator'),
   url(r'^delete_coordinator$', 'delete_coordinator', name='delete_coordinator'),
   url(r'^delete_coordinator$', 'delete_coordinator', name='delete_coordinator'),

+ 1 - 0
apps/oozie/src/oozie/views/api.py

@@ -233,6 +233,7 @@ def _update_workflow_json(json_workflow):
       setattr(workflow, key, json_workflow[key])
       setattr(workflow, key, json_workflow[key])
 
 
   workflow.save()
   workflow.save()
+  workflow.doc.update(name=workflow.name, description=workflow.description)
 
 
   return workflow
   return workflow
 
 

+ 27 - 67
apps/oozie/src/oozie/views/editor.py

@@ -14,6 +14,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # limitations under the License.
+from desktop.models import Document
 
 
 try:
 try:
   import json
   import json
@@ -35,11 +36,9 @@ from desktop.lib.django_util import render, extract_field_data
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.i18n import smart_str
 from desktop.lib.i18n import smart_str
 from desktop.lib.rest.http_client import RestException
 from desktop.lib.rest.http_client import RestException
-from hadoop.fs.exceptions import WebHdfsException
 from liboozie.submittion import Submission
 from liboozie.submittion import Submission
 
 
 from filebrowser.lib.archives import archive_factory
 from filebrowser.lib.archives import archive_factory
-from oozie.conf import SHARE_JOBS
 from oozie.decorators import check_job_access_permission, check_job_edition_permission,\
 from oozie.decorators import check_job_access_permission, check_job_edition_permission,\
                              check_dataset_access_permission, check_dataset_edition_permission
                              check_dataset_access_permission, check_dataset_edition_permission
 from oozie.import_workflow import import_workflow as _import_workflow
 from oozie.import_workflow import import_workflow as _import_workflow
@@ -57,102 +56,61 @@ LOG = logging.getLogger(__name__)
 
 
 
 
 def list_workflows(request):
 def list_workflows(request):
-  data = Workflow.objects.available().filter(managed=True)
-
-  if not SHARE_JOBS.get() and not request.user.is_superuser:
-    data = data.filter(owner=request.user)
-  else:
-    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
-
-  data = data.order_by('-last_modified')
+  data = Document.objects.available(Workflow, request.user)
+  data = [job for job in data if job.managed]
 
 
   return render('editor/list_workflows.mako', request, {
   return render('editor/list_workflows.mako', request, {
-    'jobs': list(data),
-    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
+    'jobs': data,
+    'json_jobs': json.dumps([job.id for job in data]),
   })
   })
 
 
 
 
 def list_trashed_workflows(request):
 def list_trashed_workflows(request):
-  data = Workflow.objects.trashed().filter(managed=True)
-
-  if not SHARE_JOBS.get() and not request.user.is_superuser:
-    data = data.filter(owner=request.user)
-  else:
-    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
-
-  data = data.order_by('-last_modified')
+  data = Document.objects.trashed(Workflow, request.user)
+  data = [job for job in data if job.managed]
 
 
   return render('editor/list_trashed_workflows.mako', request, {
   return render('editor/list_trashed_workflows.mako', request, {
-    'jobs': list(data),
-    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
+    'jobs': data,
+    'json_jobs': json.dumps([job.id for job in data]),
   })
   })
 
 
 
 
 def list_coordinators(request, workflow_id=None):
 def list_coordinators(request, workflow_id=None):
-  data = Coordinator.objects.available()
+  data = Document.objects.available(Coordinator, request.user)
+    
   if workflow_id is not None:
   if workflow_id is not None:
-    data = data.filter(workflow__id=workflow_id)
-
-  if not SHARE_JOBS.get() and not request.user.is_superuser:
-    data = data.filter(owner=request.user)
-  else:
-    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
-
-  data = data.order_by('-last_modified')
+    data = [job for job in data if job.workflow.id == workflow_id]
 
 
   return render('editor/list_coordinators.mako', request, {
   return render('editor/list_coordinators.mako', request, {
-    'jobs': list(data),
-    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
+    'jobs': data,
+    'json_jobs': json.dumps([job.id for job in data]),
   })
   })
 
 
 
 
-def list_trashed_coordinators(request, workflow_id=None):
-  data = Coordinator.objects.trashed()
-  if workflow_id is not None:
-    data = data.filter(workflow__id=workflow_id)
-
-  if not SHARE_JOBS.get() and not request.user.is_superuser:
-    data = data.filter(owner=request.user)
-  else:
-    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
-
-  data = data.order_by('-last_modified')
+def list_trashed_coordinators(request):
+  data = Document.objects.trashed(Coordinator, request.user)
 
 
   return render('editor/list_trashed_coordinators.mako', request, {
   return render('editor/list_trashed_coordinators.mako', request, {
-    'jobs': list(data),
-    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
+    'jobs': data,
+    'json_jobs': json.dumps([job.id for job in data]),
   })
   })
 
 
 
 
 def list_bundles(request):
 def list_bundles(request):
-  data = Bundle.objects.available()
-
-  if not SHARE_JOBS.get() and not request.user.is_superuser:
-    data = data.filter(owner=request.user)
-  else:
-    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
-
-  data = data.order_by('-last_modified')
+  data = Document.objects.available(Bundle, request.user)
 
 
   return render('editor/list_bundles.mako', request, {
   return render('editor/list_bundles.mako', request, {
-    'jobs': list(data),
-    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
+    'jobs': data,
+    'json_jobs': json.dumps([job.id for job in data]),
   })
   })
 
 
 
 
 def list_trashed_bundles(request):
 def list_trashed_bundles(request):
-  data = Bundle.objects.trashed()
-
-  if not SHARE_JOBS.get() and not request.user.is_superuser:
-    data = data.filter(owner=request.user)
-  else:
-    data = data.filter(Q(is_shared=True) | Q(owner=request.user))
-
-  data = data.order_by('-last_modified')
+  data = Document.objects.trashed(Bundle, request.user)
 
 
   return render('editor/list_trashed_bundles.mako', request, {
   return render('editor/list_trashed_bundles.mako', request, {
-    'jobs': list(data),
-    'json_jobs': json.dumps(list(data.values_list('id', flat=True))),
+    'jobs': data,
+    'json_jobs': json.dumps([job.id for job in data]),
   })
   })
 
 
 
 
@@ -276,7 +234,7 @@ def restore_workflow(request):
   job_ids = request.POST.getlist('job_selection')
   job_ids = request.POST.getlist('job_selection')
 
 
   for job_id in job_ids:
   for job_id in job_ids:
-    job = Job.objects.is_accessible_or_exception(request, job_id)
+    job = Document.objects.is_accessible_or_exception(request.user, Job, job_id)
     Job.objects.can_edit_or_exception(request, job)
     Job.objects.can_edit_or_exception(request, job)
     job.workflow.restore()
     job.workflow.restore()
 
 
@@ -363,6 +321,7 @@ def create_coordinator(request, workflow=None):
 
 
     if coordinator_form.is_valid():
     if coordinator_form.is_valid():
       coordinator = coordinator_form.save()
       coordinator = coordinator_form.save()
+      Document.objects.link(coordinator, owner=coordinator.owner, name=coordinator.name, description=coordinator.description)
       return redirect(reverse('oozie:edit_coordinator', kwargs={'coordinator': coordinator.id}) + "#step3")
       return redirect(reverse('oozie:edit_coordinator', kwargs={'coordinator': coordinator.id}) + "#step3")
     else:
     else:
       request.error(_('Errors on the form: %s') % coordinator_form.errors)
       request.error(_('Errors on the form: %s') % coordinator_form.errors)
@@ -632,6 +591,7 @@ def create_bundle(request):
 
 
     if bundle_form.is_valid():
     if bundle_form.is_valid():
       bundle = bundle_form.save()
       bundle = bundle_form.save()
+      Document.objects.link(bundle, owner=bundle.owner, name=bundle.name, description=bundle.description)
       return redirect(reverse('oozie:edit_bundle', kwargs={'bundle': bundle.id}))
       return redirect(reverse('oozie:edit_bundle', kwargs={'bundle': bundle.id}))
     else:
     else:
       request.error(_('Errors on the form: %s') % bundle_form.errors)
       request.error(_('Errors on the form: %s') % bundle_form.errors)

+ 21 - 9
apps/pig/src/pig/models.py

@@ -23,9 +23,12 @@ import posixpath
 
 
 from django.db import models
 from django.db import models
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
+from django.contrib.contenttypes import generic
+from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 
 
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.exceptions_renderable import PopupException
+from desktop.models import Document as Doc
 from hadoop.fs.hadoopfs import Hdfs
 from hadoop.fs.hadoopfs import Hdfs
 
 
 
 
@@ -34,10 +37,10 @@ class Document(models.Model):
   is_design = models.BooleanField(default=True, db_index=True, verbose_name=_t('Is a user document, not a document submission.'),
   is_design = models.BooleanField(default=True, db_index=True, verbose_name=_t('Is a user document, not a document submission.'),
                                      help_text=_t('If the document is not a submitted job but a real query, script, workflow.'))
                                      help_text=_t('If the document is not a submitted job but a real query, script, workflow.'))
 
 
-  def is_editable(self, user):
+  def is_editable(self, user): # Deprecated
     return user.is_superuser or self.owner == user
     return user.is_superuser or self.owner == user
 
 
-  def can_edit_or_exception(self, user, exception_class=PopupException):
+  def can_edit_or_exception(self, user, exception_class=PopupException): # Deprecated
     if self.is_editable(user):
     if self.is_editable(user):
       return True
       return True
     else:
     else:
@@ -57,12 +60,17 @@ class PigScript(Document):
       'hadoopProperties': []
       'hadoopProperties': []
   }))
   }))
 
 
+  doc = generic.GenericRelation(Doc, related_name='pig_doc')
+
   def update_from_dict(self, attrs):
   def update_from_dict(self, attrs):
     data_dict = self.dict
     data_dict = self.dict
 
 
     for attr in PigScript._ATTRIBUTES:
     for attr in PigScript._ATTRIBUTES:
       if attrs.get(attr) is not None:
       if attrs.get(attr) is not None:
         data_dict[attr] = attrs[attr]
         data_dict[attr] = attrs[attr]
+        
+    if 'name' in attrs:
+      self.doc.update(name=attrs['name'])
 
 
     self.data = json.dumps(data_dict)
     self.data = json.dumps(data_dict)
 
 
@@ -70,14 +78,17 @@ class PigScript(Document):
   def dict(self):
   def dict(self):
     return json.loads(self.data)
     return json.loads(self.data)
 
 
+  def get_absolute_url(self):
+    return reverse('pig:index') + '#edit/%s' % self.id
+
 
 
 def create_or_update_script(id, name, script, user, parameters, resources, hadoopProperties, is_design=True):
 def create_or_update_script(id, name, script, user, parameters, resources, hadoopProperties, is_design=True):
-  """This take care of security"""
   try:
   try:
     pig_script = PigScript.objects.get(id=id)
     pig_script = PigScript.objects.get(id=id)
-    pig_script.can_edit_or_exception(user)
+    pig_script.doc.get().can_read_or_exception(user)
   except:
   except:
     pig_script = PigScript.objects.create(owner=user, is_design=is_design)
     pig_script = PigScript.objects.create(owner=user, is_design=is_design)
+    Doc.objects.link(pig_script, owner=pig_script.owner, name=name)
 
 
   pig_script.update_from_dict({
   pig_script.update_from_dict({
       'name': name,
       'name': name,
@@ -90,14 +101,15 @@ def create_or_update_script(id, name, script, user, parameters, resources, hadoo
   return pig_script
   return pig_script
 
 
 
 
-def get_scripts(user, max_count=200, is_design=None):
-  scripts = []
-  objects = PigScript.objects.filter(owner__pk__in=[user.pk, 1100713])
+def get_scripts(user, is_design=None):
+  scripts = []  
+  data = Doc.objects.available(PigScript, user)
+  print data
 
 
   if is_design is not None:
   if is_design is not None:
-    objects = objects.filter(is_design=is_design)
+    data = [job for job in data if job.is_design]
 
 
-  for script in objects.order_by('-id')[:max_count]:
+  for script in data:
     data = script.dict
     data = script.dict
     massaged_script = {
     massaged_script = {
       'id': script.id,
       'id': script.id,

+ 15 - 7
apps/pig/src/pig/views.py

@@ -30,6 +30,8 @@ from django.views.decorators.http import require_http_methods
 from desktop.lib.django_util import render
 from desktop.lib.django_util import render
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.rest.http_client import RestException
 from desktop.lib.rest.http_client import RestException
+from desktop.models import Document
+
 from oozie.views.dashboard import show_oozie_error, check_job_access_permission,\
 from oozie.views.dashboard import show_oozie_error, check_job_access_permission,\
                                   check_job_edition_permission
                                   check_job_edition_permission
 
 
@@ -57,7 +59,7 @@ def dashboard(request):
   pig_api = api.get(request.fs, request.user)
   pig_api = api.get(request.fs, request.user)
 
 
   jobs = pig_api.get_jobs()
   jobs = pig_api.get_jobs()
-  hue_jobs = PigScript.objects.filter(owner=request.user)
+  hue_jobs = Document.objects.available(PigScript, request.user)
   massaged_jobs = pig_api.massaged_jobs_for_json(request, jobs, hue_jobs)
   massaged_jobs = pig_api.massaged_jobs_for_json(request, jobs, hue_jobs)
 
 
   return HttpResponse(json.dumps(massaged_jobs), mimetype="application/json")
   return HttpResponse(json.dumps(massaged_jobs), mimetype="application/json")
@@ -143,7 +145,7 @@ def copy(request):
     raise PopupException(_('POST request required.'))
     raise PopupException(_('POST request required.'))
 
 
   pig_script = PigScript.objects.get(id=request.POST.get('id'))
   pig_script = PigScript.objects.get(id=request.POST.get('id'))
-  pig_script.can_edit_or_exception(request.user)
+  pig_script.doc.get.can_edit_or_exception(request.user)
 
 
   existing_script_data = pig_script.dict
   existing_script_data = pig_script.dict
   name = existing_script_data["name"] + _(' (Copy)')
   name = existing_script_data["name"] + _(' (Copy)')
@@ -152,18 +154,23 @@ def copy(request):
   resources = existing_script_data["resources"]
   resources = existing_script_data["resources"]
   hadoopProperties = existing_script_data["hadoopProperties"]
   hadoopProperties = existing_script_data["hadoopProperties"]
 
 
-  pig_script = PigScript.objects.create(owner=request.user)
-  pig_script.update_from_dict({
+  script_copy = PigScript.objects.create(owner=request.user)
+  script_copy.update_from_dict({
       'name': name,
       'name': name,
       'script': script,
       'script': script,
       'parameters': parameters,
       'parameters': parameters,
       'resources': resources,
       'resources': resources,
       'hadoopProperties': hadoopProperties
       'hadoopProperties': hadoopProperties
-  })
-  pig_script.save()
+  })  
+  script_copy.save()
+  Document.objects.link(script_copy, owner=script_copy.owner, name=name)
+  copy_doc = pig_script.doc.get().copy()
+  copy_doc.name = name
+  copy_doc.save()
+  script_copy.doc.add(copy_doc)   
 
 
   response = {
   response = {
-    'id': pig_script.id,
+    'id': script_copy.id,
     'name': name,
     'name': name,
     'script': script,
     'script': script,
     'parameters': parameters,
     'parameters': parameters,
@@ -184,6 +191,7 @@ def delete(request):
     try:
     try:
       pig_script = PigScript.objects.get(id=script_id)
       pig_script = PigScript.objects.get(id=script_id)
       pig_script.can_edit_or_exception(request.user)
       pig_script.can_edit_or_exception(request.user)
+      pig_script.doc.all().delete()
       pig_script.delete()
       pig_script.delete()
     except:
     except:
       None
       None

+ 0 - 8
desktop/conf.dist/hue.ini

@@ -413,10 +413,6 @@
   # Location on HDFS where the oozie examples and workflows are stored.
   # Location on HDFS where the oozie examples and workflows are stored.
   ## remote_data_dir=/user/hue/oozie/workspaces
   ## remote_data_dir=/user/hue/oozie/workspaces
 
 
-  # Share workflows and coordinators information with all users. If set to false,
-  # they will be visible only to the owner and administrators.
-  ## share_jobs=True
-
   # Maximum of Oozie workflows or coodinators to retrieve in one API call.
   # Maximum of Oozie workflows or coodinators to retrieve in one API call.
   ## oozie_jobs_count=100
   ## oozie_jobs_count=100
 
 
@@ -440,10 +436,6 @@
   # Timeout in seconds for thrift calls to Hive service
   # Timeout in seconds for thrift calls to Hive service
   ## server_conn_timeout=120
   ## server_conn_timeout=120
 
 
-  # Share saved queries with all users. If set to false, saved queries are
-  # visible only to the owner and administrators.
-  ## share_saved_queries=true
-
   # Path to HiveServer2 start script
   # Path to HiveServer2 start script
   ## hive_server_bin=/usr/lib/hive/bin/hiveserver2
   ## hive_server_bin=/usr/lib/hive/bin/hiveserver2
 
 

+ 0 - 8
desktop/conf/pseudo-distributed.ini.tmpl

@@ -416,10 +416,6 @@
   # Location on HDFS where the oozie examples and workflows are stored.
   # Location on HDFS where the oozie examples and workflows are stored.
   ## remote_data_dir=/user/hue/oozie/workspaces
   ## remote_data_dir=/user/hue/oozie/workspaces
 
 
-  # Share workflows and coordinators information with all users. If set to false,
-  # they will be visible only to the owner and administrators.
-  ## share_jobs=True
-
   # Maximum of Oozie workflows or coodinators to retrieve in one API call.
   # Maximum of Oozie workflows or coodinators to retrieve in one API call.
   ## oozie_jobs_count=100
   ## oozie_jobs_count=100
 
 
@@ -443,10 +439,6 @@
   # Timeout in seconds for thrift calls to Hive service
   # Timeout in seconds for thrift calls to Hive service
   ## server_conn_timeout=120
   ## server_conn_timeout=120
 
 
-  # Share saved queries with all users. If set to false, saved queries are
-  # visible only to the owner and administrators.
-  ## share_saved_queries=true
-
   # Path to HiveServer2 start script
   # Path to HiveServer2 start script
   ## hive_server_bin=/usr/lib/hive/bin/hiveserver2
   ## hive_server_bin=/usr/lib/hive/bin/hiveserver2
 
 

+ 30 - 0
desktop/core/src/desktop/management/commands/sync_documents.py

@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# 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.
+
+import logging
+
+from django.core.management.base import NoArgsCommand
+
+from desktop.models import Document
+
+
+class Command(NoArgsCommand):
+
+  def handle_noargs(self, **options):
+    logging.info('Creating document objects...')
+    Document.objects.sync()
+    logging.info('Documents created.')

+ 171 - 0
desktop/core/src/desktop/migrations/0007_auto__add_documentpermission__add_documenttag__add_document.py

@@ -0,0 +1,171 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+from desktop.models import Document
+
+class Migration(SchemaMigration):
+    
+    def forwards(self, orm):
+        
+        # Adding model 'DocumentPermission'
+        db.create_table('desktop_documentpermission', (
+            ('perms', self.gf('django.db.models.fields.TextField')(default='read')),
+            ('doc', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['desktop.Document'])),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+        ))
+        db.send_create_signal('desktop', ['DocumentPermission'])
+
+        # Adding M2M table for field users on 'DocumentPermission'
+        db.create_table('desktop_documentpermission_users', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('documentpermission', models.ForeignKey(orm['desktop.documentpermission'], null=False)),
+            ('user', models.ForeignKey(orm['auth.user'], null=False))
+        ))
+        db.create_unique('desktop_documentpermission_users', ['documentpermission_id', 'user_id'])
+
+        # Adding M2M table for field groups on 'DocumentPermission'
+        db.create_table('desktop_documentpermission_groups', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('documentpermission', models.ForeignKey(orm['desktop.documentpermission'], null=False)),
+            ('group', models.ForeignKey(orm['auth.group'], null=False))
+        ))
+        db.create_unique('desktop_documentpermission_groups', ['documentpermission_id', 'group_id'])
+
+        # Adding model 'DocumentTag'
+        db.create_table('desktop_documenttag', (
+            ('owner', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+            ('tag', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+        ))
+        db.send_create_signal('desktop', ['DocumentTag'])
+
+        # Adding model 'Document'
+        db.create_table('desktop_document', (
+            ('description', self.gf('django.db.models.fields.TextField')(default='')),
+            ('extra', self.gf('django.db.models.fields.TextField')(default='')),
+            ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('last_modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
+            ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
+            ('version', self.gf('django.db.models.fields.SmallIntegerField')(default=1)),
+            ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='doc_owner', to=orm['auth.User'])),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.TextField')(default='')),
+        ))
+        db.send_create_signal('desktop', ['Document'])
+
+        # Adding M2M table for field tags on 'Document'
+        db.create_table('desktop_document_tags', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('document', models.ForeignKey(orm['desktop.document'], null=False)),
+            ('documenttag', models.ForeignKey(orm['desktop.documenttag'], null=False))
+        ))
+        db.create_unique('desktop_document_tags', ['document_id', 'documenttag_id'])
+    
+    
+        Document.objects.sync()
+    
+    
+    def backwards(self, orm):
+        
+        # Deleting model 'DocumentPermission'
+        db.delete_table('desktop_documentpermission')
+
+        # Removing M2M table for field users on 'DocumentPermission'
+        db.delete_table('desktop_documentpermission_users')
+
+        # Removing M2M table for field groups on 'DocumentPermission'
+        db.delete_table('desktop_documentpermission_groups')
+
+        # Deleting model 'DocumentTag'
+        db.delete_table('desktop_documenttag')
+
+        # Deleting model 'Document'
+        db.delete_table('desktop_document')
+
+        # Removing M2M table for field tags on 'Document'
+        db.delete_table('desktop_document_tags')
+    
+    
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 9, 12, 14, 47, 31, 225858)'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 9, 12, 14, 47, 31, 225783)'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'desktop.document': {
+            'Meta': {'object_name': 'Document'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'extra': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'doc_owner'", 'to': "orm['auth.User']"}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['desktop.DocumentTag']", 'db_index': 'True', 'symmetrical': 'False'}),
+            'version': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+        },
+        'desktop.documentpermission': {
+            'Meta': {'object_name': 'DocumentPermission'},
+            'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['desktop.Document']"}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'db_index': 'True', 'symmetrical': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'perms': ('django.db.models.fields.TextField', [], {'default': "'read'"}),
+            'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
+        },
+        'desktop.documenttag': {
+            'Meta': {'object_name': 'DocumentTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'tag': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'})
+        },
+        'desktop.settings': {
+            'Meta': {'object_name': 'Settings'},
+            'collect_usage': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'tours_and_tutorials': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True', 'blank': 'True'})
+        },
+        'desktop.userpreferences': {
+            'Meta': {'object_name': 'UserPreferences'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'value': ('django.db.models.fields.TextField', [], {'max_length': '4096'})
+        }
+    }
+    
+    complete_apps = ['desktop']

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

@@ -15,8 +15,20 @@
 # See the License for the specific language governing permissions and
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # limitations under the License.
 
 
+from itertools import chain
+
 from django.db import models
 from django.db import models
+from django.db.models import Q
 from django.contrib.auth import models as auth_models
 from django.contrib.auth import models as auth_models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.utils.translation import ugettext as _, ugettext_lazy as _t
+
+from desktop.lib.i18n import force_unicode
+from desktop.lib.exceptions_renderable import PopupException
+
+from useradmin.models import get_default_user_group
+
 
 
 
 
 class UserPreferences(models.Model):
 class UserPreferences(models.Model):
@@ -34,3 +46,223 @@ class Settings(models.Model):
   def get_settings(cls):
   def get_settings(cls):
     settings, created = Settings.objects.get_or_create(id=1)
     settings, created = Settings.objects.get_or_create(id=1)
     return settings
     return settings
+
+
+class DocumentTag(models.Model):
+  owner = models.ForeignKey(auth_models.User, db_index=True)
+  tag = models.SlugField()
+
+  DEFAULT = 'default'
+  TRASH = 'trash'
+
+  unique_together = ('owner', 'tag')
+
+  def __unicode__(self):
+    return force_unicode('%s') % (self.tag,)
+
+  @classmethod
+  def get_trash_tag(cls, user):
+    tag, created = DocumentTag.objects.get_or_create(owner=user, tag=DocumentTag.TRASH)
+    return tag
+  
+  @classmethod
+  def add_or_create_tag(cls, owner, doc_id, tag_name, tag_id=None):
+    tag = DocumentTag.objects.get(id=tag_id, owner=owner)
+    if tag.id == DocumentTag.get_trash_tag(owner):
+      raise Exception(_("Can't add trash tag. Please trash the document from instead."))
+        
+    if tag_id is not None:
+      tag = DocumentTag.objects.create(tag=tag_name, owner=owner)  
+      
+    doc = Document.objects.get_doc(doc_id, owner=owner)
+    doc.add(tag)
+
+
+  @classmethod
+  def remove_tag(cls, tag_id, owner, doc_id):
+    tag = DocumentTag.objects.get(id=tag_id, owner=owner)
+    if tag.id == DocumentTag.get_trash_tag(owner):
+      raise Exception(_("Can't remove trash tag. Please restore the document from the trash instead."))
+
+    doc = Document.objects.get_doc(doc_id, owner=owner)
+    doc.remove(tag)
+    if tag.tag != DocumentTag.TRASH and not tag.document_set.exists():
+      tag.delete()    
+
+
+class DocumentManager(models.Manager):
+
+  def documents(self, user):
+    return Document.objects.filter(Q(owner=user) | Q(documentpermission__users=user) | Q(documentpermission__groups__in=user.groups.all()))
+  
+  def get_docs(self, user):
+    return Document.objects.documents(user).exclude(name='pig-app-hue-script')
+    
+  def get_doc(self, doc_id, user):
+    return Document.objects.documents(user).get(id=doc_id)  
+  
+  def trashed_docs(self, model_class, user):
+    ct = ContentType.objects.get_for_model(model_class)
+    tag = DocumentTag.get_trash_tag(user=user)
+    
+    return Document.objects.get_docs(user).filter(content_type=ct).filter(tags__in=[tag]).order_by('-last_modified')
+
+  def trashed(self, model_class, user):
+    docs = self.trashed_docs(model_class, user)
+    
+    return [job.content_object for job in docs if job.content_object]
+  
+  def available_docs(self, model_class, user):
+    ct = ContentType.objects.get_for_model(model_class)
+    tag = DocumentTag.get_trash_tag(user=user)
+    
+    return Document.objects.get_docs(user).filter(content_type=ct).exclude(tags__in=[tag]).order_by('-last_modified')
+          
+  def available(self, model_class, user):
+    docs = self.available_docs(model_class, user)
+    
+    return [job.content_object for job in docs if job.content_object]
+  
+  def is_accessible_or_exception(self, user, doc_class, doc_id, exception_class=PopupException):
+    if doc_id is None:
+      return
+    try:
+      ct = ContentType.objects.get_for_model(doc_class)
+      doc = Document.objects.get(object_id=doc_id, content_type=ct)
+      if doc.is_accessible(user):
+        return doc
+      else:
+        message = _("Permission denied. %(username)s does not have the permissions required to access document %(id)s") % \
+            {'username': user.username, 'id': doc.id}
+        raise exception_class(message)
+
+    except Document.DoesNotExist:
+      raise exception_class(_('Document %(id)s does not exist') % {'id': doc_id})
+
+  def is_accessible(self, user, doc_class, doc_id):
+    ct = ContentType.objects.get_for_model(doc_class)    
+    doc = Document.objects.get(object_id=doc_id, content_type=ct)
+
+    return doc.is_accessible(user)
+  
+  def link(self, content_object, owner, name='', description='', extra=''):
+    doc = Document.objects.create(
+              content_object=content_object,
+              owner=owner,
+              name=name,
+              description=description,
+              extra=extra
+          )
+
+    tag, created = DocumentTag.objects.get_or_create(owner=owner, tag=DocumentTag.DEFAULT)
+    doc.tags.add(tag)
+
+    return doc
+
+  def sync(self):
+    try:
+      from oozie.models import Workflow, Coordinator, Bundle
+
+      for job in list(chain(Workflow.objects.all(), Coordinator.objects.all(), Bundle.objects.all())):
+        if not job.doc.exists():
+          doc = Document.objects.link(job, owner=job.owner, name=job.name, description=job.description)
+          tag, created = DocumentTag.objects.get_or_create(owner=job.owner, tag='default')
+          doc.tags.add(tag)
+          if job.is_trashed:
+            doc.send_to_trash()
+          if job.is_shared:
+            DocumentPermission.share_to_default(doc)
+    except Exception, e:
+      print e
+       
+       
+class Document(models.Model):
+  owner = models.ForeignKey(auth_models.User, db_index=True, verbose_name=_t('Owner'), help_text=_t('User who can own the job.'), related_name='doc_owner')
+  name = models.TextField(default='')
+  description = models.TextField(default='')
+
+  last_modified = models.DateTimeField(auto_now=True, db_index=True, verbose_name=_t('Last modified'))  
+  version = models.SmallIntegerField(default=1, verbose_name=_t('Schema version'))
+  extra = models.TextField(default='')
+  
+  tags = models.ManyToManyField(DocumentTag, db_index=True)
+  
+  content_type = models.ForeignKey(ContentType)
+  object_id = models.PositiveIntegerField()
+  content_object = generic.GenericForeignKey('content_type', 'object_id')
+
+  objects = DocumentManager()
+  unique_together = ('content_type', 'object_id')
+
+  def __unicode__(self):
+    return force_unicode('%s %s %s') % (self.content_type, self.name, self.owner)
+      
+  def is_editable(self, user):
+    """Deprecated by can_read"""
+    return self.can_write(user)
+
+  def can_edit_or_exception(self, user, exception_class=PopupException):
+    """Deprecated by can_write_or_exception"""
+    return self.can_write_or_exception(user, exception_class)
+      
+  def send_to_trash(self):
+    tag = DocumentTag.get_trash_tag(user=self.owner)
+    self.tags.add(tag)
+
+  def restore_from_trash(self):
+    tag = DocumentTag.get_trash_tag(user=self.owner)
+    self.tags.remove(tag)
+
+  def is_accessible(self, user):
+    return user.is_superuser or self.owner == user or Document.objects.get_doc(self.id, user) 
+
+  def can_read(self, user):
+    return user.is_superuser or self.owner == user or Document.objects.get_doc(self.id, user)
+
+  def can_write(self, user):
+    return user.is_superuser or self.owner == user
+
+  def can_read_or_exception(self, user, exception_class=PopupException):
+    if self.can_read(user):
+      return True
+    else:
+      raise exception_class(_('Only superusers and %s are allowed to read this document.') % user)
+
+  def can_write_or_exception(self, user, exception_class=PopupException):
+    if self.can_write(user):
+      return True
+    else:
+      raise exception_class(_('Only superusers and %s are allowed to modify this document.') % user) 
+
+  def copy(self):
+    copy_doc = self
+
+    tags = self.tags.all()
+
+    copy_doc.pk = None
+    copy_doc.id = None
+    copy_doc.save()
+    
+    copy_doc.tags.add(*tags)
+    
+    return copy_doc 
+
+
+
+class DocumentPermission(models.Model):
+  doc = models.ForeignKey(Document)
+  
+  users = models.ManyToManyField(auth_models.User, db_index=True)
+  groups = models.ManyToManyField(auth_models.Group, db_index=True) 
+  perms = models.TextField(
+      default='read', choices=(('read', 'read'),),)    
+
+  @classmethod
+  def share_to_default(cls, document):
+    perm, created = DocumentPermission.objects.get_or_create(doc=document)
+    default_group = get_default_user_group()
+    if default_group:
+      perm.groups.add(default_group)
+
+# HistoryTable
+# VersionTable

+ 31 - 49
desktop/core/src/desktop/templates/home.mako

@@ -37,87 +37,69 @@ ${ commonheader(_('Welcome Home'), "home", user) | n,unicode }
       </div>
       </div>
   </div>
   </div>
 
 
-<%def name="app_link(app, label=None, extra_path = '')">
-  % if app in apps:
-  <li>
-    <a href="/${ app }/${ extra_path }"  title="${ apps[app].nice_name }" class="app-tooltips">
-      <i class="icon-double-angle-right"></i> ${ label }
-    </a>
-  </li>
-  % endif
-</%def>
-
 <div style="position: absolute;top:80px;right:30px"><img src="/static/art/hue-logo-subtle.png"/></div>
 <div style="position: absolute;top:80px;right:30px"><img src="/static/art/hue-logo-subtle.png"/></div>
 
 
 <div class="container-fluid">
 <div class="container-fluid">
   <div class="row-fluid">
   <div class="row-fluid">
     <div class="span12">
     <div class="span12">
       <h1>${_('Welcome Home.')}</h1>
       <h1>${_('Welcome Home.')}</h1>
-
-      <p>
-        ${ _('Hue is a Web UI for Apache Hadoop. Select an application below.') }
-      </p>
     </div>
     </div>
   </div>
   </div>
   <div class="row-fluid" style="margin-top: 30px">
   <div class="row-fluid" style="margin-top: 30px">
 
 
-    <div class="span4">
+    <div class="span2">
       <div class="card card-home card-listcontent">
       <div class="card card-home card-listcontent">
-        <h2 class="card-heading simple">${_('Query')}</h2>
-
-        <div class="card-body">
-          <p>
-          <ul>
-            ${ app_link("beeswax", "Hive") }
-            ${ app_link("impala", "Impala") }
-            ${ app_link("pig", "Pig") }
-            ${ app_link("search", _('Search')) }
-            ${ app_link("hbase", _('HBase')) }
-            ${ app_link("shell", _('Shell')) }
-          </ul>
-          </p>
-        </div>
+        <h2 class="card-heading simple">${_('New')}</h2>
       </div>
       </div>
-    </div>
 
 
-    <div class="span4">
       <div class="card card-home card-listcontent">
       <div class="card card-home card-listcontent">
-        <h2 class="card-heading simple">${_('Hadoop')}</h2>
+        <h2 class="card-heading simple">${_('Tags')}</h2>
 
 
         <div class="card-body">
         <div class="card-body">
           <p>
           <p>
           <ul>
           <ul>
-            ${ app_link("filebrowser", _('Files')) }
-            ${ app_link("jobbrowser", _('Jobs')) }
-            ${ app_link("metastore", _('Tables')) }
-            ${ app_link("sqoop", _('Sqoop 2')) }
-            ${ app_link("jobsub", _('Designs')) }
+            % for tag in tags:            
+            <li>
+              <span class="label label-info">${ tag.tag }</span>
+              % if loop.first: 
+                (selected)
+              % endif
+            </li>
+            % endfor
           </ul>
           </ul>
           </p>
           </p>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
 
 
-    <div class="span4">
+    <div class="span10">
       <div class="card card-home card-listcontent">
       <div class="card card-home card-listcontent">
-        <h2 class="card-heading simple">${_('Workflow')}</h2>
+        <h2 class="card-heading simple">${_('Documents')}</h2>
 
 
         <div class="card-body">
         <div class="card-body">
           <p>
           <p>
-          <ul>
-            ${ app_link("oozie", _('Dashboard')) }
-            ${ app_link("oozie", _('Editor'), "list_workflows/") }
-          </ul>
+          <table>
+            % for doc in documents:
+              <tr>
+                <td>${ doc.content_type }</td>
+                <td><a href="${ doc.content_object.get_absolute_url() }">${ doc.name }</a></td>
+                <td>${ doc.description }</td>
+                <td class="span1">
+                  % for tag in doc.tags.all():
+                    <span class="label label-info">${ tag.tag }</span>
+                  % endfor
+                </td>
+                <td class="span1">${ doc.owner }</td>
+                <td class="span1">${ doc.last_modified }</td>
+              </tr>
+            % endfor
+          </table>
+          </p>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
+
   </div>
   </div>
 </div>
 </div>
 
 
-<script type="text/javascript" charset="utf-8">
-  $(document).ready(function(){
-    $(".app-tooltips").tooltip();
-  });
-</script>
-
 ${ commonfooter(messages) | n,unicode }
 ${ commonfooter(messages) | n,unicode }

+ 35 - 23
desktop/core/src/desktop/urls.py

@@ -45,35 +45,47 @@ def static_pattern(urlprefix, root):
 admin.autodiscover()
 admin.autodiscover()
 
 
 # Some django-wide URLs
 # Some django-wide URLs
-dynamic_patterns = patterns('',
-  (r'^accounts/login/$', 'desktop.auth.views.dt_login'),
-  (r'^accounts/logout/$', 'desktop.auth.views.dt_logout', {'next_page': '/'}),
-  (r'^logs$','desktop.views.log_view'),
-  (r'^home$','desktop.views.home'),
-  (r'^dump_config$','desktop.views.dump_config'),
-  (r'^download_logs$','desktop.views.download_log_view'),
-  (r'^bootstrap.js$', 'desktop.views.bootstrap'),
-  (r'^profile$', 'desktop.auth.views.profile'),
-  (r'^prefs/(?P<key>\w+)?$', 'desktop.views.prefs'),
-  (r'^status_bar/?$', 'desktop.views.status_bar'),
-  (r'^admin/', include(admin.site.urls)),
-  (r'^debug/threads$', 'desktop.views.threads'),
-  (r'^debug/who_am_i$', 'desktop.views.who_am_i'),
-  (r'^debug/check_config$', 'desktop.views.check_config'),
-  (r'^debug/check_config_ajax$', 'desktop.views.check_config_ajax'),
-  (r'^log_frontend_event$', 'desktop.views.log_frontend_event'),
-
-  # Oauth
-  (r'^login/oauth/?$', 'desktop.auth.views.oauth_login'),
-  (r'^login/oauth_authenticated/?$', 'desktop.auth.views.oauth_authenticated'),
+dynamic_patterns = patterns('desktop.auth.views',
+  (r'^accounts/login/$', 'dt_login'),
+  (r'^accounts/logout/$', 'dt_logout', {'next_page': '/'}),
+  (r'^profile$', 'profile'),  
+  (r'^login/oauth/?$', 'oauth_login'),
+  (r'^login/oauth_authenticated/?$', 'oauth_authenticated'),  
+)
+  
+dynamic_patterns += patterns('desktop.views',
+  (r'^logs$','log_view'),
+  (r'^home$','home'),
+  (r'^dump_config$','dump_config'),
+  (r'^download_logs$','download_log_view'),
+  (r'^bootstrap.js$', 'bootstrap'),
+  
+  (r'^prefs/(?P<key>\w+)?$', 'prefs'),
+  (r'^status_bar/?$', 'status_bar'),
+  (r'^debug/threads$', 'threads'),
+  (r'^debug/who_am_i$', 'who_am_i'),
+  (r'^debug/check_config$', 'check_config'),
+  (r'^debug/check_config_ajax$', 'check_config_ajax'),
+  (r'^log_frontend_event$', 'log_frontend_event'),
+
+  # Doc model
+  (r'^doc/add_or_create_tag$', 'add_or_create_tag'),
+  (r'^doc/remove_tag$', 'remove_tag'),
+  (r'^doc/add_or_update_permission$', 'add_or_update_permission'),
+  (r'^doc/remove_permission$', 'remove_permission'),
 
 
   # Jasmine
   # Jasmine
-  (r'^jasmine', 'desktop.views.jasmine'),
+  (r'^jasmine', 'jasmine'),
 
 
   # Top level web page!
   # Top level web page!
-  (r'^$', 'desktop.views.index'),
+  (r'^$', 'index'),
 )
 )
 
 
+dynamic_patterns += patterns('',
+  (r'^admin/', include(admin.site.urls)),
+)
+
+
 static_patterns = []
 static_patterns = []
 
 
 # SAML specific
 # SAML specific

+ 45 - 5
desktop/core/src/desktop/views.py

@@ -35,10 +35,10 @@ import django.views.debug
 from desktop.lib import django_mako
 from desktop.lib import django_mako
 from desktop.lib.conf import GLOBAL_CONFIG
 from desktop.lib.conf import GLOBAL_CONFIG
 from desktop.lib.django_util import login_notrequired, render_json, render
 from desktop.lib.django_util import login_notrequired, render_json, render
-from desktop.lib.i18n import smart_str
+from desktop.lib.i18n import smart_str, force_unicode
 from desktop.lib.paths import get_desktop_root
 from desktop.lib.paths import get_desktop_root
 from desktop.log.access import access_log_level, access_warn
 from desktop.log.access import access_log_level, access_warn
-from desktop.models import UserPreferences, Settings
+from desktop.models import UserPreferences, Settings, Document, DocumentTag
 from desktop import appmanager
 from desktop import appmanager
 import desktop.conf
 import desktop.conf
 import desktop.log.log_buffer
 import desktop.log.log_buffer
@@ -46,11 +46,51 @@ import desktop.log.log_buffer
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
+
 @access_log_level(logging.WARN)
 @access_log_level(logging.WARN)
 def home(request):
 def home(request):
-  apps = appmanager.get_apps(request.user)
-  apps = dict([(app.name, app) for app in apps])
-  return render('home.mako', request, dict(apps=apps))
+  return render('home.mako', request, {
+    'documents': Document.objects.get_docs(request.user).order_by('-last_modified')[:100],
+    'tags': DocumentTag.objects.filter(owner=request.user),
+  })
+
+
+def add_or_create_tag(request):
+  response = {'status': -1, 'message': ''}
+  
+  if request.action == 'POST':
+    json = {'tag_id': 1, 'tag': 'hue project', 'doc_id': 1}   # instead ... json.load(request.POST) 
+    try:
+      DocumentTag.add_or_create_tag(request.user, json['doc_id'], json['tag'], json.get('tag_id'))
+    except Exception, e:
+      response['message'] = force_unicode(e)
+  else:
+    response['message'] = _('POST request only')
+  
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def remove_tag(request):
+  response = {'status': -1, 'message': _('Error')}
+  
+  if request.action == 'POST':
+    json = {'tag_id': 1, 'tag': 'hue project', 'doc_id': 1}  # instead ... json.load(request.POST)
+    try:
+      DocumentTag.remove_tag(id=json['tag_id'], owner=request.user, doc_id=json['doc_id'])
+      response['message'] = _('Tag removed !')
+    except Exception, e:
+      response['message'] = force_unicode(e)
+  else:
+    response['message'] = _('POST request only')
+  
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def add_or_update_permission(request):
+  pass
+
+def remove_permission(request):
+  pass
 
 
 
 
 @access_log_level(logging.WARN)
 @access_log_level(logging.WARN)