Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions backend/samfundet/migrations/0006_event_is_hidden.py
Original file line number Diff line number Diff line change
@@ -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',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hvorfor kan ikke status feltet i Event brukes til dette formålet? Der kan man sette status til "privat".

Status sjekkes forsåvidt ikke i homepage, fikses i denne PRen.

Det mangler også et felt i frontend admin til å sette status

field=models.BooleanField(default=False),
),
]
5 changes: 5 additions & 0 deletions backend/samfundet/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
# ======================== #
Expand Down
16 changes: 16 additions & 0 deletions backend/samfundet/utils.py
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

burde ikke trenge denne ifen siden type hinting eval blir forsinka av from __future__ import annotations

from django.conf import settings
from django.http import QueryDict
from django.contrib import admin
Expand All @@ -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',
Expand Down
15 changes: 11 additions & 4 deletions backend/samfundet/view/event_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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]
Expand All @@ -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.
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,7 @@
padding-bottom: 3.75em;
gap: 0.5em;
}

.checkbox {
margin-top: 0.5em;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { toast } from 'react-toastify';
import type { z } from 'zod';
import {
Button,
Checkbox,
Dropdown,
Form,
FormControl,
Expand All @@ -19,6 +20,7 @@ import {
FormMessage,
ImageCard,
Input,
Text,
Textarea,
} from '~/Components';
import type { DropdownOption } from '~/Components/Dropdown/Dropdown';
Expand Down Expand Up @@ -102,6 +104,7 @@ export function EventCreatorAdminPage() {
image: undefined,
visibility_from_dt: '',
visibility_to_dt: '',
is_hidden: false,
},
});

Expand Down Expand Up @@ -139,6 +142,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);
})
Expand Down Expand Up @@ -498,20 +502,39 @@ export function EventCreatorAdminPage() {
return !!data.visibility_from_dt;
},
template: (
<FormField
control={form.control}
name="visibility_from_dt"
key="visibility_from_dt"
render={({ field }) => (
<FormItem className={styles.form_item}>
<FormLabel>{t(KEY.saksdokumentpage_publication_date) ?? ''}</FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<>
<FormField
control={form.control}
name="visibility_from_dt"
key="visibility_from_dt"
render={({ field }) => (
<FormItem className={styles.form_item}>
<FormLabel>{t(KEY.saksdokumentpage_publication_date) ?? ''}</FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="is_hidden"
key="is_hidden"
render={({ field }) => (
<FormItem className={styles.form_item}>
<FormLabel>{t(KEY.event_form_is_hidden)}</FormLabel>
<FormControl>
<div className={styles.checkbox}>
<Checkbox checked={field.value} onChange={() => field.onChange(!field.value)} />
</div>
</FormControl>
<Text size="s">{t(KEY.event_form_is_hidden_help)}</Text>
<FormMessage />
</FormItem>
)}
/>
</>
),
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EVENT_DURATION,
EVENT_END_DT,
EVENT_HOST,
EVENT_IS_HIDDEN,
EVENT_LOCATION,
EVENT_REGISTRATION_URL,
EVENT_START_DT,
Expand Down Expand Up @@ -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<typeof eventSchema>;
4 changes: 2 additions & 2 deletions frontend/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ export const DAY_MILLIS = 24 * HOUR_MILLIS;
*/

export const textSizes: Record<string, string> = {
xs: '0.1rem',
s: '0.5rem',
xs: '0.5rem',
s: '0.75rem',
m: '1rem',
l: '2rem',
xl: '3rem',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]: 'Gjør kun synlig via direktelenke',
[KEY.event_form_is_hidden_help]:
'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({
Expand Down Expand Up @@ -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]: 'Make only visible via direct link',
[KEY.event_form_is_hidden_help]:
'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.',
});
1 change: 1 addition & 0 deletions frontend/src/schema/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading