소스 검색

implemented file upload and listing

Dimitri Korsch 3 년 전
부모
커밋
d0d02b89a7

+ 2 - 0
backend/.gitignore

@@ -1,2 +1,4 @@
 secret.txt
 mysql.cnf
+
+uploads

+ 1 - 0
backend/pycs_api/admin.py

@@ -2,5 +2,6 @@ from django.contrib import admin
 from pycs_api import models
 
 admin.site.register(models.Model)
+admin.site.register(models.File)
 admin.site.register(models.Project)
 admin.site.register(models.LabelProvider)

+ 26 - 0
backend/pycs_api/models/file.py

@@ -2,6 +2,8 @@ import uuid
 import enum
 
 from django.db import models
+from django.core.files.uploadedfile import UploadedFile
+from pathlib import Path
 from pycs_api.models import base
 from pycs_api.models.project import Project
 
@@ -30,6 +32,8 @@ class File(base.BaseModel):
     project = models.ForeignKey(
         Project,
         on_delete=models.CASCADE,
+        related_name="files",
+        related_query_name="file",
     )
 
     extension = models.CharField(
@@ -43,6 +47,28 @@ class File(base.BaseModel):
 
     path = models.ImageField(upload_to=project_directory)
 
+    serializer_fields = base.BaseModel.serializer_fields + [
+        "project",
+        "created",
+        "path",
+        "uuid",
+    ]
+
+    @classmethod
+    def create(cls, uploaded_file: UploadedFile, project: Project):
+
+        filename = Path(uploaded_file.name)
+        file = cls.objects.create(
+            project=project,
+            size=uploaded_file.size,
+            name=filename.stem,
+            extension=filename.suffix.lower(),
+            path=uploaded_file
+        )
+
+        return file
+
+
     class Meta:
         unique_together = [
             "project",

+ 1 - 0
backend/pycs_api/models/project.py

@@ -63,6 +63,7 @@ class Project(base.BaseModel):
         "root_folder",
     ]
 
+
 @receiver(models.signals.pre_save, sender=Project)
 def project_pre_save(sender, instance, **kwargs):
     is_external = instance.data_folder != ''

+ 7 - 0
backend/pycs_api/serializers.py

@@ -23,3 +23,10 @@ class ProjectSerializer(BaseSerializer):
         model = models.Project
         fields = models.Project.serializer_fields
         read_only_fields = models.Project.read_only_fields
+
+class FileSerializer(BaseSerializer):
+
+    class Meta:
+        model = models.File
+        fields = models.File.serializer_fields
+        read_only_fields = models.File.read_only_fields

+ 29 - 0
backend/pycs_api/views/project.py

@@ -2,10 +2,13 @@ from django.shortcuts import get_object_or_404
 
 from rest_framework import status
 from rest_framework.decorators import permission_classes
+from rest_framework.decorators import action
 from rest_framework.response import Response
 
 from pycs_api.models import Project
+from pycs_api.models import File
 from pycs_api.serializers import ProjectSerializer
+from pycs_api.serializers import FileSerializer
 from pycs_api.views.base import BaseViewSet
 
 
@@ -18,3 +21,29 @@ class ProjectViewSet(BaseViewSet):
 
     def perform_create(self, serializer):
         serializer.save(user=self.request.user)
+
+    @action(detail=True, methods=['post'])
+    def file(self, request, pk=None):
+        if "file" not in request.FILES:
+            return Response({"status": "File missing"},
+                status=status.HTTP_400_BAD_REQUEST)
+        project = self.get_object()
+
+        try:
+            file = File.create(request.FILES['file'], project)
+
+        except Exception as e:
+            return Response({"status": str(e)},
+                status=status.HTTP_400_BAD_REQUEST)
+
+        else:
+            return Response({'status': 'File uploaded'})
+
+    @action(detail=True)
+    def files(self, request, pk=None):
+        project = self.get_object()
+
+        files = project.files
+
+        serializer = FileSerializer(files, many=True)
+        return Response(serializer.data)

+ 6 - 1
frontend/package-lock.json

@@ -1,5 +1,5 @@
 {
-  "name": "frontend",
+  "name": "PyCS2",
   "version": "0.1.0",
   "lockfileVersion": 1,
   "requires": true,
@@ -8942,6 +8942,11 @@
       "dev": true,
       "optional": true
     },
+    "pretty-bytes": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+      "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
+    },
     "pretty-error": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz",

+ 1 - 0
frontend/package.json

@@ -11,6 +11,7 @@
     "axios": "^0.24.0",
     "core-js": "^3.6.5",
     "idle-vue": "^2.0.5",
+    "pretty-bytes": "^5.6.0",
     "vue": "^2.6.11",
     "vue-router": "^3.5.3",
     "vuelidate": "^0.7.6",

+ 132 - 0
frontend/src/components/Upload.vue

