Compare commits

8 Commits

40 changed files with 491 additions and 598 deletions

View File

@@ -30,11 +30,6 @@ docker-compose up
### Command Befehle ### Command Befehle
Erstellt die fehlenden Thumbs für die Alben in der Galerie:
<code>
python3 fet2020/manage.py create_thumbs
</code>
Erstellt alle Searchindexes neu: Erstellt alle Searchindexes neu:
<code> <code>
python3 fet2020/manage.py rebuild_index python3 fet2020/manage.py rebuild_index

View File

@@ -74,6 +74,18 @@ def get_app_list(self, request, app_label=None):
} }
app["models"].sort(key=lambda x: ordering[x["name"]]) app["models"].sort(key=lambda x: ordering[x["name"]])
elif app["app_label"] == "finance":
ordering = {
"Rechnungen": 1,
"Wiref Formulare": 2,
"Beschlüsse": 3,
"Bankdaten": 4,
}
app["models"].sort(key=lambda x: ordering[x["name"]])
else:
app["models"].sort(key=lambda x: x["name"])
return app_list return app_list

View File

@@ -22,7 +22,7 @@ def get_ep_client():
api_key = f.read() api_key = f.read()
api_key = api_key.rstrip() api_key = api_key.rstrip()
if api_key == "": if not api_key:
logger.info("API Key is missing. Path: %s", api_key_path) logger.info("API Key is missing. Path: %s", api_key_path)
return None return None
@@ -56,97 +56,114 @@ def get_ep_group(ep_group_name: str = "fet") -> dict[str, str]:
return ep_group return ep_group
def ep_pad_exists(pad_id: str | None = None) -> bool | None: def ep_pad_exists(pad_id: str = "") -> bool:
"""Check if pad exists. """Check if pad exists.
Parameters Parameters
---------- ----------
pad_id : str | None pad_id : str
Id of pad that is checked if it exists. Id of pad that is checked if it exists.
Returns Returns
------- -------
bool | None bool
- True: Pad exists. - True: Pad exists.
- False: Pad doesn't exist. - False: Pad not found.
- None: There is no pad id or no connection to etherpad server. - None: There is no pad id or no connection to etherpad server.
""" """
if pad_id is None: if not pad_id:
return None return None
if (ep_c := get_ep_client()) is None: if not (ep_c := get_ep_client()):
return None return None
if (ep_group := get_ep_group()) is None: if not (ep_group := get_ep_group()):
return None return None
lists = ep_c.listPads(groupID=ep_group["groupID"]) lists = ep_c.listPads(groupID=ep_group["groupID"])
if any(pad_id in s for s in lists["padIDs"]): if any(pad_id in s for s in lists["padIDs"]):
logger.info("Etherpad exists. Pad: %s", pad_id) logger.info("Etherpad '%s' exists.", pad_id)
return True return True
logger.info("Etherpad doesn't exist. Pad: %s", pad_id) logger.info("Etherpad '%s' not found.", pad_id)
return False return False
def ep_create_new_pad(pad_id: str | None, text="helloworld"): def ep_create_new_pad(pad_id: str, text="helloworld") -> str:
"""Create a new pad if it doesn't exist.
Parameters
----------
pad_id : str
Id of the new pad to be created.
text : str
Text for new pad.
Returns
-------
str
Id of the new pad.
- None: No pad is created.
""" """
Create a pad if it doesn't exist. if not pad_id:
Return a pad_id if new pad is created. Otherwise None (when the padID exists already).
"""
if pad_id is None:
return None return None
if (ep_c := get_ep_client()) is None: if not (ep_c := get_ep_client()):
return None return None
if (ep_group := get_ep_group()) is None: if not (ep_group := get_ep_group()):
return None
if ep_pad_exists(pad_id) is False:
ep_c.createGroupPad(groupID=ep_group["groupID"], padName=pad_id, text=text)
logger.info("Create new etherpad. Pad: %s", pad_id)
return pad_id
return None
def ep_get_html(pad_id: str | None) -> str | None:
if (ep_c := get_ep_client()) is None:
return None
if (ep_group := get_ep_group()) is None:
return None return None
if ep_pad_exists(pad_id): if ep_pad_exists(pad_id):
return ep_c.getHTML(padID=ep_group["groupID"] + "$" + pad_id)["html"] logger.info("Can't create new etherpad '%s' because it already exists.", pad_id)
return ""
def ep_set_html(pad_id: str | None, html: str):
if (ep_c := get_ep_client()) is None:
return None return None
if (ep_group := get_ep_group()) is None: ep_c.createGroupPad(groupID=ep_group["groupID"], padName=pad_id, text=text)
logger.info("Create new etherpad '%s'.", pad_id)
return pad_id
def ep_get_html(pad_id: str) -> str:
if not pad_id:
return None
if not (ep_c := get_ep_client()):
return None return None
if ep_pad_exists(pad_id): if not (ep_group := get_ep_group()):
ep_c.setHTML(padID=ep_group["groupID"] + "$" + pad_id, html=html)
return True
return None
def ep_get_url(pad_id: str | None):
if (ep_group := get_ep_group()) is None:
return None return None
if ep_pad_exists(pad_id): if not ep_pad_exists(pad_id):
return urljoin( return None
settings.ETHERPAD_CLIENT["exturl"],
"p/" + ep_group["groupID"] + "$" + str(pad_id), return ep_c.getHTML(padID=ep_group["groupID"] + "$" + pad_id)["html"]
)
return "#"
def ep_set_html(pad_id: str, html: str) -> bool:
if not pad_id:
return None
if not (ep_c := get_ep_client()):
return None
if not (ep_group := get_ep_group()):
return None
if not ep_pad_exists(pad_id):
return None
ep_c.setHTML(padID=ep_group["groupID"] + "$" + pad_id, html=html)
return True
def ep_get_url(pad_id: str):
if not pad_id:
return None
if not (ep_group := get_ep_group()):
return None
if not ep_pad_exists(pad_id):
return None
return urljoin(settings.ETHERPAD_CLIENT["exturl"], "p/" + ep_group["groupID"] + "$" + str(pad_id))

View File

