Update upload template

This commit is contained in:
Nyymix 2025-04-23 14:42:05 +03:00
parent 8015d96c7a
commit b2ffd28e8e
3 changed files with 209 additions and 165 deletions

View file

@ -75,11 +75,11 @@ class AlbumAdmin(admin.ModelAdmin):
"""Add custom upload route to admin."""
urls = super().get_urls()
custom_urls = [
path('<int:album_id>/upload/', self.admin_site.admin_view(self.upload_photos), name="gallery_album_upload"),
path('<int:album_id>/upload/', self.admin_site.admin_view(self.photo_upload), name="gallery_album_upload"),
]
return custom_urls + urls
def upload_photos(self, request, album_id, *args, **kwargs):
def photo_upload(self, request, album_id, *args, **kwargs):
"""Handle photo upload via admin UI."""
album = get_object_or_404(Album, id=album_id)
if request.method == 'POST' and request.FILES:
@ -91,7 +91,7 @@ class AlbumAdmin(admin.ModelAdmin):
Photo.objects.create(album=album, photo=file)
return JsonResponse({"message": "Photos uploaded successfully!"})
return render(request, 'admin/upload_photos.html', {'album': album})
return render(request, 'admin/photo_upload.html', {'album': album})
def upload_link(self, obj):
"""Show 'Upload Photos' button in album list."""

View file

