Просмотр исходного кода

Merge pull request #227 from zauberzeug/q-uploader

Using Quasar Uploader for ui.upload
Falko Schindler 2 лет назад
Родитель
Сommit
41465d80f1
6 измененных файлов с 763 добавлено и 99 удалено
  1. 29 27
      nicegui/elements/upload.py
  2. 0 40
      nicegui/elements/upload.vue
  3. 3 10
      nicegui/events.py
  4. 671 22
      poetry.lock
  5. 1 0
      pyproject.toml
  6. 59 0
      tests/test_upload.py

+ 29 - 27
nicegui/elements/upload.py

@@ -1,10 +1,10 @@
-from typing import Callable, Dict, Optional
+from typing import Callable, Optional
 
 
-from ..dependencies import register_component
-from ..element import Element
-from ..events import UploadEventArguments, UploadFile, handle_event
+from fastapi import Request, Response
 
 
-register_component('upload', __file__, 'upload.vue')
+from ..element import Element
+from ..events import UploadEventArguments, handle_event
+from ..nicegui import app
 
 
 
 
 class Upload(Element):
 class Upload(Element):
@@ -12,34 +12,36 @@ class Upload(Element):
     def __init__(self, *,
     def __init__(self, *,
                  multiple: bool = False,
                  multiple: bool = False,
                  on_upload: Optional[Callable] = None,
                  on_upload: Optional[Callable] = None,
-                 file_picker_label: str = '',
-                 upload_button_icon: str = 'file_upload') -> None:
+                 label: str = '',
+                 auto_upload: bool = False,
+                 ) -> None:
         """File Upload
         """File Upload
 
 
+        Based on Quasar's [QUploader](https://quasar.dev/vue-components/uploader) component.
+
         :param multiple: allow uploading multiple files at once (default: `False`)
         :param multiple: allow uploading multiple files at once (default: `False`)
-        :param on_upload: callback to execute when a file is uploaded (list of bytearrays)
-        :param file_picker_label: label for the file picker element
-        :param upload_button_icon: icon for the upload button
+        :param on_upload: callback to execute for each uploaded file (type: nicegui.events.UploadEventArguments)
+        :param label: label for the uploader (default: `''`)
+        :param auto_upload: automatically upload files when they are selected (default: `False`)
         """
         """
-        super().__init__('upload')
-        self.classes('row items-center gap-2')
+        super().__init__('q-uploader')
         self._props['multiple'] = multiple
         self._props['multiple'] = multiple
-        self._props['file_picker_label'] = file_picker_label
-        self._props['upload_button_icon'] = upload_button_icon
-
-        def upload(msg: Dict) -> None:
-            files = [
-                UploadFile(
-                    content=file['content'],
-                    name=file['name'],
-                    lastModified=file['lastModified'],
-                    size=file['size'],
-                    type=file['type'],
+        self._props['label'] = label
+        self._props['auto-upload'] = auto_upload
+        self._props['url'] = f'/_nicegui/client/{self.client.id}/upload/{self.id}'
+
+        @app.post(f'/_nicegui/client/{self.client.id}/upload/{self.id}')
+        async def upload_route(request: Request) -> Response:
+            for data in (await request.form()).values():
+                args = UploadEventArguments(
+                    sender=self,
+                    client=self.client,
+                    content=data.file,
+                    name=data.filename,
+                    type=data.content_type,
                 )
                 )
-                for file in msg['args']
-            ]
-            handle_event(on_upload, UploadEventArguments(sender=self, client=self.client, files=files))
-        self.on('upload', upload)
+                handle_event(on_upload, args)
+            return {'upload': 'success'}
 
 
     def reset(self) -> None:
     def reset(self) -> None:
         self.run_method('reset')
         self.run_method('reset')

+ 0 - 40
nicegui/elements/upload.vue

@@ -1,40 +0,0 @@
-<template>
-  <div>
-    <q-file :label="file_picker_label" v-model="file" :multiple="multiple" />
-    <q-btn :icon="upload_button_icon" @click="upload" size="sm" round color="primary" />
-  </div>
-</template>
-
-<script>
-export default {
-  data() {
-    return {
-      file: undefined,
-    };
-  },
-  methods: {
-    upload() {
-      if (!this.file) return;
-      const files = this.multiple ? this.file : [this.file];
-      const args = files.map((file) => ({
-        content: file,
-        name: file.name,
-        lastModified: file.lastModified / 1000,
-        size: file.size,
-        type: file.type,
-      }));
-      this.$emit("upload", args);
-    },
-    reset() {
-      this.file = undefined;
-    },
-  },
-  props: {
-    multiple: Boolean,
-    file_picker_label: String,
-    upload_button_icon: String,
-  },
-};
-</script>
-
-<style scoped></style>