@@ -10,11 +10,11 @@ from .api import get_ep_client, get_ep_group
@ep_authenticated_user @ep_authenticated_user
def _create_ep_session(request, expires): def _create_ep_session(request, expires):
if (ep_c := get_ep_client()) is None: if not (ep_c := get_ep_client()):
return None return None
if (ep_group := get_ep_group()) is None: if not (ep_group := get_ep_group()):
return None, None return None
author = ep_c.createAuthorIfNotExistsFor( author = ep_c.createAuthorIfNotExistsFor(
name=str(request.user), name=str(request.user),

View File

@@ -394,6 +394,8 @@ TAGGIT_FORCE_LOWERCASE = True
# THUMBNAIL # THUMBNAIL
THUMBNAIL_DEFAULT_STORAGE_ALIAS = "default"
THUMBNAIL_ALIASES = { THUMBNAIL_ALIASES = {
"": { "": {
"avatar": {"size": (50, 50), "crop": True}, "avatar": {"size": (50, 50), "crop": True},

View File

@@ -6,7 +6,7 @@ from posts.models import Event, FetMeeting, Post
def index(request): def index(request):
posts = Post.articles.date_sorted_list() posts = Post.articles.date_sorted()
# A maximum of 5 posts should be displayed on the startpage. # A maximum of 5 posts should be displayed on the startpage.
post_count = 5 post_count = 5

View File

@@ -346,28 +346,28 @@ class BillAdmin(admin.ModelAdmin):
status=obj.get_status_display(), status=obj.get_status_display(),
) )
@admin.action(description="Als 'Abgerechnet' markieren.") @admin.action(description="Als 'Für Überweisung freigegeben' markieren.")
def make_cleared(self, request, queryset): def make_cleared(self, request, queryset):
updated = queryset.update(status=Bill.Status.CLEARED) updated = queryset.update(status=Bill.Status.CLEARED)
self.message_user( self.message_user(
request, request,
ngettext( ngettext(
"%d Rechnung wurde als 'Abgerechnet' markiert.", "%d Rechnung wurde als 'Für Überweisung freigegeben' markiert.",
"%d Rechnungen wurden als 'Abgerechnet' markiert.", "%d Rechnungen wurden als 'Für Überweisung freigegeben' markiert.",
updated, updated,
) )
% updated, % updated,
messages.SUCCESS, messages.SUCCESS,
) )
@admin.action(description="Als 'Abgeschlossen' markieren.") @admin.action(description="Als 'Abgeschlossen / Überwiesen' markieren.")
def make_finished(self, request, queryset): def make_finished(self, request, queryset):
updated = queryset.update(status=Bill.Status.FINISHED) updated = queryset.update(status=Bill.Status.FINISHED)
self.message_user( self.message_user(
request, request,
ngettext( ngettext(
"%d Rechnung wurde als 'Abgeschlossen' markiert.", "%d Rechnung wurde als 'Abgeschlossen / Überwiesen' markiert.",
"%d Rechnungen wurden als 'Abgeschlossen' markiert.", "%d Rechnungen wurden als 'Abgeschlossen / Überwiesen' markiert.",
updated, updated,
) )
% updated, % updated,

View File

@@ -439,21 +439,19 @@ class ResolutionAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # to get the self.fields set super().__init__(*args, **kwargs) # to get the self.fields set
resolution = kwargs.get("instance") budget = 0.0
bills = Bill.objects.filter(resolution=resolution) total = 0.0
total = 0 if (resolution := kwargs.get("instance")) is not None:
for elem in bills: for elem in Bill.objects.filter(resolution=resolution):
total += elem.amount total += elem.amount
budget = resolution.budget
self.fields["total"].disabled = True self.fields["total"].disabled = True
self.fields["total"].initial = total self.fields["total"].initial = total
self.fields["total"].label = "Gesamtsumme (EUR)" self.fields["total"].label = "Gesamtsumme (EUR)"
self.fields["total"].required = False self.fields["total"].required = False
budget = 0
if resolution:
budget = resolution.budget
self.fields["budget_remaining"].disabled = True self.fields["budget_remaining"].disabled = True
self.fields["budget_remaining"].initial = budget - total self.fields["budget_remaining"].initial = budget - total
self.fields["budget_remaining"].label = "Restbudget (EUR)" self.fields["budget_remaining"].label = "Restbudget (EUR)"

View File

@@ -154,7 +154,7 @@ class Bill(models.Model):
class Affiliation(models.TextChoices): class Affiliation(models.TextChoices):
VEREIN = "V", "Vereinsbudget" VEREIN = "V", "Vereinsbudget"
OFFICIAL = "B", "Offizielles Budget" OFFICIAL = "B", "Wiref-Budget"
REPRESENTATION = "R", "Bundesvertretung" REPRESENTATION = "R", "Bundesvertretung"
affiliation = models.CharField( affiliation = models.CharField(
@@ -186,9 +186,9 @@ class Bill(models.Model):
class Status(models.TextChoices): class Status(models.TextChoices):
SUBMITTED = "S", "Eingereicht" SUBMITTED = "S", "Eingereicht"
INCOMPLETED = "I", "Unvollständig" INCOMPLETED = "I", "Unvollständig / Abgelehnt"
CLEARED = "C", "Abgerechnet" CLEARED = "C", "Für Überweisung freigegeben"
FINISHED = "F", "Abgeschlossen" FINISHED = "F", "Abgeschlossen / Überwiesen"
status = models.CharField( status = models.CharField(
max_length=1, max_length=1,

View File

@@ -15,11 +15,7 @@ class AlbumAdmin(admin.ModelAdmin):
"Der Ordner für die Fotos liegt am Server unter '/mnt/save/fotos/www'. " "Der Ordner für die Fotos liegt am Server unter '/mnt/save/fotos/www'. "
"Fette Schriften sind Pflichtfelder." "Fette Schriften sind Pflichtfelder."
) )
return super().add_view( return super().add_view(request, form_url, extra_context=extra_context)
request,
form_url,
extra_context=extra_context,
)
def change_view(self, request, object_id, form_url="", extra_context=None): def change_view(self, request, object_id, form_url="", extra_context=None):
extra_context = extra_context or {} extra_context = extra_context or {}
@@ -27,12 +23,7 @@ class AlbumAdmin(admin.ModelAdmin):
"Der Ordner für die Fotos liegt am Server unter '/mnt/save/fotos/www'. " "Der Ordner für die Fotos liegt am Server unter '/mnt/save/fotos/www'. "
"Fette Schriften sind Pflichtfelder." "Fette Schriften sind Pflichtfelder."
) )
return super().change_view( return super().change_view(request, object_id, form_url, extra_context=extra_context)
request,
object_id,
form_url,
extra_context=extra_context,
)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
obj.author = request.user obj.author = request.user

View File

@@ -0,0 +1,6 @@
from django.db import models
class Status(models.TextChoices):
DRAFT = "10", "DRAFT"
PUBLIC = "20", "PUBLIC"

View File

@@ -11,11 +11,7 @@ class AlbumAdminForm(forms.ModelForm):
widgets = {"description": CKEditorUploadingWidget(config_name="default")} widgets = {"description": CKEditorUploadingWidget(config_name="default")}
labels = { labels = {"slug": "Permalink", "event_place": "Event Ort", "description": "Beschreibung"}
"slug": "Permalink",
"event_place": "Event Ort",
"description": "Beschreibung",
}
help_texts = { help_texts = {
"folder_name": "Füge den Ordnername (ohne Pfade) ein.", "folder_name": "Füge den Ordnername (ohne Pfade) ein.",

View File

@@ -1,16 +0,0 @@
import logging
from django.core.management.base import BaseCommand
from gallery.utils import create_thumbs, get_folder_list
logger = logging.getLogger(__name__)
class Command(BaseCommand):
def handle(self, *args, **options):
for folder in get_folder_list():
logger.info("Folder '%s' in process.", folder)
create_thumbs(folder)
logger.info("Command 'create thumbs' ended.")

View File

@@ -0,0 +1,11 @@
from django.db import models
from .choices import Status
class AlbumManager(models.Manager):
def get_queryset(self):
return super().get_queryset().order_by("-event_date")
def public(self):
return self.get_queryset().filter(status=Status.PUBLIC)

View File

@@ -1,45 +1,44 @@
import logging
from random import randint
from django.conf import settings
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
from .choices import Status
from .managers import AlbumManager
from .utils import get_image_list from .utils import get_image_list
logger = logging.getLogger(__name__)
class Album(models.Model): class Album(models.Model):
title = models.CharField(verbose_name="Titel", max_length=200) title = models.CharField(verbose_name="Titel", max_length=200)
slug = models.SlugField(unique=True, null=True, blank=True) slug = models.SlugField(unique=True, null=True, blank=True)
folder_name = models.CharField(verbose_name="Ordner Name", max_length=200) folder_name = models.CharField(verbose_name="Ordner Name", max_length=200)
thumbnail = models.CharField(verbose_name="Thumbnail", max_length=200, null=True, blank=True) thumbnail = models.CharField(verbose_name="Thumbnail", max_length=200, blank=True, default="")
event_date = models.DateField( event_date = models.DateField(
verbose_name="Event Datum", verbose_name="Event Datum", null=True, blank=True, default=timezone.now
null=True,
blank=True,
default=timezone.now,
) )
event_place = models.CharField(max_length=200, blank=True) event_place = models.CharField(max_length=200, blank=True)
photographer = models.CharField( photographer = models.CharField(
verbose_name="Fotograph(en)", verbose_name="Fotograph(en)", max_length=200, blank=True, default=""
max_length=200,
null=True,
blank=True,
) )
DRAFT = "10" # TextChoices
PUBLIC = "20" Status = Status
STATUS = (
(DRAFT, "DRAFT"),
(PUBLIC, "PUBLIC"),
)
status = models.CharField(max_length=2, choices=STATUS, default=DRAFT)
description = models.TextField(null=True, blank=True) status = models.CharField(max_length=2, choices=Status.choices, default=Status.DRAFT)
description = models.TextField(blank=True, default="")
# Managers # Managers
objects = models.Manager() objects = AlbumManager()
class Meta: class Meta:
verbose_name = "Album" verbose_name = "Album"
@@ -57,14 +56,32 @@ class Album(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("gallery:album", kwargs={"slug": self.slug}) return reverse("gallery:album", kwargs={"slug": self.slug})
def get_images(self): def clean(self):
if not self.images:
logger.info("No thumbnails are generated.")
@property
def images(self) -> list:
return get_image_list(self.folder_name) return get_image_list(self.folder_name)
def get_images_length_sub_3(self): def get_images_length_sub_3(self):
return len(self.get_images()) - 3 return len(self.images) - 3
def get_model_name(self): def get_model_name(self):
return self._meta.model_name return self._meta.model_name
def get_thumbnail(self): @property
return None def get_album_thumbnail(self):
if img_list := self.images:
return next(
(
img["thumb_url"]
for img in img_list
if self.thumbnail and self.thumbnail in img["title"]
),
img_list[randint(0, len(img_list) - 1)]["thumb_url"],
)
self.status = Album.Status.DRAFT
logger.info("Album '%s' is empty.", self.title)
return settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"

View File

@@ -1,11 +1,12 @@
from django.urls import path from django.urls import path
from . import apps, views from . import apps, views
from .views import AlbumDetailView, DraftAlbumDetailView
app_name = apps.GalleryConfig.name app_name = apps.GalleryConfig.name
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
path("<slug:slug>/", views.show_album, name="album"), path("<slug:slug>/", AlbumDetailView.as_view(), name="album"),
path("draft/<slug:slug>/", views.show_draft_album, name="album_draft"), path("draft/<slug:slug>/", DraftAlbumDetailView.as_view(), name="album_draft"),
] ]

View File

@@ -1,80 +1,56 @@
import logging import logging
import os import os
from pathlib import Path
from django.conf import settings from django.conf import settings
from django.core.validators import get_available_image_extensions
from PIL import Image, ImageOps from PIL import Image, ImageOps
gallery_path = settings.GALLERY["path"] gallery_path = Path(settings.MEDIA_ROOT) / settings.GALLERY["path"]
gallery_thumb_path = settings.GALLERY["thumb_path"] gallery_path_url = Path(settings.MEDIA_URL) / settings.GALLERY["path"]
gallery_thumb_path = Path(settings.MEDIA_ROOT) / settings.GALLERY["thumb_path"]
gallery_thumb_path_url = Path(settings.MEDIA_URL) / settings.GALLERY["thumb_path"]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
size = (320, 320) size = (320, 320)
valid_images = [".jpg", ".png"]
def get_image_list(folder_name): def get_image_list(folder_name: str) -> list:
file_path = os.path.join(settings.MEDIA_ROOT + gallery_path, folder_name) image_path = Path(gallery_path) / folder_name
thumb_path = Path(gallery_thumb_path) / folder_name
img_list = [] img_list = []
if os.path.exists(file_path): if not Path(image_path).exists():
for img in os.listdir(file_path): logger.info("Image path '%s' not found.", image_path)
ext = os.path.splitext(img)[1] return img_list
if ext.lower() not in valid_images:
continue
thumb_path = os.path.join(settings.MEDIA_ROOT + gallery_thumb_path, folder_name) Path(thumb_path).mkdir(exist_ok=True)
thumb_file_path = os.path.join(thumb_path, f"thumb_{img}")
if os.path.exists(thumb_file_path):
thumb_url = os.path.join(
settings.MEDIA_URL + gallery_thumb_path,
folder_name + "/" + f"thumb_{img}",
)
else:
thumb_url = None
img_dict = { for _file in os.listdir(image_path):
"title": img, if Path(_file).suffix.lower()[1:] not in get_available_image_extensions():
"image_url": os.path.join( continue
settings.MEDIA_URL + gallery_path,
folder_name + "/" + img,
),
"thumb_url": thumb_url,
}
img_list.append(img_dict) thumb_file_path = Path(thumb_path) / f"thumb_{_file}"
if not Path(thumb_file_path).exists():
with Image.open(Path(image_path) / _file, "r") as im:
if im._getexif() is not None:
im = ImageOps.exif_transpose(im)
thumb = ImageOps.fit(im, size, Image.Resampling.LANCZOS)
thumb.save(thumb_file_path)
logger.info("Save thumb 'thumb_%s'.", _file)
img_dict = {
"title": _file,
"image_url": Path(gallery_path_url) / folder_name / _file,
"thumb_url": Path(gallery_thumb_path_url) / folder_name / f"thumb_{_file}",
}
img_list.append(img_dict)
return img_list return img_list
def get_folder_list(): def get_folder_list():
if os.path.exists(settings.MEDIA_ROOT + gallery_path): if Path(gallery_path).exists():
return next(os.walk(settings.MEDIA_ROOT + gallery_path))[1] return next(os.walk(gallery_path))[1]
return None return None
def create_thumbs(folder_path):
file_path = os.path.join(settings.MEDIA_ROOT + gallery_path, folder_path)
thumb_path = os.path.join(settings.MEDIA_ROOT + gallery_thumb_path, folder_path)
if os.path.exists(file_path):
os.makedirs(thumb_path, exist_ok=True)
for f in os.listdir(file_path):
ext = os.path.splitext(f)[1]
if ext.lower() not in valid_images:
continue
thumb_file_path = os.path.join(thumb_path, f"thumb_{f}")
if os.path.exists(thumb_file_path):
continue
image_path = os.path.join(file_path, f)
logger.info("Edit picture '%s'.", f)
with Image.open(str(image_path), "r") as image:
if image._getexif() is not None:
image = ImageOps.exif_transpose(image)
thumb = ImageOps.fit(image, size, Image.ANTIALIAS)
thumb.save(thumb_file_path)
logger.info("Save thumb 'thumb_%s'.", f)

View File

@@ -1,94 +1,65 @@
import logging
from collections import deque from collections import deque
from random import randint
from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404 from django.http import Http404
from django.shortcuts import render from django.shortcuts import render
from django.utils.text import slugify from django.utils.text import slugify
from django.views.generic.detail import DetailView
from authentications.decorators import authenticated_user
from .models import Album from .models import Album
from .utils import create_thumbs, get_folder_list from .utils import get_folder_list
logger = logging.getLogger(__name__)
def index(request): def index(request):
if request.user.is_authenticated: if request.user.is_authenticated:
albums = deque(Album.objects.all().order_by("-event_date")) albums = deque(Album.objects.all())
# Get albums that are in the server but not in the db.
for folder in get_folder_list():
if not Album.objects.filter(folder_name=folder):
albums.append(
Album(title=folder, slug=slugify(folder), folder_name=folder, event_date=None)
)
# get albums that are in the server but not in the db.
folders = get_folder_list()
if folders:
for folder in folders:
if not Album.objects.filter(folder_name=folder):
albums.append(
Album(
title=folder,
slug=slugify(folder),
folder_name=folder,
event_date=None,
),
)
else: else:
# show only PUBLIC albums. # Show only PUBLIC albums.
albums = deque(Album.objects.filter(status=Album.PUBLIC).order_by("-event_date")) albums = Album.objects.public()
for album in list(albums): context = {"albums": albums}
img_list = album.get_images()
if img_list:
if album.thumbnail:
for img in img_list:
if album.thumbnail in img["title"]:
album.thumbnail = img["thumb_url"]
break
else:
value = randint(0, len(img_list) - 1)
album.thumbnail = img_list[value]["thumb_url"]
else:
value = randint(0, len(img_list) - 1)
album.thumbnail = img_list[value]["thumb_url"]
else:
# empty album is temporarily set to DRAFT.
album.thumbnail = settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
album.status = Album.DRAFT
context = {
"albums": albums,
}
return render(request, "gallery/index.html", context) return render(request, "gallery/index.html", context)
def show_album(request, slug): class AlbumDetailView(DetailView):
album = Album.objects.filter(slug=slug).first() model = Album
if not album: template_name = "gallery/album.html"
for folder in get_folder_list():
if slug == slugify(folder):
album = Album(
title=folder,
slug=slugify(folder),
folder_name=folder,
event_date=None,
)
break
else:
raise Http404("wrong album slug")
img_list = album.get_images() def get_queryset(self):
if not img_list: return (
# empty album is temporarily set to DRAFT. Album.objects.public()
album.status = Album.DRAFT if not self.request.user.is_authenticated
else Album.objects.all()
create_thumbs(album.folder_name) )
context = {
"album": album,
"images": img_list,
}
return render(request, "gallery/album.html", context)
@authenticated_user class DraftAlbumDetailView(LoginRequiredMixin, DetailView):
def show_draft_album(request, slug): model = Album
return show_album(request, slug) template_name = "gallery/album.html"
def get_object(self, queryset=None):
slug = self.kwargs.get(self.slug_url_kwarg)
if not (album := Album.objects.filter(slug=slug).first()):
for folder in get_folder_list():
if slug == slugify(folder):
album = Album(
title=folder, slug=slugify(folder), folder_name=folder, event_date=None
)
break
else:
raise Http404("Album slug not found.")
return album

View File

@@ -36,7 +36,7 @@ class JobOverviewInline(JobMemberInline):
verbose_name_plural = "Tätigkeitsübersicht" verbose_name_plural = "Tätigkeitsübersicht"
def get_queryset(self, request): def get_queryset(self, request):
return JobMember.members.get_all_jobs_sorted() return JobMember.members.get_all_jobs()
class ActiveMemberInline(JobMemberInline): class ActiveMemberInline(JobMemberInline):
@@ -81,11 +81,8 @@ class MemberAdmin(admin.ModelAdmin):
"nickname", "nickname",
"mailaccount", "mailaccount",
"role", "role",
"description",
"image", "image",
"birthday", "description",
"phone",
"address",
), ),
}, },
), ),

View File

@@ -24,8 +24,9 @@ class ActiveMemberForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
member_qs = self.fields["member"].queryset.filter(role="A").order_by("firstname", "surname") self.fields["member"].queryset = (
self.fields["member"].queryset = member_qs self.fields["member"].queryset.filter(role="A").order_by("firstname", "surname")
)
class InactiveMemberForm(forms.ModelForm): class InactiveMemberForm(forms.ModelForm):
@@ -35,8 +36,9 @@ class InactiveMemberForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
member_qs = self.fields["member"].queryset.order_by("firstname", "surname") self.fields["member"].queryset = self.fields["member"].queryset.order_by(
self.fields["member"].queryset = member_qs "firstname", "surname"
)
class MemberForm(forms.ModelForm): class MemberForm(forms.ModelForm):
@@ -54,9 +56,6 @@ class MemberForm(forms.ModelForm):
labels = { labels = {
"description": "Beschreibung zu der Person", "description": "Beschreibung zu der Person",
"image": "Porträt", "image": "Porträt",
"birthday": "Geburtstag",
"phone": "Telefonnummer",
"address": "Wohnadresse",
} }
widgets = {"description": CKEditorUploadingWidget(config_name="default")} widgets = {"description": CKEditorUploadingWidget(config_name="default")}

View File

@@ -17,16 +17,13 @@ class ActiveJobMemberManager(models.Manager):
def get_queryset(self): def get_queryset(self):
date_today = timezone.now().date() date_today = timezone.now().date()
qs = ( return (
super() super()
.get_queryset() .get_queryset()
.filter(Q(member__role="A") & (Q(job_end__gt=date_today) | Q(job_end__isnull=True)))
.order_by("job_role", "member__firstname", "member__surname", "job_start") .order_by("job_role", "member__firstname", "member__surname", "job_start")
) )
return qs.filter(
Q(member__role="A") & (Q(job_end__gt=date_today) | Q(job_end__isnull=True)),
)
class InactiveJobMemberManager(models.Manager): class InactiveJobMemberManager(models.Manager):
"""return a list of inactive member.""" """return a list of inactive member."""
@@ -40,39 +37,42 @@ class InactiveJobMemberManager(models.Manager):
def get_queryset(self): def get_queryset(self):
date_today = timezone.now().date() date_today = timezone.now().date()
qs = super().get_queryset().order_by("member__firstname", "member__surname", "-job_start") return (
super()
return qs.filter( .get_queryset()
Q(member__role="P") .filter(
| (Q(job_end__lt=date_today + timedelta(days=1)) & Q(job_end__isnull=False)), Q(member__role="P")
| (Q(job_end__lt=date_today + timedelta(days=1)) & Q(job_end__isnull=False))
)
.order_by("member__firstname", "member__surname", "-job_start")
) )
class JobMemberManager(models.Manager): class JobMemberManager(models.Manager):
def get_members(self, role): def get_all_jobs(self):
qs = self.get_queryset().order_by("member__firstname") return self.get_queryset().order_by(
F("job_end").desc(nulls_first=True), "-job_start", "job__name"
return qs.filter(Q(member__role=role))
def get_all_jobs_sorted(self):
qs = self.get_queryset().order_by(
F("job_end").desc(nulls_first=True),
"-job_start",
"job__name",
) )
return qs
def get_active_jobs(self, member_id): def get_active_jobs(self, member_id):
date_today = timezone.now().date() date_today = timezone.now().date()
qs = self.get_queryset().filter(member__id=member_id).order_by("-job_start", "job__name") return (
self.get_queryset()
return qs.filter(Q(job_end__gt=date_today) | Q(job_end__isnull=True)) .filter(Q(member__id=member_id) & (Q(job_end__gt=date_today) | Q(job_end__isnull=True)))
.order_by("-job_start", "job__name")
)
def get_inactive_jobs(self, member_id): def get_inactive_jobs(self, member_id):
date_today = timezone.now().date() date_today = timezone.now().date()
qs = self.get_queryset().filter(member__id=member_id).order_by("-job_start", "job__name") return (
self.get_queryset()
return qs.filter(Q(job_end__lt=date_today + timedelta(days=1)) & Q(job_end__isnull=False)) .filter(
Q(member__id=member_id)
& Q(job_end__lt=date_today + timedelta(days=1))
& Q(job_end__isnull=False)
)
.order_by("-job_start", "job__name")
)
class MemberManager(models.Manager): class MemberManager(models.Manager):

View File

@@ -26,7 +26,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='member', model_name='member',
name='phone', name='phone',
field=models.CharField(blank=True, max_length=17, validators=[members.validators.PhoneNumberValidator()]), field=models.CharField(blank=True, max_length=17),
), ),
migrations.AlterField( migrations.AlterField(
model_name='member', model_name='member',

View File

@@ -16,12 +16,12 @@ from .managers import (
MemberManager, MemberManager,
) )
from .validators import ( from .validators import (
PhoneNumberValidator,
validate_domainonly_email, validate_domainonly_email,
validate_file_size, validate_file_size,
validate_image_dimension, validate_image_dimension,
) )
fet_logo_url = settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -55,23 +55,13 @@ class Member(models.Model):
default=MemberRole.ACTIVE, default=MemberRole.ACTIVE,
) )
description = models.TextField(null=True, blank=True) description = models.TextField(blank=True, default="")
image = ThumbnailerImageField( image = ThumbnailerImageField(
upload_to="uploads/members/image/", upload_to="uploads/members/image/",
validators=[validate_file_size, validate_image_dimension], validators=[validate_file_size, validate_image_dimension],
) )
birthday = models.DateField(null=True, blank=True)
phone = models.CharField(
max_length=17,
blank=True,
validators=[PhoneNumberValidator()],
)
address = models.TextField(null=True, blank=True)
date_modified = models.DateTimeField(auto_now=True) date_modified = models.DateTimeField(auto_now=True)
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
@@ -88,7 +78,7 @@ class Member(models.Model):
# need to have 'View on site' link in admin app # need to have 'View on site' link in admin app
def get_absolute_url(self): def get_absolute_url(self):
return reverse("members:member", kwargs={"member_id": self.id}) return reverse("members:member", kwargs={"pk": self.pk})
def clean(self): def clean(self):
if not self.image: if not self.image:
@@ -98,7 +88,7 @@ class Member(models.Model):
try: try:
user = User.objects.get(username=self.username.lower()) user = User.objects.get(username=self.username.lower())
except User.DoesNotExist as e: except User.DoesNotExist as e:
logger.info("Username does not exist. Error: %s", e) logger.info("Username not found. Error: %s", e)
else: else:
user.first_name = self.firstname user.first_name = self.firstname
user.save() user.save()
@@ -108,35 +98,19 @@ class Member(models.Model):
@property @property
def image_url(self): def image_url(self):
if self.image: return self.image.url if self.image else fet_logo_url
return self.image.url
# return default image
return settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
@property @property
def avatar_url(self): def avatar_url(self):
if self.image: return self.image["avatar"].url if self.image else fet_logo_url
return self.image["avatar"].url
# return default image
return settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
@property @property
def portrait_url(self): def portrait_url(self):
if self.image: return self.image["portrait"].url if self.image else fet_logo_url
return self.image["portrait"].url
# return default image
return settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
@property @property
def thumb_url(self): def thumb_url(self):
if self.image: return self.image["thumb"].url if self.image else fet_logo_url
return self.image["thumb"].url
# return default image
return settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
class JobGroup(models.Model): class JobGroup(models.Model):
@@ -145,7 +119,7 @@ class JobGroup(models.Model):
shortterm = models.CharField(max_length=128, unique=True, blank=True) shortterm = models.CharField(max_length=128, unique=True, blank=True)
slug = models.SlugField(unique=True, null=True, blank=True) slug = models.SlugField(unique=True, null=True, blank=True)
description = models.TextField(null=True, blank=True) description = models.TextField(blank=True, default="")
# Managers # Managers
objects = models.Manager() objects = models.Manager()

