mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 10:56:39 +03:00
updated user form and collection view, cache fixes
This commit is contained in:
parent
e94f90d091
commit
9ac5a1f235
|
@ -11,7 +11,7 @@ class HasPermissions(SingleObjectMixin):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
has_perm = False
|
||||
if self.request.user.is_authentificated:
|
||||
if self.request.user.is_authenticated:
|
||||
has_perm = self.object.user == self.request.user
|
||||
context["has_permissions"] = has_perm
|
||||
return context
|
||||
|
|
|
@ -126,7 +126,7 @@ class Folder(BaseFileItem, ShortLinkModel, UserHistoryModel):
|
|||
amount = IntegerField(default=0)
|
||||
|
||||
def get_last_preview_files(self, cut=4):
|
||||
return self.children.filter(~Q(File___preview=""))[:cut]
|
||||
return self.children.cache().filter(~Q(File___preview=""))[:cut]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("files:folder", kwargs={"slug": self.slug})
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
from django import forms
|
||||
|
||||
from akarpov.common.forms import MultipleFileField
|
||||
from akarpov.gallery.models import Collection, Image
|
||||
|
||||
|
||||
class ImageUploadForm(forms.ModelForm):
|
||||
image = MultipleFileField
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = (
|
||||
"image",
|
||||
"collection",
|
||||
"public",
|
||||
)
|
||||
|
@ -21,17 +17,6 @@ def __init__(self, *args, **kwargs):
|
|||
if user is not None:
|
||||
self.fields["collection"].queryset = Collection.objects.filter(user=user)
|
||||
|
||||
def save(self, commit=True):
|
||||
files = self.files.getlist("image")
|
||||
instances = []
|
||||
for file in files:
|
||||
instance = self.instance
|
||||
instance.image = file
|
||||
if commit:
|
||||
instance.save()
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
|
||||
ImageFormSet = forms.modelformset_factory(
|
||||
Image,
|
||||
|
|
|
@ -20,6 +20,9 @@ class Collection(TimeStampedModel, ShortLinkModel, UserHistoryModel):
|
|||
def get_absolute_url(self):
|
||||
return reverse("gallery:collection", kwargs={"slug": self.slug})
|
||||
|
||||
def get_preview_images(self):
|
||||
return self.images.cache().all()[:6]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
|
|
@ -16,6 +16,17 @@ def get_queryset(self):
|
|||
return self.request.user.collections.all()
|
||||
return Collection.objects.filter(public=True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["collection_previews"] = [
|
||||
{
|
||||
"collection": collection,
|
||||
"preview_images": collection.get_preview_images(),
|
||||
}
|
||||
for collection in context["collection_list"]
|
||||
]
|
||||
return context
|
||||
|
||||
|
||||
list_collections_view = ListCollectionsView.as_view()
|
||||
|
||||
|
|
|
@ -22,9 +22,7 @@
|
|||
<!-- Latest compiled and minified Bootstrap CSS -->
|
||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
||||
{% if request.user.is_authenticated %}
|
||||
{% if request.user.theme %}
|
||||
<link href="{{ request.user.theme.file.url }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
<link href="{{ request.user.get_theme_url }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css">
|
||||
<script src="https://kit.fontawesome.com/32fd82c823.js" crossorigin="anonymous"></script>
|
||||
|
|
|
@ -1 +1,36 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
.gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start; /* Align items to the start of the container */
|
||||
align-items: stretch; /* Stretch items to fill the container */
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
/* Adjust the margin as needed */
|
||||
margin: 5px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto; /* Automatically adjust the basis based on the content size */
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%; /* Make image responsive */
|
||||
height: auto; /* Maintain aspect ratio */
|
||||
display: block; /* Remove bottom space under the image */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="gallery">
|
||||
{% for image in object_list %}
|
||||
<div class="gallery-item">
|
||||
<img src="{{ image.url }}" alt="Image">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,42 +2,149 @@
|
|||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/jquery.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action="{% url 'gallery:upload' %}" enctype="multipart/form-data">
|
||||
<form method="post" action="{% url 'api:gallery:list-create-all' %}" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.management_form }}
|
||||
{% for field in form %}
|
||||
{{ field | as_crispy_field }}
|
||||
{% endfor %}
|
||||
<button type="submit">Upload</button>
|
||||
{% for field in form %}
|
||||
{{ field | as_crispy_field }}
|
||||
{% endfor %}
|
||||
<input type="file" id="imageInput" multiple accept="image/*" class="form-control" />
|
||||
<div class="preview mt-3" id="preview"></div>
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
|
||||
<div id="progress-bar" style="width:0; height:20px; background:green;"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/jquery.min.js' %}"></script>
|
||||
<script>
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', form.action, true);
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const percentage = (e.loaded / e.total) * 100;
|
||||
document.getElementById('progress-bar').style.width = percentage + '%';
|
||||
}
|
||||
};
|
||||
xhr.onload = function() {
|
||||
if (this.status === 200) {
|
||||
console.log('Upload complete!');
|
||||
} else {
|
||||
console.error('Upload failed:', this.responseText);
|
||||
}
|
||||
};
|
||||
xhr.send(formData);
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let form = document.querySelector('form');
|
||||
let imageInput = document.getElementById('imageInput');
|
||||
const preview = document.getElementById('preview');
|
||||
let currentFiles = [];
|
||||
|
||||
imageInput.addEventListener('change', function () {
|
||||
updateFiles(this.files);
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
function updateFiles(newFiles) {
|
||||
currentFiles = Array.from(newFiles);
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
preview.innerHTML = '';
|
||||
currentFiles.forEach((file, index) => {
|
||||
let imgContainer = document.createElement('div');
|
||||
imgContainer.classList.add('position-relative', 'd-inline-block', 'm-2');
|
||||
imgContainer.setAttribute('data-index', index); // Set the data-index attribute
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.classList.add('img-thumbnail');
|
||||
img.style.width = '150px';
|
||||
img.style.height = '150px';
|
||||
imgContainer.appendChild(img);
|
||||
|
||||
let deleteButton = document.createElement('span');
|
||||
deleteButton.classList.add('material-icons', 'position-absolute', 'top-0', 'end-0', 'btn', 'btn-danger');
|
||||
deleteButton.innerText = 'delete';
|
||||
deleteButton.style.cursor = 'pointer';
|
||||
deleteButton.onclick = function () {
|
||||
currentFiles.splice(index, 1);
|
||||
updatePreview();
|
||||
imageInput.value = "";
|
||||
};
|
||||
imgContainer.appendChild(deleteButton);
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = (function (aImg) {
|
||||
return function (e) {
|
||||
aImg.src = e.target.result;
|
||||
};
|
||||
})(img);
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
preview.appendChild(imgContainer);
|
||||
});
|
||||
}
|
||||
|
||||
function createProgressBar(index) {
|
||||
const progressBarContainer = document.createElement('div');
|
||||
progressBarContainer.classList.add('progress', 'mb-2');
|
||||
progressBarContainer.setAttribute('id', 'progress-container-' + index);
|
||||
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.id = 'progress-bar-' + index;
|
||||
progressBar.classList.add('progress-bar');
|
||||
progressBar.setAttribute('role', 'progressbar');
|
||||
progressBar.setAttribute('aria-valuenow', '0');
|
||||
progressBar.setAttribute('aria-valuemin', '0');
|
||||
progressBar.setAttribute('aria-valuemax', '100');
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.style.height = '20px'; // Set the height of the progress bar
|
||||
|
||||
progressBarContainer.appendChild(progressBar);
|
||||
form.appendChild(progressBarContainer); // Append the progress bar container to the form
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get the CSRF token from the hidden input
|
||||
const csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||
|
||||
// Clear previous progress bars if any
|
||||
document.querySelectorAll('.progress').forEach(function(progressBar) {
|
||||
progressBar.remove();
|
||||
});
|
||||
|
||||
// Create a new progress bar for each file
|
||||
currentFiles.forEach((file, index) => createProgressBar(index));
|
||||
|
||||
// Perform the upload for each file
|
||||
currentFiles.forEach((file, index) => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('collection', document.querySelector('#id_collection').value);
|
||||
formData.append('public', document.querySelector('#id_public').checked);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', form.action, true);
|
||||
xhr.setRequestHeader('X-CSRFToken', csrfToken);
|
||||
|
||||
xhr.upload.onprogress = function (e) {
|
||||
if (e.lengthComputable) {
|
||||
const progressBar = document.getElementById('progress-bar-' + index);
|
||||
const percentage = (e.loaded / e.total) * 100;
|
||||
progressBar.style.width = percentage + '%';
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function () {
|
||||
if (this.status === 200 || this.status === 201) {
|
||||
console.log('Upload complete for file', index);
|
||||
// Remove the image preview and progress bar for the uploaded file
|
||||
const imgPreview = preview.querySelector(`div[data-index="${index}"]`);
|
||||
if (imgPreview) {
|
||||
preview.removeChild(imgPreview);
|
||||
}
|
||||
const progressBarContainer = document.getElementById('progress-container-' + index);
|
||||
if (progressBarContainer) {
|
||||
progressBarContainer.remove();
|
||||
}
|
||||
// Remove the file from the currentFiles array
|
||||
currentFiles = currentFiles.filter((_, i) => i !== index);
|
||||
} else {
|
||||
console.error('Upload failed for file', index, ':', this.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,36 +1,49 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{% for collection_preview in collection_previews %}
|
||||
<div class="col-md-4 col-sm-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<!-- Folder Icon -->
|
||||
<div class="folder-icon">
|
||||
<i class="fas fa-folder" style="font-size: 6em;"></i>
|
||||
</div>
|
||||
<h5 class="card-title text-center">{{ collection_preview.collection.name }}</h5>
|
||||
<!-- Image Thumbnails -->
|
||||
<div class="folder-images d-flex flex-wrap justify-content-center">
|
||||
{% for image in collection_preview.preview_images %}
|
||||
<img src="{{ image.image_cropped.url }}" class="img-thumbnail m-1" alt="{{ image }}" style="width: 50px; height: 50px; object-fit: cover;">
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{{ collection_preview.collection.get_absolute_url }}" class="btn btn-primary btn-block">View Collection</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>No collections found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
.gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start; /* Align items to the start of the container */
|
||||
align-items: stretch; /* Stretch items to fill the container */
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
/* Adjust the margin as needed */
|
||||
margin: 5px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto; /* Automatically adjust the basis based on the content size */
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%; /* Make image responsive */
|
||||
height: auto; /* Maintain aspect ratio */
|
||||
display: block; /* Remove bottom space under the image */
|
||||
}
|
||||
.folder-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 120px; /* Adjust size as needed */
|
||||
}
|
||||
.folder-images img {
|
||||
transition: transform 0.2s; /* Smooth transition for image hover */
|
||||
}
|
||||
.folder-images img:hover {
|
||||
transform: scale(1.1); /* Slightly enlarge images on hover */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="gallery">
|
||||
{% for image in object_list %}
|
||||
<div class="gallery-item">
|
||||
<img src="{{ image.url }}" alt="Image">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
from akarpov.common.models import BaseImageModel
|
||||
from akarpov.tools.shortener.models import ShortLinkModel
|
||||
from akarpov.users.themes.models import Theme
|
||||
|
||||
|
||||
class User(AbstractUser, BaseImageModel, ShortLinkModel):
|
||||
|
@ -29,6 +30,11 @@ class User(AbstractUser, BaseImageModel, ShortLinkModel):
|
|||
)
|
||||
theme = models.ForeignKey("themes.Theme", null=True, on_delete=models.SET_NULL)
|
||||
|
||||
def get_theme_url(self):
|
||||
if self.theme_id:
|
||||
return Theme.objects.cache().get(id=self.theme_id).file.url
|
||||
return ""
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Get url for user's detail view.
|
||||
|
||||
|
|
|
@ -63,7 +63,10 @@
|
|||
CACHEOPS = {
|
||||
"auth.user": {"ops": "get", "timeout": 60 * 15},
|
||||
"auth.*": {"ops": ("fetch", "get"), "timeout": 60 * 2},
|
||||
"blog.post": {"ops": ("fetch", "get"), "timeout": 15},
|
||||
"blog.post": {"ops": ("fetch", "get"), "timeout": 20 * 15},
|
||||
"themes.theme": {"ops": ("fetch", "get"), "timeout": 60 * 60},
|
||||
"gallery.*": {"ops": "all", "timeout": 60 * 15},
|
||||
"files.*": {"ops": "all", "timeout": 60 * 5},
|
||||
"auth.permission": {"ops": "all", "timeout": 60 * 15},
|
||||
}
|
||||
CACHEOPS_REDIS = env.str("REDIS_URL")
|
||||
|
|
Loading…
Reference in New Issue
Block a user