+ 3 - 10
nicegui/events.py

@@ -1,7 +1,7 @@
 import traceback
 import traceback
 from dataclasses import dataclass
 from dataclasses import dataclass
 from inspect import signature
 from inspect import signature
-from typing import TYPE_CHECKING, Any, Callable, List, Optional
+from typing import TYPE_CHECKING, Any, BinaryIO, Callable, List, Optional
 
 
 from . import globals
 from . import globals
 from .async_updater import AsyncUpdater
 from .async_updater import AsyncUpdater
@@ -65,19 +65,12 @@ class JoystickEventArguments(EventArguments):
 
 
 
 
 @dataclass
 @dataclass
-class UploadFile:
-    content: bytes
+class UploadEventArguments(EventArguments):
+    content: BinaryIO
     name: str
     name: str
-    lastModified: float
-    size: int
     type: str
     type: str
 
 
 
 
-@dataclass
-class UploadEventArguments(EventArguments):
-    files: List[UploadFile]
-
-
 @dataclass
 @dataclass
 class ValueChangeEventArguments(EventArguments):
 class ValueChangeEventArguments(EventArguments):
     value: Any
     value: Any

Разница между файлами не показана из-за своего большого размера
+ 671 - 22
poetry.lock


+ 1 - 0
pyproject.toml

@@ -24,6 +24,7 @@ fastapi-socketio = "^0.0.9"
 vbuild = "^0.8.1"
 vbuild = "^0.8.1"
 watchfiles = "^0.18.1"
 watchfiles = "^0.18.1"
 jinja2 = "^3.1.2"
 jinja2 = "^3.1.2"
+python-multipart = "^0.0.5"
 
 
 [tool.poetry.group.dev.dependencies]
 [tool.poetry.group.dev.dependencies]
 icecream = "^2.1.0"
 icecream = "^2.1.0"

+ 59 - 0
tests/test_upload.py

@@ -0,0 +1,59 @@
+from pathlib import Path
+from typing import List
+
+from selenium.webdriver.common.by import By
+
+from nicegui import events, ui
+
+from .screen import Screen
+
+test_path1 = Path('tests/test_upload.py').resolve()
+test_path2 = Path('tests/test_scene.py').resolve()
+
+
+def test_uploading_text_file(screen: Screen):
+    results: List[events.UploadEventArguments] = []
+    ui.upload(on_upload=results.append, label='Test Title')
+
+    screen.open('/')
+    screen.should_contain('Test Title')
+    screen.selenium.find_element(By.CLASS_NAME, 'q-uploader__input').send_keys(str(test_path1))
+    screen.wait(0.1)
+    screen.selenium.find_elements(By.CLASS_NAME, 'q-btn')[1].click()
+    screen.wait(0.1)
+    assert len(results) == 1
+    assert results[0].name == test_path1.name
+    assert results[0].type in ['text/x-python', 'text/x-python-script']
+    assert results[0].content.read() == test_path1.read_bytes()
+
+
+def test_two_upload_elements(screen: Screen):
+    results: List[events.UploadEventArguments] = []
+    ui.upload(on_upload=results.append, auto_upload=True, label='Test Title 1')
+    ui.upload(on_upload=results.append, auto_upload=True, label='Test Title 2')
+
+    screen.open('/')
+    screen.should_contain('Test Title 1')
+    screen.should_contain('Test Title 2')
+    screen.selenium.find_element(By.CLASS_NAME, 'q-uploader__input').send_keys(str(test_path1))
+    screen.selenium.find_elements(By.CLASS_NAME, 'q-uploader__input')[1].send_keys(str(test_path2))
+    screen.wait(0.1)
+    assert len(results) == 2
+    assert results[0].name == test_path1.name
+    assert results[1].name == test_path2.name
+
+
+def test_uploading_from_two_tabs(screen: Screen):
+    @ui.page('/')
+    def page():
+        ui.upload(on_upload=lambda e: ui.label(f'uploaded {e.name}'), auto_upload=True)
+
+    screen.open('/')
+    screen.switch_to(1)
+    screen.open('/')
+    screen.should_not_contain(test_path1.name)
+    screen.selenium.find_element(By.CLASS_NAME, 'q-uploader__input').send_keys(str(test_path1))
+    screen.wait(0.3)
+    screen.should_contain(f'uploaded {test_path1.name}')
+    screen.switch_to(0)
+    screen.should_not_contain(f'uploaded {test_path1.name}')

Некоторые файлы не были показаны из-за большого количества измененных файлов