View File

@@ -1,12 +1,13 @@
from django.urls import path from django.urls import path
from . import apps, views from . import apps
from .views import ActiveMemberListView, JobListView, MemberDetailView, MemberListView
app_name = apps.MembersConfig.name app_name = apps.MembersConfig.name
urlpatterns = [ urlpatterns = [
path("members/", views.index, name="index"), path("members/", ActiveMemberListView.as_view(), name="index"),
path("members/<str:role>/", views.members, name="members"), path("members/<str:role>/", MemberListView.as_view(), name="members"),
path("member/<int:member_id>/", views.profile, name="member"), path("member/<int:pk>/", MemberDetailView.as_view(), name="member"),
path("section/<slug:slug>/", views.jobs, name="jobs"), path("section/<slug:slug>/", JobListView.as_view(), name="jobs"),
] ]

View File

@@ -1,14 +1,4 @@
from django.core.validators import RegexValidator, ValidationError from django.core.validators import ValidationError
from django.utils.deconstruct import deconstructible
@deconstructible
class PhoneNumberValidator(RegexValidator):
regex = r"^\+?1?\d{9,15}$"
message = (
"Telefonnummer muss in diesem Format +999999999999 eingegeben werden. Bis zu 15 Zahlen "
"sind erlaubt."
)
def validate_domainonly_email(value): def validate_domainonly_email(value):

