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 %}
|
{% block content %}
|
||||||
<h1>{{ object }}</h1>
|
<h1>{{ object }}</h1>
|
||||||
|
<h2>Players</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for user in users %}
|
{% for user,characters in players.items %}
|
||||||
<li>{{ user }}</li>
|
<li>{{ user }}{% if characters %} ({{ characters|join:", " }}){% endif %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -45,7 +45,12 @@ class CampaignDetailView(generic.DetailView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CampaignDetailView, self).get_context_data(**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
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,4 +6,4 @@ from characters.models import Character
|
||||||
class CharacterForm(ModelForm):
|
class CharacterForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Character
|
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 locations.models import Location
|
||||||
from rpg_notes.settings import AUTH_USER_MODEL
|
from rpg_notes.settings import AUTH_USER_MODEL
|
||||||
from utils.colors import get_random_color, is_bright_color
|
from utils.colors import get_random_color, is_bright_color
|
||||||
|
from utils.random_filename import get_file_path
|
||||||
|
|
||||||
|
|
||||||
def validate_color_hex(value: str):
|
def validate_color_hex(value: str):
|
||||||
|
@ -18,14 +19,16 @@ def validate_color_hex(value: str):
|
||||||
|
|
||||||
class Character(BaseModel, DescriptionModel):
|
class Character(BaseModel, DescriptionModel):
|
||||||
subtitle = models.CharField(max_length=100, blank=True)
|
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)
|
# faction = models.ForeignKey(Faction, on_delete=models.PROTECT, blank=True, null=True)
|
||||||
location = models.ForeignKey(Location, 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=[
|
color = models.CharField(max_length=7, default=get_random_color, validators=[
|
||||||
MinLengthValidator(7),
|
MinLengthValidator(7),
|
||||||
validate_color_hex
|
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)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
last_modified = models.DateTimeField(auto_now=True)
|
last_modified = models.DateTimeField(auto_now=True)
|
||||||
|
@ -42,3 +45,13 @@ class Character(BaseModel, DescriptionModel):
|
||||||
|
|
||||||
def text_color(self):
|
def text_color(self):
|
||||||
return "black" if is_bright_color(self.color) else "white"
|
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>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<div class="character-heading" style="border-bottom-color: {{ character.color }}">
|
<div class="character-heading" style="border-bottom-color: {{ character.color }}">
|
||||||
{% if character.image %}
|
{% if character.larger_image %}
|
||||||
{% thumbnail character.image "150x150" crop="center" as im %}
|
{% thumbnail character.larger_image "150x150" crop="center" as im %}
|
||||||
<a href="{{ character.image.url }}" target="_blank">
|
<a href="{{ character.larger_image.url }}" target="_blank">
|
||||||
<img class="avatar avatar-image avatar-large rounded-circle" src="{{ im.url }}" width="{{ im.width }}"
|
<img class="avatar avatar-image avatar-large rounded-circle" src="{{ im.url }}" width="{{ im.width }}"
|
||||||
height="{{ im.height }}"
|
height="{{ im.height }}"
|
||||||
srcset="{{ im.url|srcset }}">
|
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 %}
|
{% block content %}
|
||||||
|
|
||||||
<form method="post">
|
<form method="post"
|
||||||
|
{% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="submit" class="btn btn-primary" value="Update">
|
<input type="submit" class="btn btn-primary" value="Update">
|
||||||
{% bootstrap_form form %}
|
{% bootstrap_form form %}
|
||||||
|
|
|
@ -199,9 +199,8 @@ CSP_FRAME_ANCESTORS = ["'none'"]
|
||||||
CSP_INCLUDE_NONCE_IN = ['script-src']
|
CSP_INCLUDE_NONCE_IN = ['script-src']
|
||||||
|
|
||||||
THUMBNAIL_KVSTORE = "sorl.thumbnail.kvstores.redis_kvstore.KVStore"
|
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"
|
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 = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
@ -215,6 +214,8 @@ CACHES = {
|
||||||
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||||
SESSION_CACHE_ALIAS = "default"
|
SESSION_CACHE_ALIAS = "default"
|
||||||
|
|
||||||
|
THUMBNAIL_DEBUG = DEBUG
|
||||||
|
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
SECURE_SSL_REDIRECT = True
|
SECURE_SSL_REDIRECT = True
|
||||||
SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365
|
SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365
|
||||||
|
|
|
@ -9,20 +9,26 @@
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav me-auto">
|
{% with url_name=request.resolver_match.url_name %}
|
||||||
<li class="nav-item">
|
<ul class="navbar-nav me-auto">
|
||||||
<a class="nav-link active" href="{% url "campaigndetail" %}">Home</a>
|
<li class="nav-item">
|
||||||
</li>
|
<a class="nav-link {% if url_name == 'campaigndetail' %}active{% endif %}"
|
||||||
<li class="nav-item">
|
href="{% url "campaigndetail" %}">Home</a>
|
||||||
<a class="nav-link" href="{% url "characterlist" %}">Characters</a>
|
</li>
|
||||||
</li>
|
<li class="nav-item">
|
||||||
<li class="nav-item">
|
<a class="nav-link {% if url_name == 'characterdetail' %}active{% endif %}"
|
||||||
<a class="nav-link" href="{% url "daylist" %}">Timeline</a>
|
href="{% url "characterlist" %}">Characters</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url "lootlist" %}">Loot</a>
|
<a class="nav-link {% if url_name == 'daydetail' %}active{% endif %}"
|
||||||
</li>
|
href="{% url "daylist" %}">Timeline</a>
|
||||||
</ul>
|
</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">
|
<span class="navbar-text">
|
||||||
{{ user }}
|
{{ user }}
|
||||||
</span>
|
</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