vault backup: 2025-12-26 02:09:22
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
This commit is contained in:
18
stroma/file/views/__init__.py
Normal file
18
stroma/file/views/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from .upload_files_page_view import upload_files_page
|
||||
from .upload_files_api_view import upload_files_api
|
||||
from .explorer_view import explorer_view, pdf_viewer_page, markdown_editor_page
|
||||
from .tree_api_view import get_file_tree
|
||||
from .content_api_view import get_file_content, save_file_content, serve_pdf_api
|
||||
|
||||
__all__ = [
|
||||
'upload_files_page',
|
||||
'upload_files_api',
|
||||
'explorer_view',
|
||||
'pdf_viewer_page',
|
||||
'markdown_editor_page',
|
||||
'get_file_tree',
|
||||
'get_file_content',
|
||||
'save_file_content',
|
||||
'serve_pdf_api',
|
||||
]
|
||||
|
||||
48
stroma/file/views/content_api_view.py
Normal file
48
stroma/file/views/content_api_view.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import json
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.shortcuts import get_object_or_404
|
||||
from file.models import File
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def get_file_content(request, file_id):
|
||||
"""Get the text content of a specific file."""
|
||||
file_obj = get_object_or_404(File, id=file_id, user=request.quiz_user)
|
||||
return JsonResponse({
|
||||
'id': file_obj.id,
|
||||
'name': file_obj.name,
|
||||
'content': file_obj.text
|
||||
})
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def save_file_content(request, file_id):
|
||||
"""Save updated text content to a file."""
|
||||
file_obj = get_object_or_404(File, id=file_id, user=request.quiz_user)
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
new_content = data.get('content', '')
|
||||
file_obj.text = new_content
|
||||
file_obj.save()
|
||||
return JsonResponse({'success': True})
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return JsonResponse({'success': False, 'error': 'Invalid data'}, status=400)
|
||||
|
||||
|
||||
from django.http import FileResponse
|
||||
|
||||
def serve_pdf_api(request, file_id):
|
||||
"""Serve the raw PDF file for viewing."""
|
||||
file_obj = get_object_or_404(File, id=file_id, user=request.quiz_user)
|
||||
|
||||
if not file_obj.mime_type == 'application/pdf' or not file_obj.file_content:
|
||||
return JsonResponse({'error': 'Not a PDF file'}, status=400)
|
||||
|
||||
response = FileResponse(
|
||||
file_obj.file_content.open('rb'),
|
||||
content_type='application/pdf'
|
||||
)
|
||||
# Add CORS headers to allow CDN-hosted PDF.js viewer to fetch the PDF
|
||||
response['Access-Control-Allow-Origin'] = '*'
|
||||
response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
|
||||
response['Access-Control-Allow-Headers'] = 'Content-Type'
|
||||
return response
|
||||
32
stroma/file/views/explorer_view.py
Normal file
32
stroma/file/views/explorer_view.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
def explorer_view(request):
|
||||
"""Render the file explorer page."""
|
||||
return render(request, 'file/explorer.html')
|
||||
|
||||
def pdf_viewer_page(request, file_id):
|
||||
"""Render the PDF viewer template for a specific file."""
|
||||
from django.urls import reverse
|
||||
from file.models import File
|
||||
file_obj = File.objects.get(id=file_id, user=request.quiz_user)
|
||||
relative_url = reverse('file:serve_pdf', args=[file_id])
|
||||
# Build absolute URL for PDF.js library
|
||||
pdf_url = request.build_absolute_uri(relative_url)
|
||||
return render(request, 'file/pdf_viewer.html', {
|
||||
'pdf_url': pdf_url,
|
||||
'file_name': file_obj.name
|
||||
})
|
||||
|
||||
def markdown_editor_page(request, file_id):
|
||||
"""Render the Markdown editor template for a specific file."""
|
||||
from django.urls import reverse
|
||||
from file.models import File
|
||||
file_obj = File.objects.get(id=file_id, user=request.quiz_user)
|
||||
|
||||
context = {
|
||||
'file_id': file_id,
|
||||
'file_name': file_obj.name,
|
||||
'get_content_url': reverse('file:get_content', args=[file_id]),
|
||||
'save_content_url': reverse('file:save_content', args=[file_id]),
|
||||
}
|
||||
return render(request, 'file/markdown_editor.html', context)
|
||||
31
stroma/file/views/tree_api_view.py
Normal file
31
stroma/file/views/tree_api_view.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from file.models import File
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def get_file_tree(request):
|
||||
"""Return the hierarchical file tree for the user."""
|
||||
files = File.objects.filter(user=request.quiz_user).select_related('parent').order_by('name')
|
||||
# Create a mapping of id -> item
|
||||
item_map = {}
|
||||
for f in files:
|
||||
item_map[f.id] = {
|
||||
'id': f.id,
|
||||
'name': f.name,
|
||||
'path': f.path,
|
||||
'type': 'folder' if f.mime_type == 'application/x-folder' else 'file',
|
||||
'mime_type': f.mime_type,
|
||||
'children': [],
|
||||
'content': f.text if f.mime_type.startswith('text/') else None
|
||||
}
|
||||
|
||||
root_items = []
|
||||
for f in files:
|
||||
item = item_map[f.id]
|
||||
if f.parent_id:
|
||||
if f.parent_id in item_map:
|
||||
item_map[f.parent_id]['children'].append(item)
|
||||
else:
|
||||
root_items.append(item)
|
||||
|
||||
return JsonResponse(root_items, safe=False)
|
||||
101
stroma/file/views/upload_files_api_view.py
Normal file
101
stroma/file/views/upload_files_api_view.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import os
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from file.models import File
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def upload_files_api(request):
|
||||
"""Handle file/folder uploads and create File model instances"""
|
||||
|
||||
uploaded_files = request.FILES.getlist('files')
|
||||
|
||||
if not uploaded_files:
|
||||
return JsonResponse({'error': 'No files uploaded'}, status=400)
|
||||
|
||||
created_files = []
|
||||
folder_cache = {} # Cache for created folder objects
|
||||
|
||||
for idx, uploaded_file in enumerate(uploaded_files):
|
||||
# Get the relative path if it exists (from webkitRelativePath)
|
||||
relative_path = request.POST.get(f'path_{idx}', '')
|
||||
|
||||
if relative_path:
|
||||
# This is from a folder upload
|
||||
path_obj = Path(relative_path)
|
||||
parts = path_obj.parts
|
||||
|
||||
# Create parent folders if needed
|
||||
parent = None
|
||||
for i, part in enumerate(parts[:-1]): # Exclude the file itself
|
||||
folder_path = os.path.join(*parts[:i+1])
|
||||
|
||||
if folder_path not in folder_cache:
|
||||
# Create or get folder
|
||||
folder, created = File.objects.get_or_create(
|
||||
user=request.quiz_user,
|
||||
path=folder_path,
|
||||
defaults={
|
||||
'name': part,
|
||||
'mime_type': 'application/x-folder',
|
||||
'parent': parent
|
||||
}
|
||||
)
|
||||
folder_cache[folder_path] = folder
|
||||
|
||||
parent = folder_cache[folder_path]
|
||||
|
||||
file_path = relative_path
|
||||
file_name = parts[-1]
|
||||
else:
|
||||
# Single file upload
|
||||
file_path = uploaded_file.name
|
||||
file_name = uploaded_file.name
|
||||
parent = None
|
||||
|
||||
# Determine MIME type
|
||||
mime_type, _ = mimetypes.guess_type(file_name)
|
||||
if not mime_type:
|
||||
mime_type = 'application/octet-stream'
|
||||
|
||||
# Read file content (for text files, store in text field)
|
||||
text_content = ''
|
||||
if mime_type.startswith('text/'):
|
||||
try:
|
||||
content_bytes = uploaded_file.read()
|
||||
text_content = content_bytes.decode('utf-8')
|
||||
uploaded_file.seek(0) # Reset for saving to disk
|
||||
except (UnicodeDecodeError, AttributeError):
|
||||
uploaded_file.seek(0)
|
||||
|
||||
# Generate unique filename with 8-digit hash
|
||||
import hashlib
|
||||
file_hash = hashlib.md5(f"{file_path}{uploaded_file.name}".encode()).hexdigest()[:8]
|
||||
name_parts = os.path.splitext(file_name)
|
||||
unique_filename = f"{name_parts[0]}_{file_hash}{name_parts[1]}"
|
||||
|
||||
# Create File instance
|
||||
file_obj = File.objects.create(
|
||||
user=request.quiz_user,
|
||||
name=file_name,
|
||||
path=file_path,
|
||||
mime_type=mime_type,
|
||||
parent=parent,
|
||||
text=text_content
|
||||
)
|
||||
|
||||
# Save the uploaded file to disk (not for folders)
|
||||
if mime_type != 'application/x-folder':
|
||||
file_obj.file_content.save(unique_filename, uploaded_file, save=True)
|
||||
|
||||
created_files.append(file_obj)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'count': len(created_files),
|
||||
'files': [{'name': f.name, 'path': f.path} for f in created_files]
|
||||
})
|
||||
|
||||
7
stroma/file/views/upload_files_page_view.py
Normal file
7
stroma/file/views/upload_files_page_view.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def upload_files_page(request):
|
||||
"""Render the file upload interface"""
|
||||
return render(request, 'file/upload_files.html')
|
||||
|
||||
Reference in New Issue
Block a user