View File

@@ -1,74 +1,57 @@
import logging
from django.db.models import F from django.db.models import F
from django.http import Http404 from django.views.generic import ListView
from django.shortcuts import render from django.views.generic.detail import DetailView
from .models import JobGroup, JobMember, Member from .models import JobGroup, JobMember, Member
logger = logging.getLogger(__name__)
class ActiveMemberListView(ListView):
allow_empty = False
model = Member
template_name = "members/members.html"
def get_queryset(self):
return Member.all_members.filter(role=Member.MemberRole.ACTIVE)
def index(request): class JobListView(ListView):
members = Member.all_members.filter(role=Member.MemberRole.ACTIVE) allow_empty = False
model = JobMember
template_name = "members/jobs.html"
context = { def get_queryset(self):
"members": members, return JobMember.active_member.get_all(slug=self.kwargs["slug"]).order_by(
} F("job__order").asc(nulls_last=True), "job__name"
)
return render(request, "members/members.html", context) def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["active_job_group"] = JobGroup.objects.filter(slug=self.kwargs["slug"]).first()
return context
def jobs(request, slug=None): class MemberListView(ListView):
try: allow_empty = False
description = JobGroup.objects.filter(slug=slug).values().first()["description"] model = Member
except Exception: template_name = "members/members.html"
logger.info("Wrong job '%s'", slug)
raise Http404("wrong job")
job_members = JobMember.active_member.get_all(slug=slug).order_by( def get_queryset(self):
F("job__order").asc(nulls_last=True), if self.kwargs["role"].capitalize() in [r.label for r in Member.MemberRole]:
"job__name", return Member.all_members.filter(role=self.kwargs["role"].capitalize()[:1])
)
active_job_group = JobGroup.objects.filter(slug=slug).first()
context = { return Member.all_members.all() if self.kwargs["role"] in "all" else None
"description": description,
"job_members": job_members,
"active_job_group": active_job_group,
}
return render(request, "members/jobs.html", context)
def members(request, role=None): class MemberDetailView(DetailView):
for elem in Member.MemberRole: model = Member
if role == elem.label.lower(): template_name = "members/member.html"
members = Member.all_members.filter(role=elem.value)
break
else:
members = Member.all_members.all()
context = { def get_context_data(self, **kwargs):
"members": members, context = super().get_context_data(**kwargs)
}
return render(request, "members/members.html", context) context["active_jobs"] = JobMember.members.get_active_jobs(self.object.id)
context["inactive_jobs"] = JobMember.members.get_inactive_jobs(self.object.id)
return context
def profile(request, member_id=None):
member = Member.all_members.filter(id=member_id).first()
if not member:
logger.info("Wrong member id '%s'", member_id)
raise Http404("no member")
active_jobs = JobMember.members.get_active_jobs(member_id)
inactive_jobs = JobMember.members.get_inactive_jobs(member_id)
context = {
"member": member,
"active_jobs": active_jobs,
"inactive_jobs": inactive_jobs,
}
return render(request, "members/member.html", context)

