浏览代码

HUE-838 [jb] Create new file button

Create a new 'touch' operation for creating a text file.
Group 'Create Directory' and 'Create File' in a dropdown button.
Put icons and refactor 'Upload file/archive' buttons.
Add a new test_touch tests with no superuser permissions.
Romain Rigaux 13 年之前
父节点
当前提交
ae0e8ba62b

+ 6 - 1
apps/filebrowser/src/filebrowser/forms.py

@@ -33,7 +33,7 @@ class PathField(CharField):
     kwargs.setdefault('required', True)
     kwargs.setdefault('required', True)
     kwargs.setdefault('min_length', 1)
     kwargs.setdefault('min_length', 1)
     forms.CharField.__init__(self, label=label, help_text=help_text, **kwargs)
     forms.CharField.__init__(self, label=label, help_text=help_text, **kwargs)
-  
+
   def clean(self, value):
   def clean(self, value):
     return normpath(CharField.clean(self, value))
     return normpath(CharField.clean(self, value))
 
 
@@ -81,6 +81,11 @@ class MkDirForm(forms.Form):
   path = PathField(label=_("Path in which to create the directory"))
   path = PathField(label=_("Path in which to create the directory"))
   name = PathField(label=_("Directory Name"))
   name = PathField(label=_("Directory Name"))
 
 
+class TouchForm(forms.Form):
+  op = "touch"
+  path = PathField(label=_("Path in which to create the file"))
+  name = PathField(label=_("File Name"))
+
 class ChownForm(forms.Form):
 class ChownForm(forms.Form):
   op = "chown"
   op = "chown"
   path = PathField(label=_("Path to change user/group ownership"))
   path = PathField(label=_("Path to change user/group ownership"))

+ 1 - 1
apps/filebrowser/src/filebrowser/static/help/index.html

@@ -280,6 +280,6 @@ File Browser displays the contents of the file in the <b>File Editor</b> window.
                         </td>
                         </td>
 		    </tr>
 		    </tr>
 	    </table>
 	    </table>
-	    
+	
     </body>
     </body>
 </html>
 </html>

+ 22 - 5
apps/filebrowser/src/filebrowser/templates/listdir.mako

@@ -27,10 +27,12 @@ ${commonheader(_('File Browser'), 'filebrowser', user)}
 
 
 <div class="container-fluid">
 <div class="container-fluid">
     <h1>${_('File Browser')}</h1>
     <h1>${_('File Browser')}</h1>
+
     <%actionbar:render>
     <%actionbar:render>
         <%def name="search()">
         <%def name="search()">
             <input type="text" class="input-xlarge search-query" placeholder="${_('Search for file name')}" data-bind="value: searchQuery">
             <input type="text" class="input-xlarge search-query" placeholder="${_('Search for file name')}" data-bind="value: searchQuery">
         </%def>
         </%def>
+
         <%def name="actions()">
         <%def name="actions()">
             <button class="btn fileToolbarBtn" title="${_('Rename')}" data-bind="click: renameFile, enable: selectedFiles().length == 1"><i class="icon-font"></i> ${_('Rename')}</button>
             <button class="btn fileToolbarBtn" title="${_('Rename')}" data-bind="click: renameFile, enable: selectedFiles().length == 1"><i class="icon-font"></i> ${_('Rename')}</button>
             <button class="btn fileToolbarBtn" title="${_('Move')}" data-bind="click: move, enable: selectedFiles().length == 1"><i class="icon-random"></i> ${_('Move')}</button>
             <button class="btn fileToolbarBtn" title="${_('Move')}" data-bind="click: move, enable: selectedFiles().length == 1"><i class="icon-random"></i> ${_('Move')}</button>
