Refined media handling in tiptap, handle upload action in backend

This commit is contained in:
Barunes Padhy 2024-06-02 12:52:33 +03:00
parent d450ac3cb4
commit 50808b27b0
19 changed files with 226 additions and 164 deletions

View File

@ -96,5 +96,5 @@ class MediaSerializer(serializers.Serializer):
media = serializers.ListField(
child=serializers.FileField(max_length=100000, allow_empty_file=False, use_url=False)
)
resource_type = serializers.CharField(allow_blank=False)
resource_id = serializers.CharField(allow_blank=False)
resource_type = serializers.CharField(max_length=255, allow_blank=False)
resource_id = serializers.CharField(max_length=255, allow_blank=False)

View File

@ -1,17 +1,10 @@
#######################Django related imports####################
import os
import subprocess
import ast
import shutil
from django.shortcuts import render
from django.contrib.auth.models import User
from django.http import HttpResponseRedirect
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.generics import GenericAPIView
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework import generics, permissions, views, serializers, status
from django.core.files.storage import default_storage
from rest_framework import generics, status
import random
#################################################################
#API related imports
from .models import (
@ -110,24 +103,22 @@ class BlogDeleteAPIView(generics.DestroyAPIView):
lookup_field = 'blog_id'
####################################################################
'''
class MediaView(APIView):
class MediaUpload(APIView):
parser_classes = (MultiPartParser, FormParser)
def post(self, request, *args, **kwargs):
file_serializer = FileSerializer(data=request.data)
file_serializer = MediaSerializer(data=request.data)
if file_serializer.is_valid():
files = dict((f, f) for f in request.FILES.getlist('file'))
nodeName = file_serializer.validated_data['nodeName']
preferredFormat = file_serializer.validated_data['preferredFormat']
for f in files.values():
fileHandlerObject = FileHandler(f, preferredFormat, nodeName)
fileProcessed = fileHandlerObject.handleUploadedFile()
if not fileProcessed[0]:
return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(file_serializer.data, status=status.HTTP_201_CREATED)
files = request.FILES.getlist('media')
resource_type = file_serializer.validated_data['resource_type']
resource_id = file_serializer.validated_data['resource_id']
file_path_base = f'static/rangolio_data'
for f in files:
file_unique_slug = ''.join(random.choices('ABCDEabcde1234', k=5))
file_path = f"{file_path_base}/{resource_type}/{resource_id}/media/{file_unique_slug+resource_id+f.name}"
default_storage.save(file_path, f)
return Response(file_serializer.data, status=status.HTTP_201_CREATED)
else:
@ -135,6 +126,7 @@ class MediaView(APIView):
'''
class ETLFunctions(GenericAPIView):
serializer_class = ETLData

View File

@ -32,6 +32,7 @@ from apimanager.views import (
BlogRetrieveAPIView,
BlogDeleteAPIView,
BlogsByCategoryAPIView,
MediaUpload
)
urlpatterns = [
@ -49,4 +50,5 @@ urlpatterns = [
path('data/blog/<slug:blog_id>/', BlogRetrieveAPIView.as_view(), name='blog-retrieve-view'),
path('data/blog/update/<slug:blog_id>/', BlogUpdateAPIView.as_view(), name='blog-update-view'),
path('data/blog/delete/<slug:blog_id>/', BlogDeleteAPIView.as_view(), name='blog-delete-view'),
path('data/upload/', MediaUpload.as_view(), name='media-upload'),
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

View File

@ -1,9 +0,0 @@
{
"id": "72e4d550-a19b-4b62-bf5a-13f98813d31a",
"name": "Blog 1",
"description": "A subtitle for Blog 1",
"tagLine": "Read blog",
"coverImage": "blogs/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579",
"contentBody": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec non ipsum in nunc pretium gravida id ut lectus. Duis ligula nisl, egestas a tortor nec, scelerisque hendrerit urna. Nullam gravida, ante id aliquet ultrices, justo metus aliquet augue, vestibulum posuere massa lacus at est. Cras vitae dolor euismod, volutpat quam eu, cursus enim. Maecenas in magna ut augue ultrices laoreet. Maecenas sapien sem, mollis sed ipsum nec, viverra vehicula quam. Sed et pulvinar justo. Quisque et vestibulum dui. Aliquam laoreet tempus neque, et eleifend nulla vestibulum eget. Cras tempus justo at nunc facilisis auctor. Duis facilisis tortor eu risus aliquam dapibus nec sit amet est. Maecenas ante lectus, sagittis eu facilisis sit amet, convallis eu ex. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean imperdiet vulputate ipsum sed scelerisque. Donec sit amet rutrum est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam gravida augue sem, quis aliquet justo varius non. Nam tortor nulla, bibendum sit amet finibus sed, ultricies non tellus. Aliquam condimentum risus ut felis porta iaculis. Nullam gravida mauris lacinia finibus hendrerit. Sed nec consectetur erat, sed tincidunt velit. Donec sit amet nulla at sem blandit imperdiet ut eu nisl. Sed condimentum, lectus quis sodales commodo, arcu turpis sodales ex, ac placerat mi turpis sit amet mi. Nullam lorem velit, porta eu quam eu, hendrerit egestas nibh. Sed non pellentesque arcu. Nam felis lectus, scelerisque in semper a, faucibus sit amet nibh. Cras est ligula, pretium id maximus nec, hendrerit eu ante. Maecenas tincidunt est ante, sed vestibulum mauris dignissim nec. Phasellus varius varius leo in pharetra. Aenean vestibulum id dui cursus lobortis. Mauris vel orci massa. Nullam vitae lorem mattis, lobortis dui in, pharetra velit. Aenean non urna ac felis volutpat consequat sit amet sed nisi. Quisque rutrum nisi ac erat ultricies tempor. Etiam nec pellentesque metus. Nulla tempus mi a ex rutrum, ut luctus tellus porta. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus et quam sit amet arcu mattis commodo quis non risus. Sed luctus non dui ullamcorper consectetur. Duis eros magna, tempus ut aliquam sit amet, tempor vel nibh. Vestibulum hendrerit odio convallis elit pretium dictum. Proin ligula dolor, finibus eget lacus sed, facilisis fringilla lorem. Quisque quis lacus sit amet massa blandit fringilla vel vitae tortor. Vivamus dictum nibh vel justo ullamcorper faucibus. Nullam vitae augue pretium erat semper rhoncus. Etiam tempus, arcu feugiat hendrerit pretium, nisi justo sollicitudin velit, sagittis elementum ante metus sed ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam quis ligula euismod, venenatis arcu vitae, condimentum arcu. Donec vitae nisl aliquam, rutrum arcu vel, sollicitudin justo. Vestibulum nec sagittis massa. Etiam maximus, erat vitae dapibus vulputate, velit lectus imperdiet est, sed lobortis ante erat lobortis arcu. Maecenas mollis nunc ut nisi tristique tempus. Nulla tempor est non dui scelerisque, eget semper augue consequat."
}

View File

@ -1,9 +0,0 @@
{
"id": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e",
"name": "Blog 2",
"description": "A subtitle for Blog 2",
"tagLine": "Read blog",
"coverImage": "blogs/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579",
"contentBody": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec non ipsum in nunc pretium gravida id ut lectus. Duis ligula nisl, egestas a tortor nec, scelerisque hendrerit urna. Nullam gravida, ante id aliquet ultrices, justo metus aliquet augue, vestibulum posuere massa lacus at est. Cras vitae dolor euismod, volutpat quam eu, cursus enim. Maecenas in magna ut augue ultrices laoreet. Maecenas sapien sem, mollis sed ipsum nec, viverra vehicula quam. Sed et pulvinar justo. Quisque et vestibulum dui. Aliquam laoreet tempus neque, et eleifend nulla vestibulum eget. Cras tempus justo at nunc facilisis auctor. Duis facilisis tortor eu risus aliquam dapibus nec sit amet est. Maecenas ante lectus, sagittis eu facilisis sit amet, convallis eu ex. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean imperdiet vulputate ipsum sed scelerisque. Donec sit amet rutrum est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam gravida augue sem, quis aliquet justo varius non. Nam tortor nulla, bibendum sit amet finibus sed, ultricies non tellus. Aliquam condimentum risus ut felis porta iaculis. Nullam gravida mauris lacinia finibus hendrerit. Sed nec consectetur erat, sed tincidunt velit. Donec sit amet nulla at sem blandit imperdiet ut eu nisl. Sed condimentum, lectus quis sodales commodo, arcu turpis sodales ex, ac placerat mi turpis sit amet mi. Nullam lorem velit, porta eu quam eu, hendrerit egestas nibh. Sed non pellentesque arcu. Nam felis lectus, scelerisque in semper a, faucibus sit amet nibh. Cras est ligula, pretium id maximus nec, hendrerit eu ante. Maecenas tincidunt est ante, sed vestibulum mauris dignissim nec. Phasellus varius varius leo in pharetra. Aenean vestibulum id dui cursus lobortis. Mauris vel orci massa. Nullam vitae lorem mattis, lobortis dui in, pharetra velit. Aenean non urna ac felis volutpat consequat sit amet sed nisi. Quisque rutrum nisi ac erat ultricies tempor. Etiam nec pellentesque metus. Nulla tempus mi a ex rutrum, ut luctus tellus porta. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus et quam sit amet arcu mattis commodo quis non risus. Sed luctus non dui ullamcorper consectetur. Duis eros magna, tempus ut aliquam sit amet, tempor vel nibh. Vestibulum hendrerit odio convallis elit pretium dictum. Proin ligula dolor, finibus eget lacus sed, facilisis fringilla lorem. Quisque quis lacus sit amet massa blandit fringilla vel vitae tortor. Vivamus dictum nibh vel justo ullamcorper faucibus. Nullam vitae augue pretium erat semper rhoncus. Etiam tempus, arcu feugiat hendrerit pretium, nisi justo sollicitudin velit, sagittis elementum ante metus sed ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam quis ligula euismod, venenatis arcu vitae, condimentum arcu. Donec vitae nisl aliquam, rutrum arcu vel, sollicitudin justo. Vestibulum nec sagittis massa. Etiam maximus, erat vitae dapibus vulputate, velit lectus imperdiet est, sed lobortis ante erat lobortis arcu. Maecenas mollis nunc ut nisi tristique tempus. Nulla tempor est non dui scelerisque, eget semper augue consequat."
}

View File

@ -1,18 +0,0 @@
[
{
"id": "72e4d550-a19b-4b62-bf5a-13f98813d31a",
"name": "Blog 1",
"description": "A subtitle for Blog 1",
"coverImage": "blogs/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
"tagLine": "Read more",
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579"
},
{
"id": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e",
"name": "Blog 2",
"description": "A subtitle for Blog 2",
"coverImage": "blogs/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
"tagLine": "Read more",
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579"
}
]

View File

@ -1,8 +0,0 @@
{
"id": "520b7982-069e-4a48-9ef3-64507d86a579",
"name": "Technology",
"coverImage": "category/520b7982-069e-4a48-9ef3-64507d86a579/media/technology.png",
"tagLine": "Read articles about tech",
"description": "I have been working in tech for long, and here are my thoughts of random stuff",
"featuredBlog": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e"
}

View File

@ -1,7 +0,0 @@
{
"id": "b9e0d686-351d-49af-8e3d-b62023f44dbe",
"name": "Gaming",
"coverImage": "category/b9e0d686-351d-49af-8e3d-b62023f44dbe/media/game.png",
"tagLine": "Read articles about games",
"description": "I like to game, and here are my thoughts on games"
}

View File

@ -1,16 +0,0 @@
[
{
"id": "520b7982-069e-4a48-9ef3-64507d86a579",
"name": "Technology",
"coverImage": "category/520b7982-069e-4a48-9ef3-64507d86a579/media/technology.png",
"tagLine": "Read articles about tech",
"description": "I have been working in tech for long, and here are my thoughts of random stuff"
},
{
"id": "b9e0d686-351d-49af-8e3d-b62023f44dbe",
"name": "Gaming",
"coverImage": "category/b9e0d686-351d-49af-8e3d-b62023f44dbe/media/game.png",
"tagLine": "Read articles about games",
"description": "I like to game, and here are my thoughts on games"
}
]

View File

@ -1,38 +0,0 @@
{
"darkTheme": {
"theme": "Dark Mode",
"background": "bg-dark",
"textColor": "text-white",
"captionColor": "#8a8a8a",
"fontAwesomeIcon": "faSun",
"borderColor": "white",
"categoryNavigator": "light",
"navBar": {
"navBarTheme": "navbar-dark",
"background": "bg-secondary",
"buttonColor": "light"
},
"footer": {
"background": "bg-secondary",
"text": "bg-white"
}
},
"lightTheme":{
"theme": "Light Mode",
"background": "bg-light",
"textColor": "text-black",
"captionColor": "#605f5f",
"fontAwesomeIcon": "faMoon",
"borderColor": "black",
"categoryNavigator": "dark",
"navBar": {
"navBarTheme": "navbar-light",
"background": "bg-secondary",
"buttonColor": "light"
},
"footer": {
"background": "bg-secondary",
"text": "bg-white"
}
}
}

View File

@ -1,13 +0,0 @@
{
"name": "John Doe",
"greetingLine": "Hi! My name is",
"tagLine": "Me like tech. Checkout my blog where I have nice stuff!",
"profilePhoto": "homepage/media/profile.png",
"links": {
"instagram": ""
},
"contact":{
"email":"",
"phone": ""
}
}

View File

@ -138,7 +138,7 @@ function Blog(props) {
<Col xs="3" className="d-none d-md-block"></Col>
<Col className={`blogContent ${ThemeConfig[GlobalTheme].textColor}`} style={{marginBottom: '25px'}}>
<EditorComponent setContent={setBlogContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig} content={blogContent}/>
<EditorComponent notificationToggler={props.notificationToggler} setContent={setBlogContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig} content={blogContent} resourceType='blog' resourceId={blogData.id}/>
<ButtonGroup className='mt-4'>
<Button onClick={(event) => setInfo(event)} color={ThemeConfig[GlobalTheme].buttonColor} outline>Save Data</Button>
<Button color={ThemeConfig[GlobalTheme].buttonColor} outline>Publish Data</Button>

View File

@ -0,0 +1,46 @@
import React, { useState } from 'react';
import { Button, Label, Input } from 'reactstrap';
import EditableDataService from '../../../services/editable-data-service';
function FileComponent(props) {
const [file, setFile] = useState(null);
const [resourceType, setResourceType] = useState('');
const [resourceId, setResourceId] = useState('');
const handleFileChange = (event) => {
setFile(event.target.files[0]); // Assuming single file upload
};
const handleUpload = async (event) => {
event.preventDefault();
const formData = new FormData();
formData.append('media', file);
formData.append('resource_type', props.resourceType);
formData.append('resource_id', props.resourceId);
try {
const response = await EditableDataService.createData('/data/upload/', formData);
props.notificationToggler('Media uploaded successfully')
} catch (error) {
props.notificationToggler('Media upload failed', 'danger')
}
};
return (
<form onSubmit={handleUpload}>
<Label for="exampleFile">
File
</Label>
<Input
id="exampleFile"
name="file"
type="file"
onChange={handleFileChange}
/>
<Button className='mt-2' type="submit">Upload</Button>
</form>
);
}
export default FileComponent;

View File

@ -0,0 +1,59 @@
import React, { useState } from 'react';
import FileComponent from './file-component.jsx';
import { Button, ButtonGroup, Modal, ModalHeader, ModalBody, ModalFooter} from 'reactstrap';
function MediaUpload(props) {
const [action, setAction] = useState('insert')
const toggleAction = () =>{
if (action === 'insert')
setAction('upload')
if (action === 'upload')
setAction('insert')
}
return (
<div>
<Modal isOpen={props.modal} toggle={props.toggle}>
<ModalHeader toggle={props.toggle}>{props.modalTitle}</ModalHeader>
<ModalBody>
<ButtonGroup>
<Button
outline
active={action === 'insert'}
onClick={() => toggleAction()}
>
Insert Media
</Button>
<Button
outline
active={action === 'upload'}
onClick={() => toggleAction()}
>
Upload Media
</Button>
</ButtonGroup>
<div className="mt-3">
{ action === 'insert' ?
<div>
<h4>
Choose media to insert
</h4>
</div>:
<div>
<FileComponent notificationToggler={props.notificationToggler} resourceType={props.resourceType} resourceId={props.resourceId} />
</div>
}
</div>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={props.toggle}>
Cancel
</Button>
</ModalFooter>
</Modal>
</div>
);
}
export default MediaUpload;

View File

@ -0,0 +1,52 @@
/*
extension credits: Angelika Tyborska: https://angelika.me/2023/02/26/how-to-add-editing-image-alt-text-tiptap/
*/
import Image from '@tiptap/extension-image'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
import {
Button
} from 'reactstrap';
function ImageNode(props) {
const { src, alt } = props.node.attrs
const { updateAttributes } = props
const onEditAlt = () => {
const newAlt = prompt('Set alt text:', alt || '')
updateAttributes({ alt: newAlt })
}
let className = 'image'
if (props.selected) { className += ' ProseMirror-selectednode'}
return (
<NodeViewWrapper className={className} data-drag-handle>
<div className="image-container">
<img onClick={() => onEditAlt()} className='mx-auto d-block' src={src} alt={alt} />
<div className="image-overlay">
<span className="image-text mx-auto d-block">
{ alt ?
<span></span> :
<span>!</span>
}
{ alt ?
<span className="text">Alt text: "{alt}".</span>:
<span className="text">Alt text missing.</span>
}
<Button className="edit" type="button" onClick={onEditAlt}>
Edit
</Button>
</span>
</div>
</div>
</NodeViewWrapper>
)
}
export default Image.extend({
addNodeView() {
return ReactNodeViewRenderer(ImageNode)
}
})

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import {
Button, ButtonGroup, Label, Input } from 'reactstrap';
import { Color } from '@tiptap/extension-color'
@ -8,7 +8,6 @@ import Highlight from '@tiptap/extension-highlight'
import TextAlign from '@tiptap/extension-text-align'
import Underline from '@tiptap/extension-underline'
import Blockquote from '@tiptap/extension-blockquote'
import Image from '@tiptap/extension-image'
import Link from '@tiptap/extension-link'
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
@ -21,7 +20,11 @@ import { faBold, faItalic,
faListUl, faLink,
faListOl, faQuoteLeft,
faQuoteRight, faRulerHorizontal,
faRotateLeft, faRotateRight } from '@fortawesome/free-solid-svg-icons';
faRotateLeft, faRotateRight, faImage } from '@fortawesome/free-solid-svg-icons';
import CustomImageExtension from './tiptap-custom-extensions/custom-image-extension.jsx'
import MediaUpload from './media-upload.jsx'
const MenuBar = (props) => {
const { editor } = useCurrentEditor()
@ -310,6 +313,14 @@ const MenuBar = (props) => {
<FontAwesomeIcon icon={faRotateRight}/>
</Button>
</ButtonGroup>
<Button
className='mt-2 ms-2'
color={ThemeConfig[GlobalTheme].buttonColor}
onClick={() => props.toggle()}
outline
>
<FontAwesomeIcon icon={faImage}/>
</Button>
</>
)
}
@ -329,12 +340,7 @@ const extensions = [
}),
Underline,
Blockquote,
Image.configure({
allowBase64: true,
HTMLAttributes: {
class: 'mx-auto d-block',
},
}),
CustomImageExtension,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
@ -349,8 +355,14 @@ export default (props) => {
const GlobalTheme = props.GlobalTheme;
const ThemeConfig = props.ThemeConfig;
const [modal, setModal] = useState(false);
const toggle = () => setModal(!modal);
if (props.content && GlobalTheme && ThemeConfig)
return (
<EditorProvider slotBefore={<MenuBar setContent={props.setContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig}/>} extensions={extensions} content={props.content}></EditorProvider>
<>
<MediaUpload notificationToggler={props.notificationToggler} modal={modal} toggle={toggle} resourceType={props.resourceType} resourceId={props.resourceId}></MediaUpload>
<EditorProvider slotBefore={<MenuBar modal={modal} toggle={toggle} setContent={props.setContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig}/>} extensions={extensions} content={props.content}></EditorProvider>
</>
)
}

View File

@ -1,12 +1,12 @@
a {
text-decoration: none !important; /* Removes underline */
color: inherit !important; /* Inherits color from parent */
border: none !important; /* Removes any borders */
text-decoration: none !important;
color: inherit !important;
border: none !important;
}
.app-container {
display: grid;
grid-template-rows: auto 1fr auto; /* Header size, flexible content, footer size */
grid-template-rows: auto 1fr auto;
height: 100vh;
}
@ -42,7 +42,27 @@ a {
.blogContent img {
display: flex;
justify-content: center; /* Center horizontally */
justify-content: center;
align-items: center;
width: 50%;
}
width: 70%;
}
.image-container {
position: relative;
display: inline-block;
}
.image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
color: white;
background: rgba(0, 0, 0, 0.5);
}
.image-text {
display: block;
padding: 5px 0;
}