mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-25 21:03:44 +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):
|
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
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
<input type="file" id="imageInput" multiple accept="image/*" class="form-control" />
|
||||||
|
<div class="preview mt-3" id="preview"></div>
|
||||||
<button type="submit">Upload</button>
|
<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 () {
|
||||||
|
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) {
|
form.addEventListener('submit', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(form);
|
|
||||||
|
// 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();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', form.action, true);
|
xhr.open('POST', form.action, true);
|
||||||
|
xhr.setRequestHeader('X-CSRFToken', csrfToken);
|
||||||
|
|
||||||
xhr.upload.onprogress = function (e) {
|
xhr.upload.onprogress = function (e) {
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
|
const progressBar = document.getElementById('progress-bar-' + index);
|
||||||
const percentage = (e.loaded / e.total) * 100;
|
const percentage = (e.loaded / e.total) * 100;
|
||||||
document.getElementById('progress-bar').style.width = percentage + '%';
|
progressBar.style.width = percentage + '%';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
if (this.status === 200) {
|
if (this.status === 200 || this.status === 201) {
|
||||||
console.log('Upload complete!');
|
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 {
|
} else {
|
||||||
console.error('Upload failed:', this.responseText);
|
console.error('Upload failed for file', index, ':', this.responseText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-basis: auto; /* Automatically adjust the basis based on the content size */
|
|
||||||
}
|
}
|
||||||
|
.folder-images img:hover {
|
||||||
.gallery-item img {
|
transform: scale(1.1); /* Slightly enlarge images on hover */
|
||||||
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 %}
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user