@@ -40,21 +42,36 @@ ${commonheader(_('File Browser'), 'filebrowser', user)}
             <button class="btn fileToolbarBtn" title="${_('Change Permissions')}" data-bind="click: changePermissions, enable: selectedFiles().length == 1"><i class="icon-list-alt"></i> ${_('Change Permissions')}</button>
             <button class="btn fileToolbarBtn" title="${_('Change Permissions')}" data-bind="click: changePermissions, enable: selectedFiles().length == 1"><i class="icon-list-alt"></i> ${_('Change Permissions')}</button>
             <button class="btn fileToolbarBtn" title="${_('Delete')}" data-bind="click: deleteSelected, enable: selectedFiles().length == 1"><i class="icon-trash"></i> ${_('Delete')}</button>
             <button class="btn fileToolbarBtn" title="${_('Delete')}" data-bind="click: deleteSelected, enable: selectedFiles().length == 1"><i class="icon-trash"></i> ${_('Delete')}</button>
         </%def>
         </%def>
+
         <%def name="creation()">
         <%def name="creation()">
+	        <div class="btn-group" style="display: inline-block;">
+	          <a href="#" data-toggle="dropdown" class="btn dropdown-toggle">
+	            <i class="icon-plus-sign"></i> ${_('New')}
+	            <span class="caret"></span>
+	          </a>
+	          <ul class="dropdown-menu">
+	            <li><a href="#" class="create-file-link" title="${_('File')}"><i class="icon-file"></i> ${_('File')}</a></li>
+	            <li><a href="#" class="create-directory-link" title="${_('Directory')}"><i class="icon-folder-close"></i> ${_('Directory')}</a></li>
+	          </ul>
+	        </div>
+
             <div id="upload-dropdown" class="btn-group" style="display: inline-block;">
             <div id="upload-dropdown" class="btn-group" style="display: inline-block;">
-                <a href="#" class="btn upload-link dropdown-toggle" title="${_('Upload')}" data-toggle="dropdown"><i class="icon-upload"></i> ${_('Upload')}</a>
+                <a href="#" class="btn upload-link dropdown-toggle" title="${_('Upload')}" data-toggle="dropdown">
+                  <i class="icon-upload"></i> ${_('Upload')}
+                  <span class="caret"></span>
+                </a>
                 <ul class="dropdown-menu">
                 <ul class="dropdown-menu">
-                  <li><a href="#" tabindex="-1" class="upload-link" title="${_('Upload files')}" data-bind="click: uploadFile">${_('Upload files')}</a></li>
-                  <li class="divider"></li>
-                  <li><a href="#" tabindex="-1" class="upload-link" title="${_('Upload archive')}" data-bind="click: uploadArchive">${_('Upload archive')}</a></li>
+                  <li><a href="#" class="upload-link" title="${_('Files')}" data-bind="click: uploadFile"><i class="icon-file"></i> ${_('Files')}</a></li>
+                  <li><a href="#" class="upload-link" title="${_('Archive')}" data-bind="click: uploadArchive"><i class="icon-gift"></i> ${_('Archives')}</a></li>
                 </ul>
                 </ul>
             </div>
             </div>
-            <a href="#" class="btn create-directory-link" title="${_('New directory')}"><i class="icon-folder-close"></i> ${_('New directory')}</a>
         </%def>
         </%def>
     </%actionbar:render>
     </%actionbar:render>
+
     % if breadcrumbs:
     % if breadcrumbs:
         ${fb_components.breadcrumbs(path, breadcrumbs, True)}
         ${fb_components.breadcrumbs(path, breadcrumbs, True)}
     %endif
     %endif
+
     <div class="scrollable">
     <div class="scrollable">
         ${dir.list_table_browser(files, path_enc, current_request_path, cwd_set)}
         ${dir.list_table_browser(files, path_enc, current_request_path, cwd_set)}
     </div>
     </div>

+ 38 - 2
apps/filebrowser/src/filebrowser/templates/listdir_components.mako

@@ -220,6 +220,27 @@ from django.utils.translation import ugettext as _
         </form>
         </form>
     </div>
     </div>
 
 