@ -0,0 +1,206 @@
{% extends "admin/base_site.html" %}
{% block content %}
<!-- UIkit CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.23.6/dist/css/uikit.min.css" />
<!-- UIkit JS -->
<script src="https://cdn.jsdelivr.net/npm/uikit@3.23.6/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.23.6/dist/js/uikit-icons.min.js"></script>
<div class="module">
<h2>Upload: {{ album.name }}</h2>
<div class="uk-section-xsmall">
<div id="upload-drop-area" class="uk-placeholder uk-text-center uk-margin-small-bottom">
<span uk-icon="icon: cloud-upload"></span>
<span class="uk-text-middle">
Drop photos here or <span class="uk-link" id="file-picker">select</span>
</span>
<input id="file-input" type="file" accept=".jpg,.jpeg,.png" multiple hidden>
</div>
<ul id="file-list" class="uk-list uk-margin-small"></ul>
<div id="total-size" class="uk-text-bold uk-margin-small"></div>
<div id="progress-container" class="uk-margin-top" hidden>
<progress id="upload-progress" class="uk-progress" value="0" max="100"></progress>
</div>
<button class="uk-button uk-button-primary uk-margin-top" onclick="uploadFiles()">Upload</button>
</div>
</div>
<script>
let selectedFiles = [];
const fileStatuses = new Map();
const objectURLs = new Map();
const fileInput = document.getElementById('file-input');
const filePicker = document.getElementById('file-picker');
const fileList = document.getElementById('file-list');
const dropArea = document.getElementById('upload-drop-area');
const allowedTypes = ['image/jpeg', 'image/png'];
const maxFileSizeMB = 10;
filePicker.addEventListener('click', () => fileInput.click());
function renderFileList() {
fileList.innerHTML = '';
let totalSize = 0;
selectedFiles.forEach((file, index) => {
totalSize += file.size;
const li = document.createElement('li');
li.classList.add('uk-flex', 'uk-flex-middle', 'uk-flex-between', 'uk-margin-small');
const left = document.createElement('div');
left.classList.add('uk-flex', 'uk-flex-middle');
const img = document.createElement('img');
const objectURL = URL.createObjectURL(file);
objectURLs.set(file.name, objectURL);
img.src = objectURL;
img.style.width = '40px';
img.style.height = '40px';
img.style.objectFit = 'cover';
img.style.marginRight = '10px';
const nameSpan = document.createElement('span');
nameSpan.textContent = `${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`;
left.appendChild(img);
left.appendChild(nameSpan);
const deleteBtn = document.createElement('button');
deleteBtn.classList.add('uk-button', 'uk-button-danger', 'uk-button-small');
deleteBtn.textContent = 'Poista';
deleteBtn.onclick = () => {
URL.revokeObjectURL(objectURLs.get(file.name));
objectURLs.delete(file.name);
selectedFiles.splice(index, 1);
renderFileList();
};
li.appendChild(left);
li.appendChild(deleteBtn);
fileList.appendChild(li);
});
if (selectedFiles.length > 0) {
const totalLi = document.createElement('li');
totalLi.classList.add('uk-text-small', 'uk-text-muted', 'uk-margin-top');
const totalMB = (totalSize / 1024 / 1024).toFixed(2);
totalLi.textContent = `Total size: ${totalMB} MB`;
fileList.appendChild(totalLi);
}
document.getElementById('total-size').textContent = '';
}
function uploadFiles() {
if (selectedFiles.length === 0) {
alert('Please select files to upload');
return;
}
const progressBar = document.getElementById('upload-progress');
document.getElementById('progress-container').hidden = false;
let uploaded = 0;
let total = selectedFiles.length;
let newFiles = [];
function updateProgress() {
progressBar.value = (uploaded / total) * 100;
}
const uploadPromises = selectedFiles.map(file => {
const formData = new FormData();
formData.append('photo', file);
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
return fetch('', {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
fileStatuses.set(file.name, 'success');
} else {
fileStatuses.set(file.name, 'error');
newFiles.push(file);
}
}).catch(() => {
fileStatuses.set(file.name, 'error');
newFiles.push(file);
}).finally(() => {
uploaded++;
updateProgress();
});
});
Promise.allSettled(uploadPromises).then(() => {
selectedFiles = newFiles;
renderFileList();
UIkit.notification({
message: `Upload completed - ${total - newFiles.length} succeeded, ${newFiles.length} failed`,
status: newFiles.length > 0 ? 'warning' : 'success',
pos: 'top-center'
});
});
}
function handleFiles(files) {
for (let file of files) {
if (!allowedTypes.includes(file.type)) {
UIkit.notification({
message: `Not allowed: ${file.name}`,
status: 'danger',
pos: 'top-center'
});
continue;
}
if (file.size / (1024 * 1024) > maxFileSizeMB) {
UIkit.notification({
message: `Too large: ${file.name} > ${maxFileSizeMB}MB`,
status: 'danger',
pos: 'top-center'
});
continue;
}
if (!selectedFiles.find(f => f.name === file.name && f.size === file.size)) {
selectedFiles.push(file);
}
}
renderFileList();
}
dropArea.addEventListener('dragover', e => {
e.preventDefault();
dropArea.classList.add('uk-background-primary', 'uk-light');
});
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('uk-background-primary', 'uk-light');
});
dropArea.addEventListener('drop', e => {
e.preventDefault();
dropArea.classList.remove('uk-background-primary', 'uk-light');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => {
handleFiles(fileInput.files);
fileInput.value = '';
});
window.addEventListener('beforeunload', () => {
objectURLs.forEach(url => URL.revokeObjectURL(url));
});
</script>
{% endblock %}

View file

@ -1,162 +0,0 @@
{% extends "admin/base_site.html" %}
{% block content %}
<div class="module">
<h2>Upload Photos to {{ album.name }}</h2>
<form id="upload-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row">
<div id="js-drop" class="drop-area">
<p><strong>Drag files here</strong> or click below to select</p>
<input id="file-input" type="file" name="photo" multiple accept=".jpg,.jpeg">
</div>
</div>
<ul id="file-list" class="messagelist"></ul>
<p>Total size: <span id="total-size">0</span> MB</p>
<progress id="js-progressbar" max="100" value="0" hidden></progress>
<div class="submit-row">
<button id="upload-button" class="default" type="button">Upload</button>
</div>
</form>
</div>
<style>
.drop-area {
border: 2px dashed #ddd;
padding: 20px;
text-align: center;
background: #f9f9f9;
cursor: pointer;
margin-bottom: 10px;
transition: background 0.3s, border-color 0.3s;
}
.drop-area.dragover {
border-color: #007bff;
background: #eef7ff;
}
#file-list {
margin-top: 10px;
list-style: none;
padding: 0;
}
#file-list li {
background: #f5f5f5;
padding: 5px;
margin-bottom: 5px;
border-radius: 3px;
}
.success {
color: green;
}
.error {
color: red;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
const fileInput = document.getElementById('file-input');
const fileList = document.getElementById('file-list');
const totalSizeElement = document.getElementById('total-size');
const uploadButton = document.getElementById('upload-button');
const progressBar = document.getElementById('js-progressbar');
const dropArea = document.getElementById('js-drop');
let filesToUpload = [];
let totalSize = 0;
const maxSize = 10 * 1024 * 1024; // 10MB per file
function updateFileList(newFiles) {
// Suodatetaan vain JPEG-tiedostot ja poistetaan liian suuret
filesToUpload = [...newFiles].filter(file => {
if (!file.name.match(/\.jpe?g$/i)) {
alert(`File ${file.name} is not a JPG!`);
return false;
}
if (file.size > maxSize) {
alert(`File ${file.name} is too large! Max size: 10MB.`);
return false;
}
return true;
});
fileList.innerHTML = '';
totalSize = filesToUpload.reduce((acc, file) => acc + file.size, 0);
filesToUpload.forEach(file => {
const listItem = document.createElement('li');
listItem.textContent = `${file.name} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`;
fileList.appendChild(listItem);
});
totalSizeElement.textContent = (totalSize / (1024 * 1024)).toFixed(2);
}
fileInput.addEventListener('change', event => updateFileList(event.target.files));
['dragenter', 'dragover'].forEach(eventName => dropArea.addEventListener(eventName, e => {
e.preventDefault();
dropArea.classList.add('dragover');
}));
['dragleave', 'drop'].forEach(eventName => dropArea.addEventListener(eventName, e => {
e.preventDefault();
dropArea.classList.remove('dragover');
}));
dropArea.addEventListener('drop', event => {
updateFileList(event.dataTransfer.files);
});
uploadButton.addEventListener('click', () => {
if (filesToUpload.length === 0) {
alert('Please select files to upload.');
return;
}
progressBar.hidden = false;
progressBar.value = 0;
let uploadedSize = 0;
filesToUpload.forEach((file, index) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('photo', file);
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
xhr.open('POST', '', true);
xhr.upload.onprogress = event => {
if (event.lengthComputable) {
progressBar.value = ((uploadedSize + event.loaded) / totalSize) * 100;
}
};
xhr.onload = () => {
const listItem = fileList.children[index];
if (xhr.status === 200) {
listItem.textContent += ' - Upload successful!';
listItem.classList.add('success');
} else {
listItem.textContent += ' - Upload failed!';
listItem.classList.add('error');
}
uploadedSize += file.size;
};
xhr.onerror = () => {
const listItem = fileList.children[index];
listItem.textContent += ' - Upload failed!';
listItem.classList.add('error');
};
xhr.send(formData);
});
});
});
</script>
{% endblock %}