diff --git a/core/api/serializers/custom/__init__.py b/core/api/serializers/custom/__init__.py index 8fdf3741..99858068 100644 --- a/core/api/serializers/custom/__init__.py +++ b/core/api/serializers/custom/__init__.py @@ -48,9 +48,7 @@ def to_internal_value(self, data): def to_representation(self, obj): return obj.model - - -class AuthorSerializer(serializers.ModelSerializer): +class SingleUserSerializer(serializers.ModelSerializer): gravatar_url = serializers.SerializerMethodField(read_only=True) class Meta: @@ -87,7 +85,7 @@ def to_representation(self, obj): class CommentSerializer(serializers.ModelSerializer): - author = AuthorSerializer() + author = SingleUserSerializer() has_children = serializers.SerializerMethodField(read_only=True) edited = serializers.SerializerMethodField(read_only=True) likes = LikeField() @@ -113,9 +111,9 @@ class Meta: ] -class AuthorField(serializers.Field): +class SingleUserField(serializers.Field): def to_representation(self, value): - return AuthorSerializer(value).data if value else None + return SingleUserSerializer(value).data if value else None def to_internal_value(self, data): if data is None: @@ -143,6 +141,38 @@ def __init__(self, **kwargs): self.default_error_messages.update(default_error_messages) +class MembersField(serializers.Field): + def to_representation(self, value): + return SingleUserSerializer(value, many=True).data + + def to_internal_value(self, data): + if data is None: + return None + if not isinstance(data, list): + raise serializers.ValidationError("Expected a list of user IDs.") + + users = User.objects.filter(id__in=data).values_list("id", flat=True) + if len(users) != len(data): + missing_ids = set(data) - set(users) + for missing_id in missing_ids: + self.fail( + "invalid", message=f"User with ID {missing_id} does not exist." + ) + return User.objects.filter(id__in=data) + + @staticmethod + def get_queryset(): + return User.objects.exclude(is_active=False) + + def __init__(self, **kwargs): + default_error_messages = { + "does_not_exist": "User with ID {value} does not exist.", + } + kwargs["help_text"] = "The User ID of the member of this object." + super().__init__(**kwargs) + self.default_error_messages.update(default_error_messages) + + class OrganizationField(serializers.Field): def to_representation(self, value): return OrganizationSerializer(value).data if value else None @@ -172,6 +202,25 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.default_error_messages.update(default_error_messages) +class UserOrganizationField(OrganizationField): + def to_representation(self, value): + return OrganizationSerializer(value, many=True).data if value else None + + def to_internal_value(self, data): + if data is None: + return None + if not isinstance(data, list): + raise serializers.ValidationError("Expected a list of organization IDs.") + + organizations = Organization.objects.filter(id__in=data).values_list("id", flat=True) + if len(organizations) != len(data): + missing_ids = set(data) - set(organizations) + for missing_id in missing_ids: + self.fail( + "invalid", message=f"Organization with ID {missing_id} does not exist." + ) + return Organization.objects.filter(id__in=data) + class TagRelatedField(serializers.MultipleChoiceField): """ @@ -212,7 +261,7 @@ def to_internal_value(self, data): return Tag.objects.filter(id__in=data) - + class CommentField(Field): def __init__(self, **kwargs): kwargs["read_only"] = True diff --git a/core/api/views/meta.py b/core/api/views/meta.py index 86b06167..11a4ab0f 100644 --- a/core/api/views/meta.py +++ b/core/api/views/meta.py @@ -1,7 +1,7 @@ -from datetime import datetime from typing import Dict from django.conf import settings +from django.utils import timezone from rest_framework.response import Response from rest_framework.views import APIView @@ -33,9 +33,9 @@ def get(request): @classmethod def calculate_banners(cls): - now = datetime.now(settings.TZ) - current = filter(lambda b: b["start"] < now < b["end"], settings.BANNER3) + now = timezone.now() + current = filter(lambda b: b["start"] <= now < b["end"], settings.BANNER3) current = list(map(Banners.censor, current)) - upcoming = filter(lambda b: now <= b["start"], settings.BANNER3) + upcoming = filter(lambda b: now < b["start"], settings.BANNER3) upcoming = list(map(Banners.censor, upcoming)) return dict(current=current, upcoming=upcoming) diff --git a/core/api/views/objects/announcement.py b/core/api/views/objects/announcement.py index f5d4eb41..b29a1913 100644 --- a/core/api/views/objects/announcement.py +++ b/core/api/views/objects/announcement.py @@ -13,7 +13,7 @@ from .base import BaseProvider from ...serializers.custom import ( TagRelatedField, - AuthorField, + SingleUserField, OrganizationField, CommentField, LikeField, @@ -47,7 +47,7 @@ class Serializer(serializers.ModelSerializer): comments = CommentField() likes = LikeField() tags = TagRelatedField() - author = AuthorField() + author = SingleUserField() organization = OrganizationField() def save(self, **kwargs): diff --git a/core/api/views/objects/blog_post.py b/core/api/views/objects/blog_post.py index a71e8cb5..71062131 100644 --- a/core/api/views/objects/blog_post.py +++ b/core/api/views/objects/blog_post.py @@ -3,14 +3,14 @@ from rest_framework import permissions, serializers from .base import BaseProvider -from ...serializers.custom import TagRelatedField, CommentField, LikeField, AuthorField +from ...serializers.custom import TagRelatedField, CommentField, LikeField, SingleUserField from ....models import BlogPost class Serializer(serializers.ModelSerializer): likes = LikeField() comments = CommentField() - author = AuthorField() + author = SingleUserField() tags = TagRelatedField() def to_representation(self, instance: BlogPost): diff --git a/core/api/views/objects/exhibit.py b/core/api/views/objects/exhibit.py index 8e516b76..295a303c 100644 --- a/core/api/views/objects/exhibit.py +++ b/core/api/views/objects/exhibit.py @@ -4,7 +4,7 @@ from .base import BaseProvider from ...serializers.custom import ( TagRelatedField, - AuthorField, + SingleUserField, LikeField, CommentField, ) @@ -15,7 +15,7 @@ class Serializer(serializers.ModelSerializer): likes = LikeField() comments = CommentField() tags = TagRelatedField() - author = AuthorField() + author = SingleUserField() class Meta: model = Exhibit diff --git a/core/api/views/objects/organization.py b/core/api/views/objects/organization.py index 796b73b5..13336781 100644 --- a/core/api/views/objects/organization.py +++ b/core/api/views/objects/organization.py @@ -1,3 +1,4 @@ +from core.api.serializers.custom import MembersField, TagRelatedField, SingleUserField from django.contrib.admin.models import LogEntry from django.contrib.contenttypes.models import ContentType from django.db.models import Count @@ -8,13 +9,17 @@ class Serializer(serializers.ModelSerializer): + tags = TagRelatedField() + members = MembersField() + execs = MembersField() + supervisors = MembersField() + owner = SingleUserField() + links = serializers.SlugRelatedField( slug_field="url", many=True, queryset=models.OrganizationURL.objects.all() ) - members = serializers.PrimaryKeyRelatedField( - many=True, queryset=models.User.objects.all() - ) - + + class Meta: model = models.Organization fields = "__all__" diff --git a/core/api/views/objects/post_interactions.py b/core/api/views/objects/post_interactions.py index 0a89ed01..ce1ea7fb 100644 --- a/core/api/views/objects/post_interactions.py +++ b/core/api/views/objects/post_interactions.py @@ -14,7 +14,7 @@ from ...serializers.custom import ( ContentTypeField, CommentField, - AuthorField, + SingleUserField, LikeField, ) from ....models import Comment, User, Like @@ -40,7 +40,7 @@ def has_permission(self, request, view): class CommentSerializer(serializers.ModelSerializer): likes = LikeField() - author = AuthorField() + author = SingleUserField() edited = serializers.SerializerMethodField(read_only=True) children = CommentField() content_type = ContentTypeField() diff --git a/core/api/views/objects/user.py b/core/api/views/objects/user.py index ee2c7bcd..002c2c53 100644 --- a/core/api/views/objects/user.py +++ b/core/api/views/objects/user.py @@ -1,6 +1,7 @@ import base64 import hashlib +from core.api.serializers.custom import UserOrganizationField from django.conf import settings from django.contrib.admin.models import LogEntry from django.contrib.contenttypes.models import ContentType @@ -21,11 +22,15 @@ class Serializer(serializers.ModelSerializer): old_password = serializers.CharField( required=False, write_only=True, trim_whitespace=False ) - - def get_gravatar_url(self, obj): + organizations = UserOrganizationField() + organizations_leading = UserOrganizationField() + + @staticmethod + def get_gravatar_url(obj): return gravatar_url(obj.email) - def get_email_hash(self, obj): + @staticmethod + def get_email_hash(obj): return base64.standard_b64encode( hashlib.md5(obj.email.encode("utf-8")).digest() ) diff --git a/core/static/core/css/admin-secondary.css b/core/static/core/css/admin-secondary.css index f59f81aa..12a9e4de 100644 --- a/core/static/core/css/admin-secondary.css +++ b/core/static/core/css/admin-secondary.css @@ -162,14 +162,19 @@ td, th { } .btn-light{ - width: 100%; + /*width: 100%;*/ background-color: var(--light-grey); + padding: 0.375rem 0.75rem; } .btn-light:hover, .btn-light:active { background-color: var(--line-grey); } +/*.ms-lg-auto {*/ /*Uncomment to move to left*/ +/* margin-left: unset !important;*/ +/*}*/ + .form-row{ margin-top: 5px; } @@ -232,11 +237,6 @@ textarea { /*padding-left: 0px !important;*/ } -*, *:before, *:after { - -webkit-box-sizing: initial !important; - box-sizing: initial !important; -} - .aligned label { width: 160px !important; } diff --git a/core/static/core/css/base.css b/core/static/core/css/base.css index 02b02ee8..c46da2f6 100644 --- a/core/static/core/css/base.css +++ b/core/static/core/css/base.css @@ -11,7 +11,7 @@ --status-red: #e85856; --status-yellow: #ebc942; --status-green: #5cc478; - + font-family: 'Roboto', sans-serif; letter-spacing: 0.15px; background-color: var(--bg-grey); @@ -36,7 +36,7 @@ --status-red: #e85856; --status-yellow: #ebc942; --status-green: #5cc478; - + font-family: 'Roboto', sans-serif; letter-spacing: 0.15px; background-color: var(--dark-grey); @@ -263,6 +263,13 @@ label { align-items: center; padding: 20px; background-color: var(--contrast-colour); + opacity: 1; + transition: opacity 0.5s ease-in-out; +} + +.install-popup.hide { + /*display: none;*/ + opacity: 0; } .install-popup > *{ @@ -273,7 +280,7 @@ label { display: block; /*height: 64px;*/ height: 48px; - margin: none; + margin: 0; width: auto; background-color: var(--dark-colour); border-radius: 5px; @@ -284,7 +291,7 @@ label { } .install-popup > span{ - margin: 0px 15px; + margin: 0 15px; font-size: 16px; color: var(--dark-colour); } diff --git a/core/static/core/js/install.js b/core/static/core/js/install.js index b258c492..46468460 100644 --- a/core/static/core/js/install.js +++ b/core/static/core/js/install.js @@ -4,7 +4,7 @@ let installPopupClass = '.install-popup' function displayInstallPrompt(event) { event.preventDefault(); deferredPrompt = event; - if (Cookies.get('hide_install_prompt') != '1') { + if (Cookies.get('hide_install_prompt') !== '1') { $(installPopupClass).show(); } } diff --git a/core/templates/core/base.html b/core/templates/core/base.html index 39a68460..13714222 100644 --- a/core/templates/core/base.html +++ b/core/templates/core/base.html @@ -21,7 +21,11 @@ {% if pre %}{{ pre | safe }}{% endif %} {% banners "current" as current_banners %} {% for value in current_banners %} -