Explorar o código

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

Converting the Pig app, Beeswax, Impala and Oozie apps
Romain Rigaux %!s(int64=12) %!d(string=hai) anos
pai
achega
c9e7ed0e1a
Modificáronse 34 ficheiros con 755 adicións e 322 borrados
  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,
   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):
   # 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.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 enum import Enum
 
 from desktop.lib.exceptions_renderable import PopupException
+from desktop.models import Document
 
 from beeswax.design import HQLdesign, hql_query
 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
   notify = models.BooleanField(default=False)                        # Notify on completion
 
+
   class Meta:
     ordering = ['-submission_date']
 
@@ -87,8 +91,9 @@ class QueryHistory(models.Model):
   def get(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'
     else:
       return 'beeswax'
@@ -96,7 +101,7 @@ class QueryHistory(models.Model):
   def get_query_server_config(self):
     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({
         'server_name': self.server_name,
         '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'),
                                    help_text=_t('If this query is trashed.'))
 
+  doc = generic.GenericRelation(Document, related_name='hql_doc')
+
   class Meta:
     ordering = ['-mtime']
 
@@ -276,6 +283,9 @@ class SavedQuery(models.Model):
     except:
       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):
   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 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 + ':list_trashed_designs') }" title="${_('Go to the trash')}"><i class="icon-trash"></i> ${_('View trash')}</a>
         </div>
       </%def>
     </%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()">
         <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">
             <i class="icon-fire"></i> ${_('Empty trash')}
+          </button>        
+          <button id="viewQueriesBtn" class="btn" title="${_('View queries')}">
+            <i class="icon-home"></i> ${_('Back')}
           </button>
         </div>
       </%def>

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

@@ -102,7 +102,7 @@ ${layout.menubar(section='my queries')}
             </td>
             <td>
               % if design.desc:
-              ${design.desc}
+                ${ design.desc }
               % endif
             </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 login_notrequired, get_desktop_uri_prefix
 from desktop.lib.exceptions_renderable import PopupException
+from desktop.models import Document
 
 from jobsub.parameterization import find_variables, substitute_variables
 
@@ -42,7 +43,7 @@ import beeswax.forms
 import beeswax.design
 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.design import HQLdesign
 from beeswax.models import SavedQuery, make_query_context, QueryHistory
@@ -58,7 +59,6 @@ LOG = logging.getLogger(__name__)
 def index(request):
   return execute_query(request)
 
-
 """
 Design views
 """
@@ -111,6 +111,9 @@ def save_design(request, form, type, design, explicit_save):
   design.data = new_data
 
   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' %
            (explicit_save and '' or 'auto ', design.name, design.id, design.owner))
@@ -136,6 +139,7 @@ def save_design_properties(request):
     elif field == 'description':
       design.desc = request.POST.get('value')
     design.save()
+    design.doc.update(name=design.name, description=design.desc)
     response['status'] = 0
   except Exception, e:
     response['data'] = str(e)
@@ -146,17 +150,17 @@ def save_design_properties(request):
 def delete_design(request):
   if request.method == 'POST':
     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():
       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)
 
     for design in designs.values():
       if request.POST.get('skipTrash', 'false') == 'false':
-        design.is_trashed = True
-        design.save()
+        design.doc.get().send_to_trash()
       else:
+        design.doc.all().delete()
         design.delete()
     return redirect(reverse(get_app_name(request) + ':list_designs'))
   else:
@@ -173,8 +177,7 @@ def restore_design(request):
       return list_designs(request)
 
     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'))
   else:
     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.save()
   messages.info(request, _('Copied design: %(name)s') % {'name': design.name})