+    <!-- new directory modal -->
+    <div id="createFileModal" class="modal hide fade">
+        <form id="createFileForm" data-bind="submit: createFile" method="POST" enctype="multipart/form-data" class="form-inline form-padding-fix">
+        <div class="modal-header">
+            <a href="#" class="close" data-dismiss="modal">&times;</a>
+            <h3>${_('Create File')}</h3>
+        </div>
+        <div class="modal-body">
+            <label>${_('File Name')} <input id="newFileNameInput" name="name" value="" type="text" class="input-xlarge"/></label>
+            <input type="hidden" name="path" type="text" data-bind="value: currentPath"/>
+        </div>
+        <div class="modal-footer">
+             <div id="fileNameRequiredAlert" class="alert-message error hide" style="position: absolute; left: 10;">
+                <p><strong>${_('Sorry, file name is required.')}</strong>
+            </div>
+            <a id="cancelCreateFileBtn" class="btn" href="#">${_('Cancel')}</a>
+            <input class="btn primary" type="submit" value="${_('Submit')}" />
+        </div>
+        </form>
+    </div>
+
     <script type="text/javascript" charset="utf-8">
     <script type="text/javascript" charset="utf-8">
         // ajax modal windows
         // ajax modal windows
         function openChownWindow(path, user, group, next){
         function openChownWindow(path, user, group, next){
@@ -313,7 +334,6 @@ from django.utils.translation import ugettext as _
                 $("#moveForm").find("input[name='dest_path']").removeClass("fieldError");
                 $("#moveForm").find("input[name='dest_path']").removeClass("fieldError");
             });
             });
 
 
-            //create directory handlers
             $(".create-directory-link").click(function(){
             $(".create-directory-link").click(function(){
                 $("#createDirectoryModal").modal({
                 $("#createDirectoryModal").modal({
                     keyboard: true,
                     keyboard: true,
@@ -321,10 +341,21 @@ from django.utils.translation import ugettext as _
                 });
                 });
             });
             });
 
 
+            $(".create-file-link").click(function(){
+                $("#createFileModal").modal({
+                    keyboard: true,
+                    show: true
+                });
+            });
+
             $("#cancelCreateDirectoryBtn").click(function(){
             $("#cancelCreateDirectoryBtn").click(function(){
                 $("#createDirectoryModal").modal("hide");
                 $("#createDirectoryModal").modal("hide");
             });
             });
 
 
