diff --git a/onboarding/migrations/0008_files_for_page_of_package.py b/onboarding/migrations/0008_files_for_page_of_package.py new file mode 100644 index 00000000..9f0d330e --- /dev/null +++ b/onboarding/migrations/0008_files_for_page_of_package.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.10 on 2021-08-25 11:34 + +from django.db import migrations, models +import django.db.models.deletion +import onboarding.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('onboarding', '0007_model_listing_sessions_for_users'), + ] + + operations = [ + migrations.CreateModel( + name='PageFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data_file', models.FileField(upload_to=onboarding.models.page_file_upload_to, validators=[onboarding.models.validate_file_extension])), + ('name', models.CharField(max_length=100)), + ('content_type', models.CharField(blank=True, max_length=200, null=True)), + ('size', models.DecimalField(decimal_places=2, max_digits=6)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='onboarding.company')), + ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='onboarding.page')), + ], + ), + ] diff --git a/onboarding/models.py b/onboarding/models.py index 95ef04d3..4318066f 100644 --- a/onboarding/models.py +++ b/onboarding/models.py @@ -6,6 +6,7 @@ from django.contrib.auth.base_user import BaseUserManager from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django.core.exceptions import ValidationError def upload_to(instance, filename): @@ -20,6 +21,39 @@ def upload_to(instance, filename): return f"users/{instance.pk}/{now:%Y%m%d%H%M%S}{milliseconds}{extension}" +def page_file_upload_to(instance, filename): + """ + :param instance: object of PageFile model + :param filename: name (with extension) of the file being uploaded + """ + company_id = instance.company + return f"company/{company_id}/{instance.page}/{filename}" + + +def validate_file_extension(value): + # valid_extensions = ['.doc', '.docx', '.pdf', '.jpg', '.xls', '.pptx'] + content_type = "" + try: + if content_type in value.file: + content_type = value.file.content_type + else: + content_type = value.content_type + + whitelist = ['application/msword', 'application/pdf', 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/rtf', 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'text/plain', + 'image/png', 'image/bmp', 'image/gif', 'image/jpe', 'image/jpeg', 'image/jpeg', 'image/svg+xml', 'image/x-icon'] + except (AttributeError, KeyError): + content_type = os.path.splitext(value.name)[1] + whitelist = ['.doc', '.docx', '.pdf', '.xls', '.pptx', '.jpg', '.png', '.gif', '.txt'] + + if not content_type in whitelist: + raise ValidationError('Unsupported file extension.') + + class Company(models.Model): company_logo = models.ImageField(upload_to=upload_to, null=True, blank=True) @@ -174,6 +208,37 @@ def __str__(self): return self.title +class PageFile(models.Model): + """ + Stores files for respective Page (many-to-one). + data_file - the file which is to be storred + name - keeps original name of the file + content_type - content type of data_file guessed by file extension by DJango + company - Company that owns this file + size - number of kilobytes of uploaded file + """ + data_file = models.FileField(upload_to=page_file_upload_to, validators=[validate_file_extension]) + page = models.ForeignKey(Page, on_delete=models.CASCADE) + name = models.CharField(max_length=100) + content_type = models.CharField(max_length=200, null=True, blank=True) + company = models.ForeignKey(Company, on_delete=models.CASCADE) + # description = models.TextField(max_length=700, help_text='Enter a description about file', null=True, blank=True) + size = models.DecimalField(max_digits=6, decimal_places=2) + # updated_on = models.DateTimeField(auto_now=True) + + # class Meta: + # constraints = [ + # models.UniqueConstraint(fields=['data_file', 'company'], name='unique file for each company') + # ] + + def delete(self): + self.data_file.storage.delete(self.data_file.name) + super().delete() + + def __str__(self): + return f"file: {self.name}" + + class Section(models.Model): """ Owner - company where the HR/user who created the section is employed diff --git a/onboarding/serializers.py b/onboarding/serializers.py index d67ba1ef..eeee1ab0 100644 --- a/onboarding/serializers.py +++ b/onboarding/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from django.contrib.auth import get_user_model -from onboarding.models import ContactRequestDetail, Package, Page, Section, User, PackagesUsers +from onboarding.models import ContactRequestDetail, Package, Page, PageFile, Section, User, PackagesUsers from onboarding.models import Answer, Company, CompanyQuestionAndAnswer from . import mock_password @@ -291,6 +291,44 @@ class Meta: } +# FILES for PAGE +class PageFileSerializer(serializers.ModelSerializer): + class Meta: + model = PageFile + fields = '__all__' + read_only_fields = ('name', 'content_type', 'company', 'size') + + def validate(self, validated_data): + try: + validated_data['content_type'] = validated_data['data_file'].content_type + except (KeyError, AttributeError): + pass + validated_data['name'] = validated_data['data_file'].name + validated_data['size'] = validated_data['data_file'].size / 1024.0 + return validated_data + + +#class PageFileMetaDataSerializer(serializers.ModelSerializer): +# class Meta: +# model = PageFile +# fields = ( +# 'id', +# 'page', +# 'name', +# 'company', +# 'description', +# 'size' +# ) +# read_only_fields = ('company', 'size') + + +class PageDataFileSerializer(serializers.ModelSerializer): + class Meta: + model = PageFile + fields = ('data_file',) + read_only_fields = ('data_file',) # [f.name for f in PageFile._meta.get_fields()] + + # PACKAGE with PAGEs class PackagePagesSerializer(serializers.ModelSerializer): page_set = PageSerializer(many=True) diff --git a/onboarding/src/Components/FormsEdit/FormDescription.js b/onboarding/src/Components/FormsEdit/FormDescription.js index c982a4fb..08857a35 100644 --- a/onboarding/src/Components/FormsEdit/FormDescription.js +++ b/onboarding/src/Components/FormsEdit/FormDescription.js @@ -1,16 +1,24 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; -import { savePageDetails } from "../hooks/FormsEdit"; +import { savePageDetails, getFilesForPage, removePageFile, addNewFiles } from "../hooks/FormsEdit"; import ModalWarning from "../ModalWarning"; import { isValidUrl } from "../utils"; import bookOpenedIcon from "../../static/icons/book-opened.svg"; +import trashIcon from "../../static/icons/trash.svg"; + const FormDescription = ({ formId, formData }) => { + const formFilesRef = useRef(); const location = useLocation(); const [formName, setFormName] = useState(""); const [link, setLink] = useState(""); const [description, setDescription] = useState(""); const [saveModal, setSaveModal ] = useState(<>); + const [formFiles, updateFormFiles] = useState([]); + const [filesToSend, appendFileToSend] = useState([]); + const [filesToSendTable, updateFileToSendTable] = useState([]); + const [uploadingFilesProgress, updateUploadingProgress] = useState(true);// array of progresses of files or true if there is no files and list can be updated; + useEffect(() => { if(location.state && !formName) { @@ -22,10 +30,90 @@ const FormDescription = ({ formId, formData }) => { formData.title && setFormName(formData.title); formData.description && setDescription(formData.description); formData.link && setLink(formData.link); - }; - }; + } + } }, [formData]); + useEffect(() => { + if(uploadingFilesProgress === true){ + updateFileToSendTable([]);// remove list of un-uploaded files; + let abortCont = getFilesForPage(formId, arrayOfFilesToTable, arrayOfFilesToTable); + return () => abortCont.abort(); + } else if(updateUploadingProgress && Object.keys(updateUploadingProgress).length > 0){ + + let filesToSendNewTable = [], percentage; + Object.keys(updateUploadingProgress).forEach( (fileName) => { + percentage = progressCopy[fileName].loaded / progressCopy[fileName].total * 100.0; + percentage = parseFloat(percentage).toFixed(2); + filesToSendNewTable.push(
{ fileName } | { percentage }%
); + }); + + updateFileToSendTable(filesToSendNewTable); + } + }, [formId, uploadingFilesProgress]); + + + const arrayOfFilesToTable = (files) => { + let tableFiles = []; + if(typeof files === 'string' || files instanceof String){ + tableFiles.push(
{ files }
); + updateFormFiles(tableFiles); + return; + } + + let row, button, link; + files.forEach((element) => { + button = + link = { element.name } ({ element.size } kB) + row =
{ link } | { button }
+ tableFiles.push(row); + }); + updateFormFiles(tableFiles); + }; + + const openedFilesToTable = (openedFiles) => { + let openedFilesDOM = [], rmButton; + openedFiles.forEach((file, index) => { + if(!file.name || !file.size) + return; + + rmButton = + openedFilesDOM.push(
{ file.name } ({ (file.size/1024.0).toFixed(2) } kB) | { rmButton }
); + }); + updateFileToSendTable(openedFilesDOM); + }; + + + const updateFiles = function(fileId){ + setSaveModal(<>); + + if(fileId > 0) + getFilesForPage(formId, arrayOfFilesToTable, arrayOfFilesToTable); + }; + + const openFile = function(e){ + e.preventDefault(); + if(typeof formFilesRef.current.files !== 'undefined' && formFilesRef.current.files.length > 0){ + let newFilesList = [...filesToSend, formFilesRef.current.files[0]]; + appendFileToSend(newFilesList); + openedFilesToTable(newFilesList); + } + }; + + const removeFromOpened = (e) => { + e.preventDefault(); + let fileName = e.target.value; + if(typeof fileName === 'undefined')// when is clicked, this happen; + fileName = e.target.parentNode.value; + + if(typeof fileName !== 'undefined'){ + let newFilesList = filesToSend.filter((file) => file.name !== fileName); + appendFileToSend(newFilesList); + openedFilesToTable(newFilesList); + } + }; + + const hideModal = () => { setSaveModal(<>); }; @@ -43,6 +131,43 @@ const FormDescription = ({ formId, formData }) => { ); }; + const popUpAskForDeleteFile = function(e){ + e.preventDefault(); + let fileId = e.target.value; + if(typeof fileId === 'undefined')// when is clicked, this happen; + fileId = e.target.parentNode.value; + + setSaveModal( + + ); + }; + + const popUpDeleteFileInformation = (message, fileId) => { + setSaveModal( + + ); + }; + + const handleRemoveFile = (fileId) => { + removePageFile(fileId, popUpDeleteFileInformation); + }; + + const handleSave = (e) => { e.preventDefault(); const isValid = isValidUrl(link); @@ -55,9 +180,62 @@ const FormDescription = ({ formId, formData }) => { description ); // pack as one argument; } else { - popUpSaveFormDetails("Błąd: Wprowadzono nieprawidłowy adres url") + popUpSaveFormDetails("Błąd: Wprowadzono nieprawidłowy adres url"); + } + + if(filesToSend.length > 0){ + let filesToSendCopy = []; + for(let i = 0; i < filesToSend.length; i++) + filesToSendCopy.push(filesToSend[i]); + + showProgress(); + addNewFiles(formId, filesToSendCopy, messageWhenOneFileUploaded, function(){}, showProgress); + appendFileToSend([]); + } + }; + + const showProgress = function(fileName, loaded, totalBytes){ + let progressCopy = {}; + if(fileName === undefined || fileName === null){ + let fName; + for(let i = 0; i < filesToSend.length; i++){ + fName = filesToSend[i].name; + progressCopy[fName] = {loaded: 0, total: 0};// filesToSend[i].size; + } + + updateUploadingProgress(progressCopy); + return; } + + + if(uploadingFilesProgress !== true) + progressCopy = JSON.parse(JSON.stringify(uploadingFilesProgress) ); + + progressCopy[fileName] = {loaded: loaded, total: totalBytes}; + updateUploadingProgress(progressCopy); + }; + + const messageWhenOneFileUploaded = function(fileName, response){ + let button = + let link = { response.name } ({ (response.size/1024.0).toFixed(2) } kB) + let row =
{ link } | { button }
+ + formFiles.push(row); + updateFormFiles(formFiles); + + let progressCopy = JSON.parse(JSON.stringify(uploadingFilesProgress) ); + + if(progressCopy.hasOwnProperty(fileName) ) + delete progressCopy[fileName]; + + if(Object.keys(progressCopy).length < 1) + updateUploadingProgress(true); + else + updateUploadingProgress(progressCopy); }; + + + return (
@@ -86,6 +264,7 @@ const FormDescription = ({ formId, formData }) => {
+ {/* (Limit na rozdział: 10MB - potrzebujesz więcej? < a >Dowiedz się jak...) */}
{ onChange={ (e) => setLink(e.target.value) } maxLength="200" /> - {/* TODO: funkcjonalność przycisku */} - + + + {/* Dołącz plik */}
+
+ { filesToSendTable } + { formFiles } +
+