@@ -0,0 +1,132 @@
+<template>
+    <v-card
+      @drop.prevent="onDrop($event)"
+      @dragover.prevent="dragover = true"
+      @dragenter.prevent="dragover = true"
+      @dragleave.prevent="dragover = false"
+      :class="{ 'grey lighten-2': dragover }"
+    >
+      <v-card-text>
+        <v-row class="d-flex flex-column" dense align="center" justify="center">
+          <v-icon :class="[dragover ? 'mt-2, mb-6' : 'mt-5']" size="60">
+            mdi-cloud-upload
+          </v-icon>
+          <p>
+            Drop your file(s) here, or click to select them.
+          </p>
+        </v-row>
+        <v-virtual-scroll
+          v-if="uploadedFiles.length > 0"
+          :items="uploadedFiles"
+          height="150"
+          item-height="50"
+        >
+          <template v-slot:default="{ item }">
+            <v-list-item :key="item.name">
+              <v-list-item-content>
+                <v-list-item-title>
+                  <v-row>
+                    <v-col cols=6>{{ item.name }}</v-col>
+                    <v-col cols=6>
+                      <span class="ml-3 text--secondary">
+                        {{ prettyBytes(item.size) }}
+                      </span>
+                    </v-col>
+                  </v-row>
+
+                </v-list-item-title>
+              </v-list-item-content>
+
+              <v-list-item-action>
+                <v-btn @click.stop="removeFile(item.name)" icon>
+                  <v-icon> mdi-close-circle </v-icon>
+                </v-btn>
+              </v-list-item-action>
+            </v-list-item>
+
+            <v-divider></v-divider>
+          </template>
+        </v-virtual-scroll>
+      </v-card-text>
+      <v-card-actions>
+        <v-spacer></v-spacer>
+        <v-btn plain @click.stop="submit">
+          Upload
+          <v-icon id="upload-button">mdi-upload</v-icon>
+        </v-btn>
+      </v-card-actions>
+    </v-card>
+</template>
+
+<script>
+const prettyBytes = require('pretty-bytes');
+
+export default {
+  name: "Upload",
+  props: {
+    multiple: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      dragover: false,
+      uploadedFiles: []
+    };
+  },
+
+  methods: {
+    prettyBytes,
+
+    removeFile(fileName) {
+      // Find the index of the
+      const index = this.uploadedFiles.findIndex(
+        file => file.name === fileName
+      );
+      // If file is in uploaded files remove it
+      if (index > -1) this.uploadedFiles.splice(index, 1);
+    },
+
+    onDrop(e) {
+      this.dragover = false;
+      // If there are already uploaded files remove them
+      // if (this.uploadedFiles.length > 0)
+      //   this.uploadedFiles = [];
+
+      // If user has uploaded multiple files but the component is not multiple throw error
+      if (!this.multiple && e.dataTransfer.files.length > 1) {
+        let message = "Only one file can be uploaded at a time";
+        console.log(message)
+        // this.$store.dispatch("addNotification", {
+        //   message ,
+        //   colour: "error"
+        // });
+      }
+      // Add each file to the array of uploaded files
+      else{
+        for (const file of e.dataTransfer.files) {
+          this.uploadedFiles.push(file)
+        }
+      }
+    },
+
+
+    submit() {
+      // If there aren't any files to be uploaded throw error
+      if (!this.uploadedFiles.length > 0) {
+        let message = "There are no files to upload";
+        console.log(message)
+        // this.$store.dispatch("addNotification", {
+        //   message,
+        //   colour: "error"
+        // });
+
+      } else {
+        // Send uploaded files to parent component
+        this.$emit("upload", this.uploadedFiles);
+      }
+    }
+  }
+};
+</script>

+ 32 - 0
frontend/src/services/data.service.js

@@ -56,6 +56,38 @@ class DataService {
         return response.data;
       });
   }
+
+  getFiles(projectId) {
+    return api.get(`/project/${projectId}/files`)
+      .then((response) => {
+        return response.data;
+      })
+      .catch((error) =>{
+        if (error.response.status == 404)
+          return null;
+      });
+  }
+
+  uploadFile(projectId, file){
+    let data = new FormData();
+
+    data.append('file', file);
+
+    let config = {
+      headers: {'content-type': 'multipart/form-data'},
+      onUploadProgress: function (progressEvent) {
+        console.log(progressEvent)
+      },
+    }
+
+    return api.post(`project/${projectId}/file/`, data, config)
+      .then((response) => {
+        console.log("OK", response.status);
+      })
+      .catch((error) =>{
+        console.log("ERROR:", error)
+      })
+  }
 }
 
 

+ 80 - 0
frontend/src/views/project/Data.vue

@@ -10,5 +10,85 @@
         </v-btn>
       </v-col>
     </v-row>
+
+    <v-card>
+      <v-card-title>Data Upload</v-card-title>
+      <v-card-text>
+        <Upload
+          :multiple="true"
+          @upload="upload"
+        />
+
+      </v-card-text>
+      <v-divider/>
+      <v-card-title>
+        Data List
+      </v-card-title>
+      <v-card-text>
+        <v-row dense>
+          <v-col
+            v-for="file in files"
+            :key="file.uuid"
+            :cols=4
+          >
+
+            <v-card>
+              <v-card-title>File #{{file.id}}</v-card-title>
+              <v-card-text>
+                {{file}}
+              </v-card-text>
+            </v-card>
+          </v-col>
+        </v-row>
+
+
+      </v-card-text>
+    </v-card>
   </v-container>
 </template>
+
+<script>
+  import Upload from "@/components/Upload"
+  import DataService from '@/services/data.service';
+
+
+  export default {
+    components: {
+      Upload
+    },
+
+    computed: {
+
+      // ...mapState(['data']),
+      projectId() {
+        return this.$route.params.id;
+      },
+    },
+
+    data() {
+      return{
+        files: []
+      }
+    },
+
+    created() {
+      this.getFiles();
+    },
+
+    methods: {
+      upload(files){
+        for (const file of files)
+          DataService.uploadFile(this.projectId, file)
+      },
+
+      getFiles(){
+        DataService.getFiles(this.projectId)
+          .then((files) => {
+            this.files = files;
+          })
+      },
+    }
+  };
+</script>
+
+<style scoped></style>