View File

@@ -10,8 +10,7 @@ from .models import Event, FetMeeting, FileUpload, News, Post
def make_fetmeeting(self, request, queryset): def make_fetmeeting(self, request, queryset):
qs = self.get_queryset(request).filter(id=request.POST["_selected_action"]).first() qs = self.get_queryset(request).filter(id=request.POST["_selected_action"]).first()
agenda_key = ep_create_new_pad(qs.slug + "-agenda") if not (agenda_key := ep_create_new_pad(qs.slug + "-agenda")):
if not agenda_key:
self.message_user( self.message_user(
request, request,
"Das Agenda konnte nicht erstellt werden.", "Das Agenda konnte nicht erstellt werden.",
@@ -19,8 +18,7 @@ def make_fetmeeting(self, request, queryset):
) )
return return
protocol_key = ep_create_new_pad(qs.slug + "-protocol") if not (protocol_key := ep_create_new_pad(qs.slug + "-protocol")):
if not protocol_key:
self.message_user( self.message_user(
request, request,
"Das Protokoll konnte nicht erstellt werden.", "Das Protokoll konnte nicht erstellt werden.",
@@ -79,7 +77,7 @@ class PostAdmin(admin.ModelAdmin):
) )
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if obj.author is None: if not obj.author:
obj.author = request.user obj.author = request.user
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)

13
fet2020/posts/choices.py Normal file
View File

@@ -0,0 +1,13 @@
from django.db import models
class PostType(models.TextChoices):
NEWS = "N", "News"
EVENT = "E", "Event"
FETMEETING = "F", "FetMeeting"
class Status(models.TextChoices):
DRAFT = "10", "DRAFT"
ONLY_INTERN = "15", "ONLY_INTERN"
PUBLIC = "20", "PUBLIC"

View File

