updated user form and collection view, cache fixes

This commit is contained in:
Alexander Karpov 2023-11-19 02:38:51 +03:00
parent e94f90d091
commit 9ac5a1f235
11 changed files with 241 additions and 80 deletions

View File

@ -11,7 +11,7 @@ class HasPermissions(SingleObjectMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
has_perm = False has_perm = False
if self.request.user.is_authentificated: if self.request.user.is_authenticated:
has_perm = self.object.user == self.request.user has_perm = self.object.user == self.request.user
context["has_permissions"] = has_perm context["has_permissions"] = has_perm
return context return context

View File

@ -126,7 +126,7 @@ class Folder(BaseFileItem, ShortLinkModel, UserHistoryModel):
amount = IntegerField(default=0) amount = IntegerField(default=0)
def get_last_preview_files(self, cut=4): 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): def get_absolute_url(self):
return reverse("files:folder", kwargs={"slug": self.slug}) return reverse("files:folder", kwargs={"slug": self.slug})

View File

@ -1,16 +1,12 @@
from django import forms from django import forms
from akarpov.common.forms import MultipleFileField
from akarpov.gallery.models import Collection, Image from akarpov.gallery.models import Collection, Image
class ImageUploadForm(forms.ModelForm): class ImageUploadForm(forms.ModelForm):
image = MultipleFileField
class Meta: class Meta:
model = Image model = Image
fields = ( fields = (
"image",
"collection", "collection",
"public", "public",
) )
@ -21,17 +17,6 @@ def __init__(self, *args, **kwargs):
if user is not None: if user is not None:
self.fields["collection"].queryset = Collection.objects.filter(user=user) 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( ImageFormSet = forms.modelformset_factory(
Image, Image,

View File

@ -20,6 +20,9 @@ class Collection(TimeStampedModel, ShortLinkModel, UserHistoryModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("gallery:collection", kwargs={"slug": self.slug}) return reverse("gallery:collection", kwargs={"slug": self.slug})
def get_preview_images(self):
return self.images.cache().all()[:6]
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -16,6 +16,17 @@ def get_queryset(self):
return self.request.user.collections.all() return self.request.user.collections.all()
return Collection.objects.filter(public=True) 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() list_collections_view = ListCollectionsView.as_view()

View File

@ -22,9 +22,7 @@
<!-- Latest compiled and minified Bootstrap CSS --> <!-- Latest compiled and minified Bootstrap CSS -->
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{% if request.user.theme %} <link href="{{ request.user.get_theme_url }}" rel="stylesheet">
<link href="{{ request.user.theme.file.url }}" rel="stylesheet">
{% endif %}
{% endif %} {% endif %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"> <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> <script src="https://kit.fontawesome.com/32fd82c823.js" crossorigin="anonymous"></script>

View File

@ -1 +1,36 @@
{% extends 'base.html' %} {% 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 %}

View File

@ -2,42 +2,149 @@
{% load static %} {% load static %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block javascript %}
<script src="{% static 'js/jquery.min.js' %}"></script>
{% endblock %}
{% block content %} {% 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 %} {% csrf_token %}
{{ form.management_form }} {{ form.management_form }}
{% for field in form %} {% for field in form %}
{{ field | as_crispy_field }} {{ field | as_crispy_field }}
{% endfor %} {% endfor %}
<button type="submit">Upload</button> <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> </form>
<div id="progress-bar" style="width:0; height:20px; background:green;"></div> <div id="progress-bar" style="width:0; height:20px; background:green;"></div>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/jquery.min.js' %}"></script>
<script> <script>
const form = document.querySelector('form'); document.addEventListener('DOMContentLoaded', function () {
form.addEventListener('submit', function(e) { let form = document.querySelector('form');
e.preventDefault(); let imageInput = document.getElementById('imageInput');
const formData = new FormData(form); const preview = document.getElementById('preview');
const xhr = new XMLHttpRequest(); let currentFiles = [];
xhr.open('POST', form.action, true);
xhr.upload.onprogress = function(e) { imageInput.addEventListener('change', function () {
if (e.lengthComputable) { updateFiles(this.files);
const percentage = (e.loaded / e.total) * 100; updatePreview();
document.getElementById('progress-bar').style.width = percentage + '%'; });
}
}; function updateFiles(newFiles) {
xhr.onload = function() { currentFiles = Array.from(newFiles);
if (this.status === 200) { }
console.log('Upload complete!');
} else { function updatePreview() {
console.error('Upload failed:', this.responseText); preview.innerHTML = '';
} currentFiles.forEach((file, index) => {
}; let imgContainer = document.createElement('div');
xhr.send(formData); 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> </script>
{% endblock %} {% endblock %}

View File

@ -1,36 +1,49 @@
{% extends 'base.html' %} {% 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 %} {% block css %}
<style> <style>
.gallery { .folder-icon {
display: flex; display: flex;
flex-wrap: wrap; justify-content: center;
justify-content: start; /* Align items to the start of the container */ align-items: center;
align-items: stretch; /* Stretch items to fill the container */ min-height: 120px; /* Adjust size as needed */
} }
.folder-images img {
.gallery-item { transition: transform 0.2s; /* Smooth transition for image hover */
/* Adjust the margin as needed */ }
margin: 5px; .folder-images img:hover {
flex-grow: 1; transform: scale(1.1); /* Slightly enlarge images on hover */
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> </style>
{% endblock %} {% 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 %}

View File

@ -8,6 +8,7 @@
from akarpov.common.models import BaseImageModel from akarpov.common.models import BaseImageModel
from akarpov.tools.shortener.models import ShortLinkModel from akarpov.tools.shortener.models import ShortLinkModel
from akarpov.users.themes.models import Theme
class User(AbstractUser, BaseImageModel, ShortLinkModel): 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) 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): def get_absolute_url(self):
"""Get url for user's detail view. """Get url for user's detail view.

View File

@ -63,7 +63,10 @@
CACHEOPS = { CACHEOPS = {
"auth.user": {"ops": "get", "timeout": 60 * 15}, "auth.user": {"ops": "get", "timeout": 60 * 15},
"auth.*": {"ops": ("fetch", "get"), "timeout": 60 * 2}, "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}, "auth.permission": {"ops": "all", "timeout": 60 * 15},
} }
CACHEOPS_REDIS = env.str("REDIS_URL") CACHEOPS_REDIS = env.str("REDIS_URL")