From c6aad329dedc9d0734dfd0d06a838d9f5c9e5400 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 10 Mar 2026 22:46:06 +0100 Subject: [PATCH 1/7] Add is_hidden flag to event object --- .../migrations/0006_event_is_hidden.py | 18 +++++++ backend/samfundet/models/event.py | 5 ++ .../EventCreatorAdminPage.tsx | 47 +++++++++++++------ .../EventCreatorSchema.ts | 2 + frontend/src/dto.ts | 1 + frontend/src/schema/event.ts | 1 + 6 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 backend/samfundet/migrations/0006_event_is_hidden.py diff --git a/backend/samfundet/migrations/0006_event_is_hidden.py b/backend/samfundet/migrations/0006_event_is_hidden.py new file mode 100644 index 000000000..708c86ab7 --- /dev/null +++ b/backend/samfundet/migrations/0006_event_is_hidden.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.11 on 2026-03-10 20:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('samfundet', '0005_medlemsinfo'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='is_hidden', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/samfundet/models/event.py b/backend/samfundet/models/event.py index 6b60afce3..0c9d8c0fc 100644 --- a/backend/samfundet/models/event.py +++ b/backend/samfundet/models/event.py @@ -206,6 +206,11 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # This cannot be a real foreign key because django currently does not support cross-db relationships billig_id = models.IntegerField(blank=True, null=True, unique=True) + # ======================== # + # Visibility # + # ======================== # + is_hidden = models.BooleanField(default=False) + # ======================== # # Computed Properties # # ======================== # diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx index d546d3291..efee992e5 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx @@ -20,6 +20,7 @@ import { ImageCard, Input, Textarea, + ToggleSwitch, } from '~/Components'; import type { DropdownOption } from '~/Components/Dropdown/Dropdown'; import { ImagePicker } from '~/Components/ImagePicker/ImagePicker'; @@ -102,6 +103,7 @@ export function EventCreatorAdminPage() { image: undefined, visibility_from_dt: '', visibility_to_dt: '', + is_hidden: false, }, }); @@ -498,20 +500,37 @@ export function EventCreatorAdminPage() { return !!data.visibility_from_dt; }, template: ( - ( - - {t(KEY.saksdokumentpage_publication_date) ?? ''} - - - - - - )} - /> + <> + ( + + {t(KEY.saksdokumentpage_publication_date) ?? ''} + + + + + + )} + /> + ( + + {t(KEY.event_form_is_hidden)} + + field.onChange(!field.value)} /> + +

{t(KEY.event_form_is_hidden_help)}

+ +
+ )} + /> + ), }, ]; diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorSchema.ts b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorSchema.ts index 6a9ca71a6..42a8ed6ae 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorSchema.ts +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorSchema.ts @@ -9,6 +9,7 @@ import { EVENT_DURATION, EVENT_END_DT, EVENT_HOST, + EVENT_IS_HIDDEN, EVENT_LOCATION, EVENT_REGISTRATION_URL, EVENT_START_DT, @@ -53,6 +54,7 @@ export const eventSchema = z.object({ // Summary/Publication date visibility_from_dt: EVENT_VISIBILITY_FROM_DT, visibility_to_dt: EVENT_VISIBILITY_TO_DT, + is_hidden: EVENT_IS_HIDDEN, }); export type EventFormType = z.infer; diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 4ecd509da..06b4bbbd9 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -196,6 +196,7 @@ export type EventDto = { // Used to create new event with using id of existing imagedto image?: ImageDto; capacity?: number; + is_hidden?: boolean; }; export type EventGroupDto = { diff --git a/frontend/src/schema/event.ts b/frontend/src/schema/event.ts index f3da2a737..9afe64bed 100644 --- a/frontend/src/schema/event.ts +++ b/frontend/src/schema/event.ts @@ -12,6 +12,7 @@ export const EVENT_LOCATION = z.string().min(1, { message: 'Lokale er påkrevd' export const EVENT_CAPACITY = z.number().min(1, { message: 'Kapasitet må være større enn 0' }).optional(); export const EVENT_VISIBILITY_FROM_DT = z.string().min(1, { message: 'Synlig fra dato er påkrevd' }); export const EVENT_VISIBILITY_TO_DT = z.string().optional(); +export const EVENT_IS_HIDDEN = z.boolean().default(false); export const EVENT_PAID_OPTION = z.string().url().optional(); export const EVENT_BILLIG_ID = z.number().optional(); From 17da13e5ada51ad5ba19c2d4bebd830f47157317 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 10 Mar 2026 23:49:05 +0100 Subject: [PATCH 2/7] Fix bug to make hidden flag load state into form on edit --- .../PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx index efee992e5..0f60a2a2f 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx @@ -141,6 +141,7 @@ export function EventCreatorAdminPage() { visibility_from_dt: eventData.visibility_from_dt ? utcTimestampToLocal(eventData.visibility_from_dt, false) : '', + is_hidden: eventData.is_hidden || false, }); setShowSpinner(false); }) From c99a103bbd4e4b6366146941b4d3d92e5ff6e724 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 10 Mar 2026 23:49:23 +0100 Subject: [PATCH 3/7] Add translations for new is hidden field --- frontend/src/i18n/constants.ts | 3 +++ frontend/src/i18n/translations.ts | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 052742a4f..d4e8c7306 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -658,6 +658,9 @@ export const KEY = { error_server_error_description: 'error_server_error_description', error_submitting_reservation: 'error_submitting_reservation', error_invalid_reservation_data: 'error_invalid_reservation_data', + + event_form_is_hidden: 'event_form_is_hidden', + event_form_is_hidden_help: 'event_form_is_hidden_help', } as const; // This will ensure that each value matches the key exactly. diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index e9ec04582..90f0c95a9 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -673,6 +673,10 @@ export const nb = prepareTranslations({ [KEY.error_server_error_description]: 'En serverfeil har opptstått', [KEY.error_submitting_reservation]: 'Det skjedde en feil ved innsending av reservasjon', [KEY.error_invalid_reservation_data]: 'Ugyldig reservasjonsdata', + + [KEY.event_form_is_hidden]: 'Kun synlig via direktelenke', + [KEY.event_form_is_hidden_help]: + 'Arrangementet vil ikke vises i arrangementslisten for vanlige brukere, men kan nås via en direktelenke. Dette er nyttig for arrangementer som ikke er åpne for alle, eller for testing.', }); export const en = prepareTranslations({ @@ -1339,4 +1343,8 @@ export const en = prepareTranslations({ [KEY.error_server_error_description]: 'A server error has occurred', [KEY.error_submitting_reservation]: 'An error occurred while submitting the reservation', [KEY.error_invalid_reservation_data]: 'Invalid reservation data', + + [KEY.event_form_is_hidden]: 'Only visible via direct link', + [KEY.event_form_is_hidden_help]: + 'The event will not appear in the events list for regular users, but can be accessed via a direct link. This is useful for events that are not open to everyone or for testing purposes.', }); From 1324c5e67439ac48cf4a4e0946232fb3d63d4ea5 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 10 Mar 2026 23:50:29 +0100 Subject: [PATCH 4/7] Add event view filtering to hide hidden events --- backend/samfundet/utils.py | 16 ++++++++++++++++ backend/samfundet/view/event_views.py | 15 +++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/backend/samfundet/utils.py b/backend/samfundet/utils.py index de4f01be3..e5f1a1c49 100644 --- a/backend/samfundet/utils.py +++ b/backend/samfundet/utils.py @@ -1,10 +1,14 @@ from __future__ import annotations import datetime +from typing import TYPE_CHECKING from operator import or_ from functools import reduce from collections.abc import Callable +if TYPE_CHECKING: + from rest_framework.request import Request + from django.conf import settings from django.http import QueryDict from django.contrib import admin @@ -20,6 +24,18 @@ from .models.event import Event from .models.recruitment import Recruitment, OccupiedTimeslot, RecruitmentInterviewAvailability + +def visible_events_for_user(request: Request) -> QuerySet[Event]: + """Returns the base Event queryset for a request, filtering out hidden events unless the user has edit permission.""" + from root.custom_classes.permission_classes import has_required_permissions + + user = request.user + can_edit = user.is_authenticated and (user.has_perm('samfundet.change_event') or has_required_permissions(request, ['samfundet.change_event'])) + if can_edit: + return Event.objects.all() + return Event.objects.filter(is_hidden=False) + + SEARCH_FIELDS = ( 'title_nb__icontains', 'title_en__icontains', diff --git a/backend/samfundet/view/event_views.py b/backend/samfundet/view/event_views.py index fe319935f..7ceb2083d 100644 --- a/backend/samfundet/view/event_views.py +++ b/backend/samfundet/view/event_views.py @@ -6,7 +6,7 @@ from rest_framework import status from rest_framework.views import APIView from rest_framework.request import Request -from rest_framework.generics import CreateAPIView +from rest_framework.generics import QuerySet, CreateAPIView from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework.permissions import AllowAny, IsAuthenticated @@ -16,7 +16,7 @@ from root.constants import WebFeatures from root.custom_classes.permission_classes import FeatureEnabled, RoleProtectedOrAnonReadOnlyObjectPermissions -from samfundet.utils import event_query +from samfundet.utils import event_query, visible_events_for_user from samfundet.serializers import ( EventSerializer, EventGroupSerializer, @@ -41,6 +41,9 @@ class EventView(ModelViewSet): serializer_class = EventSerializer queryset = Event.objects.all() + def get_queryset(self) -> QuerySet[Event]: + return visible_events_for_user(self.request) + class EventPerDayView(APIView): permission_classes = [AllowAny] @@ -53,7 +56,11 @@ def get(self, request: Request) -> Response: # - where the event's visibility period has already begun # - where visibility period hasn't ended yet # - where status is "PUBLIC" - events = Event.objects.filter(start_dt__gt=now, visibility_from_dt__lte=now, visibility_to_dt__gte=now, status=EventStatus.PUBLIC).order_by('start_dt') + events = ( + Event.objects.filter(start_dt__gt=now, visibility_from_dt__lte=now, visibility_to_dt__gte=now, status=EventStatus.PUBLIC) + .filter(id__in=visible_events_for_user(request)) + .order_by('start_dt') + ) serialized = EventSerializer(events, many=True).data # Organize in date dictionary. @@ -70,7 +77,7 @@ class EventsUpcomingView(APIView): permission_classes = [AllowAny] def get(self, request: Request) -> Response: - events = event_query(query=request.query_params) + events = event_query(query=request.query_params, events=visible_events_for_user(request)) events = events.filter(start_dt__gt=timezone.now()).order_by('start_dt') serialized_events = EventSerializer(events, many=True).data From 51d6cf21f19924f012cddca1aa410bff4b248bcf Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 12 Mar 2026 20:11:38 +0100 Subject: [PATCH 5/7] Bump font size of s and xs text sizes --- frontend/src/constants/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/constants/constants.ts b/frontend/src/constants/constants.ts index 2f5e51778..1e0a965b7 100644 --- a/frontend/src/constants/constants.ts +++ b/frontend/src/constants/constants.ts @@ -72,8 +72,8 @@ export const DAY_MILLIS = 24 * HOUR_MILLIS; */ export const textSizes: Record = { - xs: '0.1rem', - s: '0.5rem', + xs: '0.5rem', + s: '0.75rem', m: '1rem', l: '2rem', xl: '3rem', From b48ec88dd744c4563df648e03702a59c93c6be6f Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 12 Mar 2026 20:12:16 +0100 Subject: [PATCH 6/7] Style hidden flag --- .../EventCreatorAdminPage.module.scss | 4 ++++ .../EventCreatorAdminPage/EventCreatorAdminPage.tsx | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss index 8369434ec..45afd8630 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss @@ -87,3 +87,7 @@ padding-bottom: 3.75em; gap: 0.5em; } + +.checkbox { + margin-top: 0.5em; +} diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx index 0f60a2a2f..9c6a7f350 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx @@ -10,6 +10,7 @@ import { toast } from 'react-toastify'; import type { z } from 'zod'; import { Button, + Checkbox, Dropdown, Form, FormControl, @@ -19,8 +20,8 @@ import { FormMessage, ImageCard, Input, + Text, Textarea, - ToggleSwitch, } from '~/Components'; import type { DropdownOption } from '~/Components/Dropdown/Dropdown'; import { ImagePicker } from '~/Components/ImagePicker/ImagePicker'; @@ -524,9 +525,11 @@ export function EventCreatorAdminPage() { {t(KEY.event_form_is_hidden)} - field.onChange(!field.value)} /> +
+ field.onChange(!field.value)} /> +
-

{t(KEY.event_form_is_hidden_help)}

+ {t(KEY.event_form_is_hidden_help)}
)} From 6e3f8d5f5ee306ee88134086ee5a63ecc277ba7e Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 12 Mar 2026 20:17:25 +0100 Subject: [PATCH 7/7] Change label and help text for hidden checkbox --- frontend/src/i18n/translations.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 90f0c95a9..ea1d474fc 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -674,9 +674,9 @@ export const nb = prepareTranslations({ [KEY.error_submitting_reservation]: 'Det skjedde en feil ved innsending av reservasjon', [KEY.error_invalid_reservation_data]: 'Ugyldig reservasjonsdata', - [KEY.event_form_is_hidden]: 'Kun synlig via direktelenke', + [KEY.event_form_is_hidden]: 'Gjør kun synlig via direktelenke', [KEY.event_form_is_hidden_help]: - 'Arrangementet vil ikke vises i arrangementslisten for vanlige brukere, men kan nås via en direktelenke. Dette er nyttig for arrangementer som ikke er åpne for alle, eller for testing.', + 'Arrangementet vil ikke vises i arrangementslisting for vanlige brukere, men kan nås via en direktelenke. Dette er nyttig for arrangementer som ikke skal være tilgjenlige for alle.', }); export const en = prepareTranslations({ @@ -1344,7 +1344,7 @@ export const en = prepareTranslations({ [KEY.error_submitting_reservation]: 'An error occurred while submitting the reservation', [KEY.error_invalid_reservation_data]: 'Invalid reservation data', - [KEY.event_form_is_hidden]: 'Only visible via direct link', + [KEY.event_form_is_hidden]: 'Make only visible via direct link', [KEY.event_form_is_hidden_help]: - 'The event will not appear in the events list for regular users, but can be accessed via a direct link. This is useful for events that are not open to everyone or for testing purposes.', + 'The event will not appear in the events listings for regular users, but can be accessed via a direct link. This is useful for events that should not be accessible.', });