@@ -2,7 +2,8 @@ import datetime
from django.db import models from django.db import models
from django.db.models import Case, Q, When from django.db.models import Case, Q, When
from django.utils import timezone
from .choices import PostType, Status
class PublishedManager(models.Manager): class PublishedManager(models.Manager):
@@ -10,33 +11,36 @@ class PublishedManager(models.Manager):
""" """
publish all posts with status 'PUBLIC' publish all posts with status 'PUBLIC'
""" """
qs = self.get_queryset().filter(status="20") if public else self.get_queryset() return self.get_queryset().filter(status=Status.PUBLIC) if public else self.get_queryset()
return qs
def published_all(self, public=True): def published_all(self, public=True):
""" """
publish all posts with status 'PUBLIC' and 'ONLY_INTERN' publish all posts with status 'PUBLIC' and 'ONLY_INTERN'
""" """
qs = self.get_queryset().filter(~Q(status="10")) if public else self.get_queryset() return (
return qs self.get_queryset().filter(~Q(status=Status.DRAFT)) if public else self.get_queryset()
)
class PostManager(PublishedManager, models.Manager): class PostManager(PublishedManager, models.Manager):
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = (
qs = qs.annotate( super()
date=Case( .get_queryset()
When(post_type="N", then="public_date"), .annotate(
When(post_type="E", then="event_start__date"), date=Case(
When(post_type="F", then="event_start__date"), When(post_type=PostType.NEWS, then="public_date"),
), When(post_type=PostType.EVENT, then="event_start__date"),
When(post_type=PostType.FETMEETING, then="event_start__date"),
),
)
) )
return qs.order_by("-date", "-id") return qs.order_by("-date", "-id")
def date_sorted_list(self, public=True): def date_sorted(self, public=True):
return self.published(public) return self.published(public)
def date_filtered_list( def date_filter(
self, self,
public=True, public=True,
year=None, year=None,
@@ -46,7 +50,7 @@ class PostManager(PublishedManager, models.Manager):
qs_filter = Q() qs_filter = Q()
if fet_meeting_only: if fet_meeting_only:
qs_filter &= Q(post_type="F") qs_filter &= Q(post_type=PostType.FETMEETING)
if year: if year:
qs_filter &= Q(date__year=year) qs_filter &= Q(date__year=year)
@@ -63,16 +67,16 @@ class ArticleManager(PublishedManager, models.Manager):
""" """
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset().filter(Q(post_type="N") | Q(post_type="E")) qs = super().get_queryset().filter(Q(post_type=PostType.NEWS) | Q(post_type=PostType.EVENT))
qs = qs.annotate( qs = qs.annotate(
date=Case( date=Case(
When(post_type="N", then="public_date"), When(post_type=PostType.NEWS, then="public_date"),
When(post_type="E", then="event_start__date"), When(post_type=PostType.EVENT, then="event_start__date"),
), ),
) )
return qs.order_by("-date", "-id") return qs.order_by("-date", "-id")
def date_sorted_list(self, public=True): def date_sorted(self, public=True):
return self.published(public) return self.published(public)
def pinned(self, public=True): def pinned(self, public=True):
@@ -95,10 +99,12 @@ class ArticleManager(PublishedManager, models.Manager):
return ( return (
self.published(public) self.published(public)
.filter(is_pinned=True)
.filter( .filter(
(Q(post_type="N") & Q(public_date__gt=post_date)) Q(is_pinned=True)
| (Q(post_type="E") & Q(event_end__date__gt=event_date)), & (
(Q(post_type=PostType.NEWS) & Q(public_date__gt=post_date))
| (Q(post_type=PostType.EVENT) & Q(event_end__date__gt=event_date))
)
) )
.first() .first()
) )
@@ -110,10 +116,10 @@ class NewsManager(PublishedManager, models.Manager):
""" """
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset().filter(post_type="N") qs = super().get_queryset().filter(post_type=PostType.NEWS)
qs = qs.annotate( qs = qs.annotate(
date=Case( date=Case(
When(post_type="N", then="public_date"), When(post_type=PostType.NEWS, then="public_date"),
), ),
) )
return qs.order_by("-date") return qs.order_by("-date")
@@ -125,17 +131,21 @@ class AllEventManager(PublishedManager, models.Manager):
""" """
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset().filter(Q(post_type="E") | Q(post_type="F")) qs = (
super()
.get_queryset()
.filter(Q(post_type=PostType.EVENT) | Q(post_type=PostType.FETMEETING))
)
qs = qs.annotate( qs = qs.annotate(
date=Case( date=Case(
When(post_type="E", then="event_start__date"), When(post_type=PostType.EVENT, then="event_start__date"),
When(post_type="F", then="event_start__date"), When(post_type=PostType.FETMEETING, then="event_start__date"),
), ),
) )
return qs.order_by("-date") return qs.order_by("-date")
def future_events(self, public=True): def future_events(self, public=True):
date_today = timezone.now() date_today = datetime.date.today()
qs = self.published(public).filter(event_start__gt=date_today) qs = self.published(public).filter(event_start__gt=date_today)
return qs.reverse() return qs.reverse()
@@ -147,21 +157,21 @@ class EventManager(PublishedManager, models.Manager):
""" """
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset().filter(post_type="E") qs = super().get_queryset().filter(post_type=PostType.EVENT)
qs = qs.annotate( qs = qs.annotate(
date=Case( date=Case(
When(post_type="E", then="event_start__date"), When(post_type=PostType.EVENT, then="event_start__date"),
), ),
) )
return qs.order_by("-date") return qs.order_by("-date")
def future_events(self, public=True): def future_events(self, public=True):
date_today = timezone.now() date_today = datetime.date.today()
qs = self.published(public).filter(event_start__gt=date_today) qs = self.published(public).filter(event_start__gt=date_today)
return qs.reverse() return qs.reverse()
def past_events(self, public=True): def past_events(self, public=True):
date_today = timezone.now() date_today = datetime.date.today()
qs = self.published(public).filter(event_start__lt=date_today) qs = self.published(public).filter(event_start__lt=date_today)
return qs return qs
@@ -172,20 +182,20 @@ class FetMeetingManager(PublishedManager, models.Manager):
""" """
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset().filter(post_type="F") qs = super().get_queryset().filter(post_type=PostType.FETMEETING)
qs = qs.annotate( qs = qs.annotate(
date=Case( date=Case(
When(post_type="F", then="event_start__date"), When(post_type=PostType.FETMEETING, then="event_start__date"),
), ),
) )
return qs.order_by("-date") return qs.order_by("-date")
def future_events(self): def future_events(self):
date_today = timezone.now() date_today = datetime.date.today()
qs = self.published().filter(event_start__gt=date_today) qs = self.published().filter(event_start__gt=date_today)
return qs.reverse() return qs.reverse()
def past_events(self): def past_events(self):
date_today = timezone.now() date_today = datetime.date.today()
qs = self.published().filter(event_start__lt=date_today) qs = self.published().filter(event_start__lt=date_today)
return qs return qs

View File

@@ -8,18 +8,12 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from core.models import CustomFlatPage from core.models import CustomFlatPage
from documents.api import ( from documents.api import ep_create_new_pad, ep_get_html, ep_get_url, ep_pad_exists, ep_set_html
ep_create_new_pad,
ep_get_html,
ep_get_url,
ep_pad_exists,
ep_set_html,
)
from .choices import PostType, Status
from .managers import ( from .managers import (
AllEventManager, AllEventManager,
ArticleManager, ArticleManager,
@@ -30,44 +24,21 @@ from .managers import (
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
request_logger = logging.getLogger("django.request")
def create_pad_for_post(slug, item="agenda"): def create_pad_for_post(slug, item="agenda"):
""" logger.info("Pad-Type: %s", item)
Create a Etherpad pad.
Return a Etherpad key.
"""
logger.info(f"Pad-Type: {item}")
pad_id = slug + "-" + item pad_id = slug + "-" + item
if ep_create_new_pad(pad_id): if not ep_create_new_pad(pad_id):
# Set template into the newly created pad if it exists. return ""
if page := CustomFlatPage.objects.filter(title__iexact=item).first():
ep_set_html(pad_id, page.content)
logger.info(f"Template is set. Template: {page.title}")
return pad_id # Set template into the newly created pad if it exists.
if page := CustomFlatPage.objects.filter(title__iexact=item).first():
ep_set_html(pad_id, page.content)
logger.info("Template '%s' is set.", page.title)
return "#" return pad_id
class Category(models.Model):
# Titel des Posts
title = models.CharField(max_length=200)
subtitle = models.CharField(max_length=500, null=True, blank=True)
# Slug = Text Basierter url bestandteil zb Fetsitzung 22.1.2020 --> fetsitzung_22_1_2020 für Url
slug = models.SlugField(unique=True, null=True, blank=True)
# Ein Haupt Bild für den Post
image = models.ImageField(null=True, blank=True)
tags = TaggableManager(blank=True)
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
class Post(models.Model): class Post(models.Model):
@@ -75,13 +46,12 @@ class Post(models.Model):
legacy_id = models.IntegerField(null=True, blank=True) legacy_id = models.IntegerField(null=True, blank=True)
title = models.CharField(verbose_name="Titel", max_length=200) title = models.CharField(verbose_name="Titel", max_length=200)
subtitle = models.CharField(max_length=500, null=True, blank=True) subtitle = models.CharField(max_length=500, blank=True, default="")
tags = TaggableManager(blank=True) tags = TaggableManager(blank=True)
# Slug = Text Basierter url bestandteil zb Fetsitzung 22.1.2020 --> fetsitzung_22_1_2020 für Url
slug = models.SlugField(unique=True, blank=True) slug = models.SlugField(unique=True, blank=True)
body = models.TextField(null=True, blank=True) body = models.TextField(blank=True, default="")
image = models.ImageField(null=True, blank=True) image = models.ImageField(null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
@@ -92,19 +62,10 @@ class Post(models.Model):
default=timezone.now, default=timezone.now,
) )
__choices = [("N", _("News")), ("E", _("Event")), ("F", _("FetMeeting"))] post_type = models.CharField(max_length=1, choices=PostType.choices, editable=True)
post_type = models.CharField(max_length=1, choices=__choices, editable=True)
class Status(models.TextChoices):
DRAFT = "10", _("DRAFT")
ONLY_INTERN = "15", _("ONLY_INTERN")
PUBLIC = "20", _("PUBLIC")
status = models.CharField( status = models.CharField(
verbose_name="Status", verbose_name="Status", max_length=2, choices=Status.choices, default=Status.DRAFT
max_length=2,
choices=Status.choices,
default=Status.DRAFT,
) )
# post is pinned at main page # post is pinned at main page
@@ -113,22 +74,18 @@ class Post(models.Model):
# addional infos for events # addional infos for events
event_start = models.DateTimeField(verbose_name="Event Start", null=True, blank=True) event_start = models.DateTimeField(verbose_name="Event Start", null=True, blank=True)
event_end = models.DateTimeField(verbose_name="Event Ende", null=True, blank=True) event_end = models.DateTimeField(verbose_name="Event Ende", null=True, blank=True)
event_place = models.CharField(max_length=200, null=True, blank=True) event_place = models.CharField(max_length=200, blank=True, default="")
# protocol for fet meeting # protocol for fet meeting
has_protocol = models.BooleanField(default=False) has_protocol = models.BooleanField(default=False)
has_agenda = models.BooleanField(default=False) has_agenda = models.BooleanField(default=False)
protocol_key = models.CharField(max_length=200, null=True, blank=True) protocol_key = models.CharField(max_length=200, blank=True, default="")
agenda_key = models.CharField(max_length=200, null=True, blank=True) agenda_key = models.CharField(max_length=200, blank=True, default="")
# TimeStamps # TimeStamps
date_modified = models.DateTimeField(auto_now=True) date_modified = models.DateTimeField(auto_now=True)
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
# useless (-_-)
legacy_rubrik_id = models.IntegerField(null=True, blank=True)
imported_from = models.CharField(max_length=200, null=True, blank=True)
# Managers # Managers
objects = PostManager() objects = PostManager()
articles = ArticleManager() articles = ArticleManager()
@@ -153,48 +110,45 @@ class Post(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("posts:post", kwargs={"slug": self.slug}) return reverse("posts:post", kwargs={"slug": self.slug})
# "#" for backward compatibility
_possible_empty_key_value = ["#", "", None]
@property @property
def agenda_html(self) -> str | None: def agenda_html(self) -> str | None:
"Agenda HTML from Etherpad Pad" if self.agenda_key in self._possible_empty_key_value:
if self.agenda_key in [None, "#"]:
return None return None
return ep_get_html(self.agenda_key) return ep_get_html(self.agenda_key)
@agenda_html.setter
def agenda_html(self, value: str) -> str | None:
if self.agenda_key in self._possible_empty_key_value:
self.create_agenda_key()
if not value or not self.agenda_key:
return None
ep_set_html(self.agenda_key, value)
logger.info("Set agenda etherpad '%s' for post '%s'.", self.agenda_key, self.slug)
return value
@property @property
def protocol_html(self) -> str | None: def protocol_html(self) -> str | None:
"Protocol HTML from Etherpad Pad" if self.protocol_key in self._possible_empty_key_value:
if self.protocol_key in [None, "#"]:
return None return None
return ep_get_html(self.protocol_key) return ep_get_html(self.protocol_key)
@agenda_html.setter
def agenda_html(self, value: str) -> str | None:
if self.agenda_key is None:
self.create_agenda_key()
if value is None or self.agenda_key in [None, "#"]:
return None
ep_set_html(self.agenda_key, value)
request_logger.info("Set agenda etherpad. Post: %s. Key: %s", self.slug, self.agenda_key)
return value
@protocol_html.setter @protocol_html.setter
def protocol_html(self, value: str) -> str | None: def protocol_html(self, value: str) -> str | None:
if self.protocol_key is None: if self.protocol_key in self._possible_empty_key_value:
self.create_protocol_key() self.create_protocol_key()
if value is None or self.protocol_key in [None, "#"]: if not value or not self.protocol_key:
return None return None
ep_set_html(self.protocol_key, value) ep_set_html(self.protocol_key, value)
request_logger.info( logger.info("Set protocol etherpad '%s' for post '%s'.", self.protocol_key, self.slug)
"Set protocol etherpad. Post: %s. Key: %s",
self.slug,
self.protocol_key,
)
return value return value
_agenda_filename = None _agenda_filename = None
@@ -202,15 +156,15 @@ class Post(models.Model):
@property @property
def agenda_url(self) -> str | None: def agenda_url(self) -> str | None:
if self.has_agenda is not True: if not self.has_agenda:
self._agenda_url = None self._agenda_url = None
self._agenda_filename = None self._agenda_filename = None
return self._agenda_url return self._agenda_url
if self._agenda_url is not None: if self._agenda_url:
return self._agenda_url return self._agenda_url
if (url := ep_get_url(self.agenda_key)) not in [None, "#"]: if url := ep_get_url(self.agenda_key):
self._agenda_url = url self._agenda_url = url
self._agenda_filename = self.slug + "-agenda.pdf" self._agenda_filename = self.slug + "-agenda.pdf"
else: else:
@@ -221,7 +175,7 @@ class Post(models.Model):
@property @property
def agenda_filename(self) -> str | None: def agenda_filename(self) -> str | None:
if self._agenda_filename is not None: if self._agenda_filename:
return self._agenda_filename return self._agenda_filename
if self.has_agenda and self.agenda_url: if self.has_agenda and self.agenda_url:
@@ -234,15 +188,15 @@ class Post(models.Model):
@property @property
def protocol_url(self) -> str | None: def protocol_url(self) -> str | None:
if self.has_protocol is not True: if not self.has_protocol:
self._protocol_url = None self._protocol_url = None
self._protocol_filename = None self._protocol_filename = None
return self._protocol_url return self._protocol_url
if self._protocol_url is not None: if self._protocol_url:
return self._protocol_url return self._protocol_url
if (url := ep_get_url(self.protocol_key)) not in [None, "#"]: if url := ep_get_url(self.protocol_key):
self._protocol_url = url self._protocol_url = url
self._protocol_filename = self.slug + "-protokoll.pdf" self._protocol_filename = self.slug + "-protokoll.pdf"
else: else:
@@ -253,7 +207,7 @@ class Post(models.Model):
@property @property
def protocol_filename(self) -> str | None: def protocol_filename(self) -> str | None:
if self._protocol_filename is not None: if self._protocol_filename:
return self._protocol_filename return self._protocol_filename
if self.has_protocol and self.protocol_url: if self.has_protocol and self.protocol_url:
@@ -290,14 +244,9 @@ class Post(models.Model):
@property @property
def imageurl(self) -> str: def imageurl(self) -> str:
""" return (
returns the url to the image self.image.url if self.image else settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
""" )
if self.image:
return self.image.url
# return default image
return settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
def clean(self): def clean(self):
if self.event_end and self.event_end < self.event_start: if self.event_end and self.event_end < self.event_start:
@@ -307,7 +256,7 @@ class Post(models.Model):
@property @property
def published(self): def published(self):
return self.status == self.Status.PUBLIC return self.status == Status.PUBLIC
class News(Post): class News(Post):
@@ -372,14 +321,17 @@ class FetMeeting(Event):
if not self.slug: if not self.slug:
self.slug = self.__get_slug() self.slug = self.__get_slug()
if ep_pad_exists(self.agenda_key) is not True or self.slug not in self.agenda_key: if not ep_pad_exists(self.agenda_key) or self.agenda_key in self._possible_empty_key_value:
self.create_agenda_key() self.create_agenda_key()
if self.agenda_key not in [None, "#"]: if self.agenda_key:
self.has_agenda = True self.has_agenda = True
if ep_pad_exists(self.protocol_key) is not True or self.slug not in self.protocol_key: if (
not ep_pad_exists(self.protocol_key)
or self.protocol_key in self._possible_empty_key_value
):
self.create_protocol_key() self.create_protocol_key()
if self.protocol_key not in [None, "#"]: if self.protocol_key:
self.has_protocol = True self.has_protocol = True
if not self.post_type: if not self.post_type:
@@ -393,7 +345,7 @@ class FetMeeting(Event):
self.event_end = self.event_start + timedelta(hours=2) self.event_end = self.event_start + timedelta(hours=2)
# set FET Meeting always public # set FET Meeting always public
self.status = self.Status.PUBLIC self.status = Status.PUBLIC
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@@ -17,7 +17,7 @@ class PostIndex(indexes.SearchIndex, indexes.Indexable):
return Post return Post
def index_queryset(self, using=None): def index_queryset(self, using=None):
return self.get_model().objects.date_sorted_list(public=False) return self.get_model().objects.date_sorted(public=False)
def prepare_date(self, obj): def prepare_date(self, obj):
if obj.post_type == "N": if obj.post_type == "N":

View File

@@ -39,7 +39,7 @@ class PostTestCase(TestCase):
post.event_start = timezone.now() - timedelta(1) post.event_start = timezone.now() - timedelta(1)
post.save() post.save()
post_list = Post.objects.date_sorted_list(public=False) post_list = Post.objects.date_sorted(public=False)
self.assertEqual(post_list[0].title, "zukünftiges FET Fest") self.assertEqual(post_list[0].title, "zukünftiges FET Fest")
self.assertEqual(post_list[1].title, "Informationen zur ÖH Wahl") self.assertEqual(post_list[1].title, "Informationen zur ÖH Wahl")
self.assertEqual(post_list[2].title, "vergangenes FET Fest") self.assertEqual(post_list[2].title, "vergangenes FET Fest")

View File

@@ -15,6 +15,7 @@ from documents.etherpadlib import add_ep_cookie
from fet2020.utils import add_log_action from fet2020.utils import add_log_action
from members.models import Member from members.models import Member
from .choices import PostType
from .forms import ( from .forms import (
EventUpdateForm, EventUpdateForm,
FetMeetingCreateForm, FetMeetingCreateForm,
@@ -35,7 +36,7 @@ def index(request):
if request.method == "POST": if request.method == "POST":
form = PostSearchForm(request.POST) form = PostSearchForm(request.POST)
if form.is_valid(): if form.is_valid():
posts = Post.objects.date_filtered_list( posts = Post.objects.date_filter(
public_only, public_only,
form.cleaned_data["year"], form.cleaned_data["year"],
form.cleaned_data["month"], form.cleaned_data["month"],
@@ -44,7 +45,7 @@ def index(request):
else: else:
# If no input, all posts are shown. # If no input, all posts are shown.
form = PostSearchForm() form = PostSearchForm()
posts = Post.objects.date_filtered_list(public_only) posts = Post.objects.date_filter(public_only)
context = { context = {
"formset": form, "formset": form,
@@ -92,7 +93,7 @@ class PostDetailView(DetailView):
try: try:
response = add_ep_cookie(request, response) response = add_ep_cookie(request, response)
except Exception as e: except Exception as e:
logger.info("Etherpad Server doesn't work. Error: %s", e) logger.info("Etherpad Server not working. Error: %s", e)
return response return response
@@ -134,9 +135,9 @@ class PostDetailView(DetailView):
def get_template_names(self): def get_template_names(self):
template_name = "posts/news/detail.html" template_name = "posts/news/detail.html"
if self.object.post_type == "E": if self.object.post_type == PostType.EVENT:
template_name = "posts/event/detail.html" template_name = "posts/event/detail.html"
elif self.object.post_type == "F": elif self.object.post_type == PostType.FETMEETING:
template_name = "posts/fetmeeting/detail.html" template_name = "posts/fetmeeting/detail.html"
return template_name return template_name
@@ -145,7 +146,7 @@ class PostDetailView(DetailView):
""" """
Helper function for getting previous post Helper function for getting previous post
""" """
posts = Post.objects.date_sorted_list(self.public_only).filter( posts = Post.objects.date_sorted(self.public_only).filter(
post_type=self.object.post_type, post_type=self.object.post_type,
) )
qs = posts.filter( qs = posts.filter(
@@ -165,7 +166,7 @@ class PostDetailView(DetailView):
Helper function for getting next post Helper function for getting next post
""" """
posts = ( posts = (
Post.objects.date_sorted_list(self.public_only) Post.objects.date_sorted(self.public_only)
.filter(post_type=self.object.post_type) .filter(post_type=self.object.post_type)
.reverse() .reverse()
) )
@@ -187,9 +188,9 @@ class PostUpdateView(LoginRequiredMixin, UpdateView):
def form_valid(self, form): def form_valid(self, form):
model = "news" model = "news"
if self.object.post_type == "E": if self.object.post_type == PostType.EVENT:
model = "event" model = "event"
elif self.object.post_type == "F": elif self.object.post_type == PostType.FETMEETING:
model = "fetmeeting" model = "fetmeeting"
add_log_action(self.request, form, "posts", model, False) add_log_action(self.request, form, "posts", model, False)
@@ -197,9 +198,9 @@ class PostUpdateView(LoginRequiredMixin, UpdateView):
def get_form_class(self): def get_form_class(self):
form_class = NewsUpdateForm form_class = NewsUpdateForm
if self.object.post_type == "E": if self.object.post_type == PostType.EVENT:
form_class = EventUpdateForm form_class = EventUpdateForm
elif self.object.post_type == "F": elif self.object.post_type == PostType.FETMEETING:
form_class = FetMeetingUpdateForm form_class = FetMeetingUpdateForm
return form_class return form_class
@@ -229,9 +230,9 @@ class PostUpdateView(LoginRequiredMixin, UpdateView):
def get_template_names(self): def get_template_names(self):
template_name = "posts/news/update.html" template_name = "posts/news/update.html"
if self.object.post_type == "E": if self.object.post_type == PostType.EVENT:
template_name = "posts/event/update.html" template_name = "posts/event/update.html"
elif self.object.post_type == "F": elif self.object.post_type == PostType.FETMEETING:
template_name = "posts/fetmeeting/update.html" template_name = "posts/fetmeeting/update.html"
return template_name return template_name
@@ -307,10 +308,8 @@ def show_pdf(request, html, filename):
idx = html.index("<body>") idx = html.index("<body>")
html = html[:idx] + rendered + html[idx:] html = html[:idx] + rendered + html[idx:]
pdf = render_to_pdf(html) if not (pdf := render_to_pdf(html)):
raise Http404("can't create pdf file.")
if not pdf:
raise Http404("can't create pdf file")
response = HttpResponse(pdf, content_type="application/pdf") response = HttpResponse(pdf, content_type="application/pdf")

View File

@@ -13,7 +13,7 @@
<h1 class="page-title">Fotos</h1> <h1 class="page-title">Fotos</h1>
<section class="text-gray-800 dark:text-gray-200 my-2 sm:my-4"> <section class="text-gray-800 dark:text-gray-200 my-2 sm:my-4">
{% if album.status == album.DRAFT %} {% if album.status == album.Status.DRAFT %}
<h2 class="text-gray-900 dark:text-gray-100 text-lg font-medium tracking-wide"><i class="fa-solid fa-eye-slash text-gray-600 dark:text-gray-300 mr-2" title="Album ist nicht öffentlich!"></i>{{ album.title }}</h2> <h2 class="text-gray-900 dark:text-gray-100 text-lg font-medium tracking-wide"><i class="fa-solid fa-eye-slash text-gray-600 dark:text-gray-300 mr-2" title="Album ist nicht öffentlich!"></i>{{ album.title }}</h2>
{% else %} {% else %}
<h2 class="text-gray-900 dark:text-gray-100 text-lg font-medium tracking-wide">{{ album.title }}</h2> <h2 class="text-gray-900 dark:text-gray-100 text-lg font-medium tracking-wide">{{ album.title }}</h2>
@@ -34,7 +34,7 @@
{% endif %} {% endif %}
</section> </section>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated and album.id %}
<a href="{% url 'admin:gallery_album_change' album.id %}" class="page-subtitle block btn-small btn-primary w-full sm:w-max sm:mr-0 sm:ml-auto"> <a href="{% url 'admin:gallery_album_change' album.id %}" class="page-subtitle block btn-small btn-primary w-full sm:w-max sm:mr-0 sm:ml-auto">
<i class="fa-regular fa-folder-open mr-1"></i>Album bearbeiten <i class="fa-regular fa-folder-open mr-1"></i>Album bearbeiten
</a> </a>
@@ -52,7 +52,7 @@
</div> </div>
<div id="links" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 justify-items-center gap-3"> <div id="links" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 justify-items-center gap-3">
{% for image in images %} {% for image in object.images %}
<a id="{{ image.title }}" href="{{ image.image_url }}" title="{{ image.title }}" class="block max-w-xs sm:max-w-none"> <a id="{{ image.title }}" href="{{ image.image_url }}" title="{{ image.title }}" class="block max-w-xs sm:max-w-none">
{% if image.thumb_url %} {% if image.thumb_url %}
<img src="{{ image.thumb_url }}" alt="{{ image.title }}" class="rounded-sm"> <img src="{{ image.thumb_url }}" alt="{{ image.title }}" class="rounded-sm">

View File

@@ -15,15 +15,15 @@
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 justify-items-center gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 justify-items-center gap-4">
{% for album in albums %} {% for album in albums %}
{% if request.user.is_authenticated and album.status == album.DRAFT %} {% if request.user.is_authenticated and album.status == album.Status.DRAFT %}
<a href="{% url 'gallery:album_draft' album.slug %}" class="block max-w-xs sm:max-w-none"> <a href="{% url 'gallery:album_draft' album.slug %}" class="block max-w-xs sm:max-w-none">
<img src="{{ album.thumbnail }}" class="rounded"> <img src="{{ album.get_album_thumbnail }}" class="rounded">
<h2 class="px-2 text-proprietary-dark dark:text-sky-300">{{ album.title }}</h2> <h2 class="px-2 text-proprietary-dark dark:text-sky-300">{{ album.title }}</h2>
<h3 class="px-2 text-sm text-proprietary dark:text-sky-400 font-normal"><i class="fa-solid fa-calendar-day mr-1"></i>{{ album.event_date }}</h3> <h3 class="px-2 text-sm text-proprietary dark:text-sky-400 font-normal"><i class="fa-solid fa-calendar-day mr-1"></i>{{ album.event_date }}</h3>
</a> </a>
{% elif album.status == album.PUBLIC %} {% elif album.status == album.Status.PUBLIC %}
<a href="{% url 'gallery:album' album.slug %}" class="block max-w-xs sm:max-w-none"> <a href="{% url 'gallery:album' album.slug %}" class="block max-w-xs sm:max-w-none">
<img src="{{ album.thumbnail }}" class="rounded"> <img src="{{ album.get_album_thumbnail }}" class="rounded">
<h2 class="px-2 text-proprietary-dark dark:text-sky-300">{{ album.title }}</h2> <h2 class="px-2 text-proprietary-dark dark:text-sky-300">{{ album.title }}</h2>
<h3 class="px-2 text-sm text-proprietary dark:text-sky-400 font-normal"><i class="fa-solid fa-calendar-day mr-1"></i>{{ album.event_date }}</h3> <h3 class="px-2 text-sm text-proprietary dark:text-sky-400 font-normal"><i class="fa-solid fa-calendar-day mr-1"></i>{{ album.event_date }}</h3>
</a> </a>

View File

@@ -106,13 +106,13 @@
{% endblock member_content %} {% endblock member_content %}
{% endif %} {% endif %}
{% if members %} {% if member_list %}
<!-- show all, active or pension members --> <!-- show all, active or pension members -->
{% block members_content %} {% block members_content %}
{% endblock members_content %} {% endblock members_content %}
{% endif %} {% endif %}
{% if job_members %} {% if jobmember_list %}
<!-- show job lists in a job group --> <!-- show job lists in a job group -->
{% block jobs_content %} {% block jobs_content %}
{% endblock jobs_content %} {% endblock jobs_content %}

View File

@@ -5,15 +5,15 @@
{% block jobs_content %} {% block jobs_content %}
<div class="db-page-content"> <div class="db-page-content">
<h2>{{ active_job_group.name }}</h2> <h2>{{ active_job_group.name }}</h2>
{% if description %} {% if active_job_group.description %}
{{ description|safe }} {{ active_job_group.description|safe }}
{% endif %} {% endif %}
</div> </div>
<div> <div>
{% regroup job_members by job.name as all_jobmem_list %} {% regroup jobmember_list by job.name as jobmem_list %}
{% for jobmem in all_jobmem_list %} {% for jobmem in jobmem_list %}
<article id="{{ jobmem.list.0.job.slug }}" class="members-article"> <article id="{{ jobmem.list.0.job.slug }}" class="members-article">
<a href="#{{ jobmem.list.0.job.slug }}" class="title"> <a href="#{{ jobmem.list.0.job.slug }}" class="title">
<i class="fa-solid fa-link"></i> <i class="fa-solid fa-link"></i>

View File

@@ -14,7 +14,7 @@
<article class="members-article"> <article class="members-article">
<div class="members-listing"> <div class="members-listing">
{% for member in members %} {% for member in member_list %}
<figure> <figure>
<a href="{{ member.get_absolute_url }}"> <a href="{{ member.get_absolute_url }}">
<img loading="lazy" src="{{ member.thumb_url }}" alt="Portraitfoto von {{ member.firstname }}" class="w-36 h-36 bg-white"> <img loading="lazy" src="{{ member.thumb_url }}" alt="Portraitfoto von {{ member.firstname }}" class="w-36 h-36 bg-white">