浏览代码

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

Converting the Pig app, Beeswax, Impala and Oozie apps
Romain Rigaux 12 年之前
父节点
当前提交
c9e7ed0e1a
共有 34 个文件被更改,包括 755 次插入322 次删除
  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)