+            $("#cancelCreateFileBtn").click(function(){
+                $("#createFileModal").modal("hide");
+            });
+
             $("#createDirectoryForm").submit(function(){
             $("#createDirectoryForm").submit(function(){
                 if ($.trim($("#newDirectoryNameInput").val())==""){
                 if ($.trim($("#newDirectoryNameInput").val())==""){
                     $("#directoryNameRequiredAlert").show();
                     $("#directoryNameRequiredAlert").show();
@@ -567,7 +598,7 @@ from django.utils.translation import ugettext as _
                 self.currentPath(currentDirPath);
                 self.currentPath(currentDirPath);
 
 
                 $('.uploader').trigger('fb:updatePath', {dest: self.currentPath()});
                 $('.uploader').trigger('fb:updatePath', {dest: self.currentPath()});
- 
+
                 self.isLoading(false);
                 self.isLoading(false);
                 $(".scrollable").jHueTableScroller();
                 $(".scrollable").jHueTableScroller();
             };
             };
@@ -669,6 +700,11 @@ from django.utils.translation import ugettext as _
                 return true;
                 return true;
             };
             };
 
 
+            self.createFile = function (formElement) {
+                $(formElement).attr("action", "/filebrowser/touch?next=${url('filebrowser.views.view', path=urlencode('/'))}"+ "." + self.currentPath());
+                return true;
+            };
+
             self.uploadFile = (function() {
             self.uploadFile = (function() {
                 var num_of_pending_uploads = 0;
                 var num_of_pending_uploads = 0;
                 var uploader = new qq.FileUploader({
                 var uploader = new qq.FileUploader({

+ 1 - 0
apps/filebrowser/src/filebrowser/urls.py

@@ -39,6 +39,7 @@ urlpatterns = patterns('filebrowser.views',
   url(r'upload/archive$', 'upload_archive', name='upload_archive'),
   url(r'upload/archive$', 'upload_archive', name='upload_archive'),
   url(r'rename', 'rename', name='rename'),
   url(r'rename', 'rename', name='rename'),
   url(r'mkdir', 'mkdir', name='mkdir'),
   url(r'mkdir', 'mkdir', name='mkdir'),
+  url(r'touch', 'touch', name='touch'),
   url(r'^move', 'move', name='move'),
   url(r'^move', 'move', name='move'),
   url(r'remove', 'remove', name='remove'),
   url(r'remove', 'remove', name='remove'),
   url(r'rmdir', 'rmdir', name='rmdir'),
   url(r'rmdir', 'rmdir', name='rmdir'),

+ 14 - 4
apps/filebrowser/src/filebrowser/views.py

@@ -28,7 +28,6 @@ import posixpath
 import shutil
 import shutil
 import stat as stat_module
 import stat as stat_module
 import os
 import os
-from types import MethodType
 
 
 try:
 try:
   import json
   import json
@@ -36,7 +35,7 @@ except ImportError:
   import simplejson as json
   import simplejson as json
 
 
 from django.contrib import messages
 from django.contrib import messages
-from django.core import urlresolvers, serializers
+from django.core import urlresolvers
 from django.template.defaultfilters import stringformat, filesizeformat
 from django.template.defaultfilters import stringformat, filesizeformat
 from django.http import Http404, HttpResponse, HttpResponseNotModified
 from django.http import Http404, HttpResponse, HttpResponseNotModified
 from django.views.static import was_modified_since
 from django.views.static import was_modified_since
@@ -55,7 +54,7 @@ from filebrowser.lib.archives import archive_factory
 from filebrowser.lib.rwx import filetype, rwx
 from filebrowser.lib.rwx import filetype, rwx
 from filebrowser.lib import xxd
 from filebrowser.lib import xxd
 from filebrowser.forms import RenameForm, UploadFileForm, UploadArchiveForm, MkDirForm,\
 from filebrowser.forms import RenameForm, UploadFileForm, UploadArchiveForm, MkDirForm,\
-    RmDirForm, RmTreeForm, RemoveForm, ChmodForm, ChownForm, EditorForm
+    RmDirForm, RmTreeForm, RemoveForm, ChmodForm, ChownForm, EditorForm, TouchForm
 from hadoop.fs.hadoopfs import Hdfs
 from hadoop.fs.hadoopfs import Hdfs
 from hadoop.fs.exceptions import WebHdfsException
 from hadoop.fs.exceptions import WebHdfsException
 
 
@@ -834,6 +833,17 @@ def mkdir(request):
     return generic_op(MkDirForm, request, smart_mkdir, ["path", "name"], "path")
     return generic_op(MkDirForm, request, smart_mkdir, ["path", "name"], "path")
 
 
 
 
+def touch(request):
+    def smart_touch(path, name):
+        # Make sure only the filename is specified.
+        # No absolute path specification allowed.
+        if posixpath.sep in name:
+            raise PopupException(_("Sorry, could not name file \"%s\": Slashes are not allowed in filenames." % name))
+        request.fs.create(os.path.join(path, name))
+
+    return generic_op(TouchForm, request, smart_touch, ["path", "name"], "path")
+
+
 def remove(request):
 def remove(request):
     return generic_op(RemoveForm, request, request.fs.remove, ["path"], None)
     return generic_op(RemoveForm, request, request.fs.remove, ["path"], None)
 
 
@@ -865,7 +875,7 @@ def chown(request):
 
 
     op = curry(request.fs.chown, recursive=request.POST.get('recursive', False))
     op = curry(request.fs.chown, recursive=request.POST.get('recursive', False))
     return generic_op(ChownForm, request, op, args, "path", template="chown.mako",
     return generic_op(ChownForm, request, op, args, "path", template="chown.mako",
-                      extra_params=dict(current_user=request.user, 
+                      extra_params=dict(current_user=request.user,
                       superuser=request.fs.superuser))
                       superuser=request.fs.superuser))
 
 
 
 

+ 37 - 5
apps/filebrowser/src/filebrowser/views_test.py

@@ -37,7 +37,6 @@ from desktop.lib.test_utils import grant_access
 from hadoop import pseudo_hdfs4
 from hadoop import pseudo_hdfs4
 
 
 from avro import schema, datafile, io
 from avro import schema, datafile, io
-from lib.archives import archive_factory
 from lib.rwx import expand_mode
 from lib.rwx import expand_mode
 
 
 
 
@@ -47,16 +46,15 @@ LOG = logging.getLogger(__name__)
 @attr('requires_hadoop')
 @attr('requires_hadoop')
 def test_mkdir_singledir():
 def test_mkdir_singledir():
   cluster = pseudo_hdfs4.shared_cluster()
   cluster = pseudo_hdfs4.shared_cluster()
+  cluster.fs.setuser('test')
+  c = make_logged_in_client()
 
 
   try:
   try:
-    c = make_logged_in_client(cluster.superuser)
-    cluster.fs.setuser(cluster.superuser)
-
     # We test that mkdir fails when a non-relative path is provided and a multi-level path is provided.
     # We test that mkdir fails when a non-relative path is provided and a multi-level path is provided.
     success_path = 'mkdir_singledir'
     success_path = 'mkdir_singledir'
     path_absolute = '/mkdir_singledir'
     path_absolute = '/mkdir_singledir'
     path_fail = 'fail/foo'
     path_fail = 'fail/foo'
-    prefix = '/test-filebrowser/'
+    prefix = '/tmp/test-filebrowser/'
     # Two of the following post requests should throw exceptions.
     # Two of the following post requests should throw exceptions.
     # See https://issues.cloudera.org/browse/HUE-793.
     # See https://issues.cloudera.org/browse/HUE-793.
     c.post('/filebrowser/mkdir', dict(path=prefix, name=path_fail))
     c.post('/filebrowser/mkdir', dict(path=prefix, name=path_fail))
@@ -76,6 +74,40 @@ def test_mkdir_singledir():
       pass      # Don't let cleanup errors mask earlier failures
       pass      # Don't let cleanup errors mask earlier failures
 
 
 
 
+@attr('requires_hadoop')
+def test_touch():
+  cluster = pseudo_hdfs4.shared_cluster()
+  cluster.fs.setuser('test')
+  c = make_logged_in_client()
+
+  try:
+    success_path = 'touch_file'
+    path_absolute = '/touch_file'
+    path_fail = 'touch_fail/file'
+    prefix = '/tmp/test-filebrowser-touch/'
+
+    cluster.fs.mkdir(prefix)
+
+    resp = c.post('/filebrowser/touch', dict(path=prefix, name=path_fail))
+    assert_equal(500, resp.status_code)
+    resp = c.post('/filebrowser/touch', dict(path=prefix, name=path_absolute))
+    assert_equal(500, resp.status_code)
+    resp = c.post('/filebrowser/touch', dict(path=prefix, name=success_path))
+    assert_equal(200, resp.status_code)
+
+    # Read the parent dir and make sure we created 'success_path' only.
+    response = c.get('/filebrowser/view' + prefix)
+    file_listing = response.context['files']
+    assert_equal(2, len(file_listing))
+    assert_equal(file_listing[1]['name'], success_path)
+
+  finally:
+    try:
+      cluster.fs.rmtree(prefix)
+    except:
+      pass
+
+
 @attr('requires_hadoop')
 @attr('requires_hadoop')
 def test_chmod():
 def test_chmod():
   cluster = pseudo_hdfs4.shared_cluster()
   cluster = pseudo_hdfs4.shared_cluster()