+
   return format_preserving_redirect(
       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
   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.
   prefix = 'q-'
   querydict_query = _copy_prefix(prefix, request.GET)
   # Manually limit up the user filter.
-  querydict_query[ prefix + 'user' ] = user
+  querydict_query[ prefix + 'user' ] = request.user
   querydict_query[ prefix + 'type' ] = app_name
   page, filter_params = _list_designs(querydict_query, DEFAULT_PAGE_SIZE, prefix)
 
@@ -276,7 +275,7 @@ def my_queries(request):
   prefix = 'h-'
   querydict_history = _copy_prefix(prefix, request.GET)
   # 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
 
   hist_page, hist_filter = _list_query_history(request.user,
@@ -287,7 +286,7 @@ def my_queries(request):
   prefix = 'q-'
   querydict_query = _copy_prefix(prefix, request.GET)
   # 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
 
   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
   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()
   if not share_queries:
@@ -439,7 +438,7 @@ def execute_query(request, design_id=None):
     'log': log,
     'autocomplete_base_url': reverse(get_app_name(request) + ':autocomplete', kwargs={}),
     '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
 """
 
+# owner_only is deprecated
 def authorized_get_design(request, design_id, owner_only=False, must_exist=False):
   if design_id is None and not must_exist:
     return None
   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:
       raise PopupException(_('Design %(id)s does not exist.') % {'id': design_id})
     else:
       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:
-    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):
   if query_history_id is None and not must_exist:
     return None
   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:
       raise PopupException(_('QueryHistory %(id)s does not exist.') % {'id': query_history_id})
     else:
       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):
@@ -949,10 +948,7 @@ def safe_get_design(request, design_type, design_id=None):
   design = 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:
     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
 
   SORT_ATTR_TRANSLATION = dict(
-    date='mtime',
+    date='last_modified',
     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')
-  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
   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
   frag = querydict.get(prefix + 'text')
   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
   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
   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))
-  paginator = Paginator(db_queryset, page_size)
+  paginator = Paginator(designs, page_size)
   page = paginator.page(pagenum)
 
   # 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",
   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(
   key='oozie_jobs_count',
   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,\
   Dataset, DataInput, DataOutput, Pig, Link, Hive, Sqoop, Ssh, Shell, DistCp, Fs,\
   Email, SubWorkflow, Generic, Bundle, BundledCoordinator
+from desktop.models import Document
 
 
 LOG = logging.getLogger(__name__)
@@ -273,7 +274,7 @@ class SubWorkflowForm(forms.ModelForm):
     user = kwargs.pop('user')
     workflow = kwargs.pop('workflow')
     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'}))
 
   class Meta:
@@ -350,12 +351,12 @@ class CoordinatorForm(forms.ModelForm):
     user = kwargs['user']
     del kwargs['user']
     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 = []
     for workflow in qs:
       if workflow.is_accessible(user):
         workflows.append(workflow.id)
-    qs = qs.filter(id__in=workflows)
+    qs = Workflow.objects.filter(id__in=workflows)
     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 django.db import models
+from django.db.models import Q
 from django.core.urlresolvers import reverse
 from django.core.validators import RegexValidator
 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.utils.encoding import force_unicode
 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.exceptions_renderable import PopupException
 from desktop.lib.json_utils import JSONEncoderForHTML
+from desktop.models import Document
 from hadoop.fs.exceptions import WebHdfsException
 
 from hadoop.fs.hadoopfs import Hdfs
 from liboozie.submittion import Submission
 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
 
 
@@ -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'))
 
 
-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:
@@ -106,14 +103,12 @@ class JobManager(models.Manager):
 
 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'))
-  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.'))
   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'),
@@ -121,27 +116,27 @@ class Job(models.Model):
   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 '
                                                 '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.'))
   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).'))
-  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.'))
+  doc = generic.GenericRelation(Document, related_name='oozie_doc')
 
   objects = JobManager()
   unique_together = ('owner', 'name')
 
   def delete(self, skip_trash=False, *args, **kwargs):
     if skip_trash:
+      self.doc.all().delete()
       return super(Job, self).delete(*args, **kwargs)
     else:
-      self.is_trashed = True
-      self.save()
+      self.doc.get().send_to_trash()
       return self
 
   def restore(self):
-    self.is_trashed = False
-    self.save()
+    self.doc.get().restore_from_trash()
     return self
 
   def save(self):
@@ -191,6 +186,7 @@ class Job(models.Model):
 
   @property
   def status(self):
+    # TODO
     if self.is_shared:
       return _('shared')
     else:
@@ -205,14 +201,15 @@ class Job(models.Model):
     return  [{'name': name, 'value': value} for name, value in params.iteritems()]
 
   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):
     """Only owners or admins can modify a job."""
     return user.is_superuser or self.owner == user
 
 
-class WorkflowManager(TrashManager):
+class WorkflowManager(models.Manager):
   def new_workflow(self, owner):
     workflow = Workflow(owner=owner, schema_version='uri:oozie:workflow:0.4')
 
@@ -242,6 +239,8 @@ class WorkflowManager(TrashManager):
     workflow.end = end
     workflow.save()
 
+    Document.objects.link(workflow, owner=workflow.owner, name=workflow.name, description=workflow.description)
+
     self.check_workspace(workflow, fs)
 
   def check_workspace(self, workflow, fs):
@@ -272,9 +271,6 @@ class WorkflowManager(TrashManager):
 
 
 class Workflow(Job):
-  """
-  http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/WorkflowFunctionalSpec.html
-  """
   is_single = models.BooleanField(default=False)
   start = models.ForeignKey('Start', related_name='start_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):
     source_deployment_dir = self.deployment_dir # Needed
     nodes = self.node_set.all()
+    copy_doc = self.doc.get().copy()    
     links = Link.objects.filter(parent__workflow=self)
 
     copy = self
@@ -333,6 +330,10 @@ class Workflow(Job):
     copy.end = old_nodes_mapping[self.end.id]
     copy.save()
 
+    copy_doc.name = copy.name
+    copy_doc.save()
+    copy.doc.add(copy_doc)    
+
     try:
       if copy.is_shared:
         perms = 0755
@@ -415,7 +416,7 @@ class Workflow(Job):
     return 'workflow.xml'
 
   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):
     node = Start.objects.get(workflow=self) # Uncached version of start.
@@ -666,9 +667,6 @@ class Node(models.Model):
 
 
 class Action(Node):
-  """
-  http://incubator.apache.org/oozie/docs/3.2.0-incubating/docs/WorkflowFunctionalSpec.html#a3.2_Workflow_Action_Nodes
-  """
   types = ()
 
   class Meta:
@@ -1235,9 +1233,6 @@ DATASET_FREQUENCY = ['MINUTE', 'HOUR', 'DAY', 'MONTH', 'YEAR']
 
 
 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'),
                                               help_text=_t('The number of units of the rate at which '
                                                            'data is periodically created.'))
@@ -1271,8 +1266,6 @@ class Coordinator(Job):
   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()}'))
 
-  objects = TrashManager()
-
   HUE_ID = 'hue-id-c'
 
   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')
 
   def clone(self, new_owner=None):
+    copy_doc = self.doc.get()
     datasets = Dataset.objects.filter(coordinator=self)
     data_inputs = DataInput.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.save()
 
+    copy_doc.pk = None
+    copy_doc.id = None
+    copy_doc.save()
+    copy.doc.add(copy_doc)
+
     return copy
 
   @classmethod
@@ -1528,15 +1527,10 @@ class BundledCoordinator(models.Model):
 
 
 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'),
                                        help_text=_t('When to start the first coordinators.'))
   coordinators = models.ManyToManyField(Coordinator, through='BundledCoordinator')
 
-  objects = TrashManager()
-
   HUE_ID = 'hue-id-b'
 
   def get_type(self):
@@ -1555,6 +1549,7 @@ class Bundle(Job):
 
   def clone(self, new_owner=None):
     bundleds = BundledCoordinator.objects.filter(bundle=self)
+    copy_doc = self.doc.get()
 
     copy = self
     copy.pk = None
@@ -1571,6 +1566,11 @@ class Bundle(Job):
       bundled.bundle = copy
       bundled.save()
 
+    copy_doc.pk = None
+    copy_doc.id = None
+    copy_doc.save()
+    copy.doc.add(copy_doc)
+
     return copy
 
   @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['description']) }
             ${ 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['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">
               ${ 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['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['job_properties'] | n,unicode }
               <div class="hide">
+                ${ utils.render_field_no_popover(coordinator_form['is_shared']) }
                 ${ utils.render_field(coordinator_form['timeout']) }
                 ${ coordinator_form['schema_version'] | n,unicode }
               </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">
               <fieldset>
               ${ 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 ">
                 <label class="control-label">
@@ -64,6 +63,7 @@ ${ layout.menubar(section='workflows') }
                </div>
 
                <div class="hide">
+                 ${ utils.render_field(workflow_form['is_shared']) }
                  ${ workflow_form['schema_version'] | n,unicode }
                  ${ workflow_form['job_properties'] | 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.') }
                     </div>
                   </div>
-                </div>
-                ${ utils.render_field_no_popover(bundle_form['is_shared']) }
+                </div>                
                 ${ bundle_form['parameters'] | n,unicode }
                 <div class="hide">
+                  ${ utils.render_field_no_popover(bundle_form['is_shared']) }
                   ${ bundle_form['schema_version']  | n,unicode }
                 </div>
               </div>

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

@@ -104,10 +104,10 @@ ${ layout.menubar(section='coordinators') }
             <div class="fieldWrapper">
               ${ 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['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 }
               <div class="hide">
+                ${ utils.render_field_no_popover(coordinator_form['is_shared']) }
                 ${ utils.render_field(coordinator_form['timeout']) }
                 ${ coordinator_form['schema_version']  | n,unicode }
               </div>

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

@@ -84,7 +84,9 @@ ${ layout.menubar(section='workflows') }
             <fieldset>
         ${ 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['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'], {

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

@@ -59,9 +59,9 @@ ${ layout.menubar(section='bundles') }
     </%def>
 
     <%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>
   </%actionbar:render>
 

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

@@ -58,9 +58,9 @@ ${ layout.menubar(section='coordinators') }
     </%def>
 
     <%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>
+      &nbsp;&nbsp;
+      <a href="${ url('oozie:list_trashed_coordinators') }" class="btn"><i class="icon-trash"></i> ${ _('View trash') }</a>
     </%def>
   </%actionbar:render>
 

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

@@ -43,10 +43,11 @@ ${ layout.menubar(section='bundles') }
     </%def>
 
     <%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">
         <i class="icon-fire"></i> ${ _('Empty trash') }
       </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>
   </%actionbar:render>
 

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

@@ -46,10 +46,11 @@ ${ layout.menubar(section='coordinators') }
     </%def>
 
     <%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>
   </%actionbar:render>
 

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

@@ -37,21 +37,21 @@ ${ layout.menubar(section='workflows') }
     </%def>
 
     <%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') }
       </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') }
       </button>
     </%def>
 
     <%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') }">
         <i class="icon-fire"></i> ${ _('Empty trash') }
       </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>
   </%actionbar:render>
 
@@ -180,10 +180,11 @@ ${ layout.menubar(section='workflows') }
 
     function toggleActions() {
       $(".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) {
         $("#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 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: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>
   </%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'^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'^edit_coordinator/(?P<coordinator>[-\w]+)$', 'edit_coordinator', name='edit_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])
 
   workflow.save()
+  workflow.doc.update(name=workflow.name, description=workflow.description)
 
   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.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+from desktop.models import Document
 
 try:
   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.i18n import smart_str
 from desktop.lib.rest.http_client import RestException
-from hadoop.fs.exceptions import WebHdfsException
 from liboozie.submittion import Submission
 
 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,\
                              check_dataset_access_permission, check_dataset_edition_permission
 from oozie.import_workflow import import_workflow as _import_workflow
@@ -57,102 +56,61 @@ LOG = logging.getLogger(__name__)
 
 
 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, {
-    '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):
-  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, {
-    '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):
-  data = Coordinator.objects.available()
+  data = Document.objects.available(Coordinator, request.user)
+    
   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, {
-    '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, {
-    '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):
-  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, {
-    '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):
-  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, {
-    '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')
 
   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.workflow.restore()
 
@@ -363,6 +321,7 @@ def create_coordinator(request, workflow=None):
 
     if coordinator_form.is_valid():
       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")
     else:
       request.error(_('Errors on the form: %s') % coordinator_form.errors)
@@ -632,6 +591,7 @@ def create_bundle(request):
 
     if bundle_form.is_valid():
       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}))
     else:
       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.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 desktop.lib.exceptions_renderable import PopupException
+from desktop.models import Document as Doc
 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.'),
                                      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
 
-  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):
       return True
     else:
@@ -57,12 +60,17 @@ class PigScript(Document):
       'hadoopProperties': []
   }))
 
+  doc = generic.GenericRelation(Doc, related_name='pig_doc')
+
   def update_from_dict(self, attrs):
     data_dict = self.dict
 
     for attr in PigScript._ATTRIBUTES:
       if attrs.get(attr) is not None:
         data_dict[attr] = attrs[attr]
+        
+    if 'name' in attrs:
+      self.doc.update(name=attrs['name'])
 
     self.data = json.dumps(data_dict)
 
@@ -70,14 +78,17 @@ class PigScript(Document):
   def dict(self):
     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):
-  """This take care of security"""
   try:
     pig_script = PigScript.objects.get(id=id)
-    pig_script.can_edit_or_exception(user)
+    pig_script.doc.get().can_read_or_exception(user)
   except:
     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({
       'name': name,
@@ -90,14 +101,15 @@ def create_or_update_script(id, name, script, user, parameters, resources, hadoo
   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:
-    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
     massaged_script = {
       '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.exceptions_renderable import PopupException
 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,\
                                   check_job_edition_permission
 
@@ -57,7 +59,7 @@ def dashboard(request):
   pig_api = api.get(request.fs, request.user)
 
   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)
 
   return HttpResponse(json.dumps(massaged_jobs), mimetype="application/json")
@@ -143,7 +145,7 @@ def copy(request):
     raise PopupException(_('POST request required.'))
 
   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
   name = existing_script_data["name"] + _(' (Copy)')
@@ -152,18 +154,23 @@ def copy(request):
   resources = existing_script_data["resources"]
   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,
       'script': script,
       'parameters': parameters,
       'resources': resources,
       '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 = {
-    'id': pig_script.id,
+    'id': script_copy.id,
     'name': name,
     'script': script,
     'parameters': parameters,
@@ -184,6 +191,7 @@ def delete(request):
     try:
       pig_script = PigScript.objects.get(id=script_id)
       pig_script.can_edit_or_exception(request.user)
+      pig_script.doc.all().delete()
       pig_script.delete()
     except:
       None

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

@@ -413,10 +413,6 @@
   # Location on HDFS where the oozie examples and workflows are stored.
   ## 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.
   ## oozie_jobs_count=100
 
@@ -440,10 +436,6 @@
   # Timeout in seconds for thrift calls to Hive service
   ## 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
   ## 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.
   ## 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.
   ## oozie_jobs_count=100
 
@@ -443,10 +439,6 @@
   # Timeout in seconds for thrift calls to Hive service
   ## 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
   ## 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
 # limitations under the License.
 
+from itertools import chain
+
 from django.db import models
+from django.db.models import Q
 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):
@@ -34,3 +46,223 @@ class Settings(models.Model):
   def get_settings(cls):
     settings, created = Settings.objects.get_or_create(id=1)
     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>
 
-<%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 class="container-fluid">
   <div class="row-fluid">
     <div class="span12">
       <h1>${_('Welcome Home.')}</h1>
-
-      <p>
-        ${ _('Hue is a Web UI for Apache Hadoop. Select an application below.') }
-      </p>
     </div>
   </div>
   <div class="row-fluid" style="margin-top: 30px">
 
-    <div class="span4">
+    <div class="span2">
       <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 class="span4">
       <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">
           <p>
           <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>
           </p>
         </div>
       </div>
     </div>
 
-    <div class="span4">
+    <div class="span10">
       <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">
           <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>
 
-<script type="text/javascript" charset="utf-8">
-  $(document).ready(function(){
-    $(".app-tooltips").tooltip();
-  });
-</script>
-
 ${ commonfooter(messages) | n,unicode }

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

@@ -45,35 +45,47 @@ def static_pattern(urlprefix, root):
 admin.autodiscover()
 
 # 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
-  (r'^jasmine', 'desktop.views.jasmine'),
+  (r'^jasmine', 'jasmine'),
 
   # Top level web page!
-  (r'^$', 'desktop.views.index'),
+  (r'^$', 'index'),
 )
 
+dynamic_patterns += patterns('',
+  (r'^admin/', include(admin.site.urls)),
+)
+
+
 static_patterns = []
 
 # 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.conf import GLOBAL_CONFIG
 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.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
 import desktop.conf
 import desktop.log.log_buffer
@@ -46,11 +46,51 @@ import desktop.log.log_buffer
 
 LOG = logging.getLogger(__name__)
 
+
 @access_log_level(logging.WARN)
 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)