mirror of
https://github.com/Findus23/RPGnotes.git
synced 2024-09-19 15:43:45 +02:00
better image and delete unused
This commit is contained in:
parent
17c192a559
commit
c9892a59a1
10 changed files with 146 additions and 27 deletions
|
@ -2,9 +2,10 @@
|
|||
|
||||
{% block content %}
|
||||
<h1>{{ object }}</h1>
|
||||
<h2>Players</h2>
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>{{ user }}</li>
|
||||
{% endfor %}
|
||||
{% for user,characters in players.items %}
|
||||
<li>{{ user }}{% if characters %} ({{ characters|join:", " }}){% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
|
|
@ -45,7 +45,12 @@ class CampaignDetailView(generic.DetailView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CampaignDetailView, self).get_context_data(**kwargs)
|
||||
context["users"] = self.get_object().user_set.all()
|
||||
players = self.get_object().user_set.exclude(pk__in=[1, 2])
|
||||
|
||||
context["players"] = {}
|
||||
player: TenantUser
|
||||
for player in players:
|
||||
context["players"][player] = player.characters.all()
|
||||
return context
|
||||
|
||||
|
||||
|
|
|
@ -6,4 +6,4 @@ from characters.models import Character
|
|||
class CharacterForm(ModelForm):
|
||||
class Meta:
|
||||
model = Character
|
||||
fields = ["name", "description_md", "subtitle", "player", "location", "color", "image"]
|
||||
fields = ["name", "description_md", "subtitle", "player", "location", "color", "token_image"]
|
||||
|
|
|
@ -9,6 +9,7 @@ from common.models import BaseModel, DescriptionModel
|
|||
from locations.models import Location
|
||||
from rpg_notes.settings import AUTH_USER_MODEL
|
||||
from utils.colors import get_random_color, is_bright_color
|
||||
from utils.random_filename import get_file_path
|
||||
|
||||
|
||||
def validate_color_hex(value: str):
|
||||
|
@ -18,14 +19,16 @@ def validate_color_hex(value: str):
|
|||
|
||||
class Character(BaseModel, DescriptionModel):
|
||||
subtitle = models.CharField(max_length=100, blank=True)
|
||||
player = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.PROTECT, blank=True, null=True)
|
||||
player = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.PROTECT, blank=True, null=True,
|
||||
related_name="characters")
|
||||
# faction = models.ForeignKey(Faction, on_delete=models.PROTECT, blank=True, null=True)
|
||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
||||
color = models.CharField(max_length=7, default=get_random_color, validators=[
|
||||
MinLengthValidator(7),
|
||||
validate_color_hex
|
||||
])
|
||||
image = ImageField(upload_to="character_images", blank=True, null=True)
|
||||
token_image = ImageField(upload_to=get_file_path, blank=True, null=True)
|
||||
large_image = ImageField(upload_to=get_file_path, blank=True, null=True)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
last_modified = models.DateTimeField(auto_now=True)
|
||||
|
@ -42,3 +45,13 @@ class Character(BaseModel, DescriptionModel):
|
|||
|
||||
def text_color(self):
|
||||
return "black" if is_bright_color(self.color) else "white"
|
||||
|
||||
def larger_image(self):
|
||||
if self.large_image:
|
||||
return self.large_image
|
||||
return self.token_image
|
||||
|
||||
def smaller_image(self):
|
||||
if self.token_image:
|
||||
return self.token_image
|
||||
return self.larger_image
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
</div>
|
||||
<div class="col-8">
|
||||
<div class="character-heading" style="border-bottom-color: {{ character.color }}">
|
||||
{% if character.image %}
|
||||
{% thumbnail character.image "150x150" crop="center" as im %}
|
||||
<a href="{{ character.image.url }}" target="_blank">
|
||||
{% if character.larger_image %}
|
||||
{% thumbnail character.larger_image "150x150" crop="center" as im %}
|
||||
<a href="{{ character.larger_image.url }}" target="_blank">
|
||||
<img class="avatar avatar-image avatar-large rounded-circle" src="{{ im.url }}" width="{{ im.width }}"
|
||||
height="{{ im.height }}"
|
||||
srcset="{{ im.url|srcset }}">
|
||||
|
|
76
common/management/commands/delete_unused_files.py
Normal file
76
common/management/commands/delete_unused_files.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
"""
|
||||
based on https://github.com/akolpakov/django-unused-media/
|
||||
licensed under MIT license
|
||||
by Andrey Kolpakov
|
||||
|
||||
but using pathlib and considering django-tenants
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management import BaseCommand
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
from django.db.models import FileField
|
||||
from django_tenants.utils import tenant_context
|
||||
|
||||
from campaigns.models import Campaign
|
||||
from rpg_notes.settings import MEDIA_ROOT
|
||||
|
||||
|
||||
def get_file_fields():
|
||||
all_models = apps.get_models()
|
||||
|
||||
fields = []
|
||||
|
||||
for model in all_models:
|
||||
for field in model._meta.get_fields():
|
||||
if isinstance(field, FileField):
|
||||
fields.append(field)
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def get_used_media() -> Set[Path]:
|
||||
media = set()
|
||||
for field in get_file_fields():
|
||||
is_null = {
|
||||
f'{field.name}__isnull': True,
|
||||
}
|
||||
is_empty = {
|
||||
field.name: '',
|
||||
}
|
||||
|
||||
storage = field.storage
|
||||
|
||||
for value in field.model._base_manager \
|
||||
.values_list(field.name, flat=True) \
|
||||
.exclude(**is_empty).exclude(**is_null):
|
||||
if value not in EMPTY_VALUES:
|
||||
media.add(Path(storage.path(value)).resolve())
|
||||
|
||||
return media
|
||||
|
||||
|
||||
def get_all_media(schema: str) -> Set[Path]:
|
||||
media = set()
|
||||
media_root = Path(MEDIA_ROOT) / schema
|
||||
cache_dir = media_root / "cache"
|
||||
for file in media_root.glob("**/*"):
|
||||
if not file.is_file():
|
||||
continue
|
||||
if cache_dir in file.parents:
|
||||
continue
|
||||
media.add(file.resolve())
|
||||
return media
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **kwargs):
|
||||
for campaign in Campaign.objects.exclude(pk=1):
|
||||
print(campaign, campaign.schema_name)
|
||||
with tenant_context(campaign):
|
||||
used = get_used_media()
|
||||
all = get_all_media(campaign.schema_name)
|
||||
unused = all - used
|
||||
print(all - used)
|
|
@ -15,7 +15,8 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<form method="post">
|
||||
<form method="post"
|
||||
{% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="btn btn-primary" value="Update">
|
||||
{% bootstrap_form form %}
|
||||
|
|
|
@ -199,9 +199,8 @@ CSP_FRAME_ANCESTORS = ["'none'"]
|
|||
CSP_INCLUDE_NONCE_IN = ['script-src']
|
||||
|
||||
THUMBNAIL_KVSTORE = "sorl.thumbnail.kvstores.redis_kvstore.KVStore"
|
||||
THUMBNAIL_REDIS_URL = "unix:///var/run/redis-rpgnotes/redis-server.sock?db=1"
|
||||
|
||||
redis_url = "redis://127.0.0.1:6379/9" if DEBUG else "unix:///var/run/redis-rpgnotes/redis-server.sock?db=2"
|
||||
THUMBNAIL_REDIS_URL = redis_url.replace("?db=2", "?db=1")
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
|
@ -215,6 +214,8 @@ CACHES = {
|
|||
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||
SESSION_CACHE_ALIAS = "default"
|
||||
|
||||
THUMBNAIL_DEBUG = DEBUG
|
||||
|
||||
if not DEBUG:
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365
|
||||
|
|
|
@ -9,20 +9,26 @@
|
|||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{% url "campaigndetail" %}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url "characterlist" %}">Characters</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url "daylist" %}">Timeline</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url "lootlist" %}">Loot</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% with url_name=request.resolver_match.url_name %}
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if url_name == 'campaigndetail' %}active{% endif %}"
|
||||
href="{% url "campaigndetail" %}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if url_name == 'characterdetail' %}active{% endif %}"
|
||||
href="{% url "characterlist" %}">Characters</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if url_name == 'daydetail' %}active{% endif %}"
|
||||
href="{% url "daylist" %}">Timeline</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if url_name == 'lootlist' %}active{% endif %}"
|
||||
href="{% url "lootlist" %}">Loot</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endwith %}
|
||||
<span class="navbar-text">
|
||||
{{ user }}
|
||||
</span>
|
||||
|
|
16
utils/random_filename.py
Normal file
16
utils/random_filename.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
import secrets
|
||||
import string
|
||||
from pathlib import Path
|
||||
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
|
||||
|
||||
def random_string(letters: int = 32) -> str:
|
||||
return ''.join(secrets.choice(alphabet) for i in range(letters))
|
||||
|
||||
|
||||
def get_file_path(instance, filename: str) -> str:
|
||||
ext = Path(filename).suffix
|
||||
rand = random_string()
|
||||
letter = rand[0]
|
||||
return f"{letter}/{rand}{ext}"
|
Loading…
Reference in a new issue