浏览代码

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 年之前
父节点
当前提交
ae0e8ba

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

@@ -33,7 +33,7 @@ class PathField(CharField):
     kwargs.setdefault('required', True)
     kwargs.setdefault('min_length', 1)
     forms.CharField.__init__(self, label=label, help_text=help_text, **kwargs)
-  
+
   def 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"))
   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):
   op = "chown"
   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>
 		    </tr>
 	    </table>
-	    
+	
     </body>
 </html>

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

@@ -27,10 +27,12 @@ ${commonheader(_('File Browser'), 'filebrowser', user)}
 
 <div class="container-fluid">
     <h1>${_('File Browser')}</h1>
+
     <%actionbar:render>
         <%def name="search()">
             <input type="text" class="input-xlarge search-query" placeholder="${_('Search for file name')}" data-bind="value: searchQuery">
         </%def>
+
         <%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="${_('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="${_('Delete')}" data-bind="click: deleteSelected, enable: selectedFiles().length == 1"><i class="icon-trash"></i> ${_('Delete')}</button>
         </%def>
+
         <%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;">
-                <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">
-                  <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>
             </div>
-            <a href="#" class="btn create-directory-link" title="${_('New directory')}"><i class="icon-folder-close"></i> ${_('New directory')}</a>
         </%def>
     </%actionbar:render>
+
     % if breadcrumbs:
         ${fb_components.breadcrumbs(path, breadcrumbs, True)}
     %endif
+
     <div class="scrollable">
         ${dir.list_table_browser(files, path_enc, current_request_path, cwd_set)}
     </div>

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

@@ -220,6 +220,27 @@ from django.utils.translation import ugettext as _
         </form>
     </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">
         // ajax modal windows
         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");
             });
 
-            //create directory handlers
             $(".create-directory-link").click(function(){
                 $("#createDirectoryModal").modal({
                     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(){
                 $("#createDirectoryModal").modal("hide");
             });
 
+            $("#cancelCreateFileBtn").click(function(){
+                $("#createFileModal").modal("hide");
+            });
+
             $("#createDirectoryForm").submit(function(){
                 if ($.trim($("#newDirectoryNameInput").val())==""){
                     $("#directoryNameRequiredAlert").show();
@@ -567,7 +598,7 @@ from django.utils.translation import ugettext as _
                 self.currentPath(currentDirPath);
 
                 $('.uploader').trigger('fb:updatePath', {dest: self.currentPath()});
- 
+
                 self.isLoading(false);
                 $(".scrollable").jHueTableScroller();
             };
@@ -669,6 +700,11 @@ from django.utils.translation import ugettext as _
                 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() {
                 var num_of_pending_uploads = 0;
                 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'rename', 'rename', name='rename'),
   url(r'mkdir', 'mkdir', name='mkdir'),
+  url(r'touch', 'touch', name='touch'),
   url(r'^move', 'move', name='move'),
   url(r'remove', 'remove', name='remove'),
   url(r'rmdir', 'rmdir', name='rmdir'),

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

@@ -28,7 +28,6 @@ import posixpath
 import shutil
 import stat as stat_module
 import os
-from types import MethodType
 
 try:
   import json
@@ -36,7 +35,7 @@ except ImportError:
   import simplejson as json
 
 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.http import Http404, HttpResponse, HttpResponseNotModified
 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 import xxd
 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.exceptions import WebHdfsException
 
@@ -834,6 +833,17 @@ def mkdir(request):
     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):
     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))
     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))
 
 

+ 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 avro import schema, datafile, io
-from lib.archives import archive_factory
 from lib.rwx import expand_mode
 
 
@@ -47,16 +46,15 @@ LOG = logging.getLogger(__name__)
 @attr('requires_hadoop')
 def test_mkdir_singledir():
   cluster = pseudo_hdfs4.shared_cluster()
+  cluster.fs.setuser('test')
+  c = make_logged_in_client()
 
   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.
     success_path = 'mkdir_singledir'
     path_absolute = '/mkdir_singledir'
     path_fail = 'fail/foo'
-    prefix = '/test-filebrowser/'
+    prefix = '/tmp/test-filebrowser/'
     # Two of the following post requests should throw exceptions.
     # See https://issues.cloudera.org/browse/HUE-793.
     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
 
 
+@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')
 def test_chmod():
   cluster = pseudo_hdfs4.shared_cluster()