Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
29efa12
added functionallity for replacing images in admin images page. Delet…
Madt2 Feb 26, 2026
2c1f6bb
added list of events that are tied to an image, not interactable yet
Madt2 Mar 3, 2026
3cd059c
added link to events that is listed under image. Waiting for Edit Ima…
Madt2 Mar 3, 2026
fe2f876
Bump debugpy from 1.8.17 to 1.8.20 in /backend (#2053)
dependabot[bot] Feb 3, 2026
a8061c6
Bump django from 5.2.10 to 5.2.11 in /backend (#2055)
dependabot[bot] Feb 3, 2026
1b6308e
Bump bandit from 1.8.6 to 1.9.3 in /backend (#2047)
dependabot[bot] Feb 3, 2026
c3824f7
Bump tar from 7.5.6 to 7.5.7 in /frontend (#2043)
dependabot[bot] Feb 3, 2026
c646a7d
Bump mypy from 1.19.0 to 1.19.1 in /backend (#2050)
dependabot[bot] Feb 3, 2026
4ba2407
Fix positon of Events Header (#2045)
eilifhl Feb 3, 2026
052fd89
Bump axios from 1.13.2 to 1.13.5 in /frontend (#2062)
dependabot[bot] Feb 11, 2026
127b1ce
Bump pillow from 11.3.0 to 12.1.1 in /backend (#2059)
dependabot[bot] Feb 11, 2026
b1248b9
Bump i18next from 24.2.3 to 25.8.0 in /frontend (#2048)
dependabot[bot] Feb 11, 2026
8e0809e
docs: add contribution (brage)
BragonSB Feb 12, 2026
d482132
Alternative Login Picker (#2058)
eilifhl Feb 12, 2026
e7c2496
brage-feat: add ability to sort by buy in /events
BragonSB Feb 13, 2026
5955acf
Add mdb_medlem_id field to User (#2060)
robines Feb 14, 2026
737fe4e
Create mdb database (#2061)
robines Feb 14, 2026
c395350
Bump sqlparse from 0.5.3 to 0.5.4 in /backend (#2069)
dependabot[bot] Feb 14, 2026
b9dbff0
Add endpoint for calling sett_lim_utvidet_medlemsinfo (#2070)
robines Feb 14, 2026
41c5855
Fix event search for control panel and events page (#2063)
Lidavic Feb 16, 2026
56e2c2c
Improve styling of ExpandableHeader (#2067)
eilifhl Feb 16, 2026
d1249c2
Remove white text color from dark mode dropdown component (#2056)
aTrueYety Feb 17, 2026
c4db7a0
disable body scroll when modal component is active (#2066)
aTrueYety Feb 17, 2026
1381310
added my name as contributor (#2076)
leaesc Feb 19, 2026
2da85f1
breadcrum linked to profile page which was disabled, just reanabled i…
0xSpecter Feb 24, 2026
aefbf44
Fixed labels for categories on EventsAdminPage (#2044)
Madt2 Feb 24, 2026
baaaa8a
Add pagination to control panel ImageAdminPage (#2075)
aTrueYety Feb 24, 2026
3bb25a1
Add pagination and filtering to ImagePicker component (#2073)
aTrueYety Feb 26, 2026
654f199
feat: add Choose as the default value in the Dropdowns (#2084)
BragonSB Feb 26, 2026
4c665ff
1962 recreate samf3 navbar in samf 4 (#1966)
Madt2 Mar 5, 2026
f83438d
Add translatrions to LoginPicker (#2095)
aTrueYety Mar 9, 2026
fd3a4ba
Use toast instead of badge (#2082)
eilifhl Mar 9, 2026
87c7607
feat(event creation): make validate check every change
BragonSB Mar 10, 2026
cc120a8
sigurd/web-119-input-dropdowns-option-sort-in-alphabetic-order (#2099)
0xSpecter Mar 10, 2026
613eca2
sigurd/web-116-event-number-input-unable-to-remove-all-numbers (#2081)
0xSpecter Mar 10, 2026
1fabc2f
Leaesc/web 111 change event overview nb text bord to tabell (#2094)
leaesc Mar 12, 2026
a749646
Brage/web 92 create connect to mdb form (#2068)
BragonSB Mar 17, 2026
8128f38
Bump gunicorn from 23.0.0 to 25.1.0 in /backend (#2087)
dependabot[bot] Mar 17, 2026
0b72a1c
Bump psycopg from 3.3.2 to 3.3.3 in /backend (#2088)
dependabot[bot] Mar 17, 2026
8d1ccde
Bump bandit from 1.9.3 to 1.9.4 in /backend (#2089)
dependabot[bot] Mar 17, 2026
ed257d8
Bump rollup from 4.53.2 to 4.59.0 in /frontend (#2086)
dependabot[bot] Mar 17, 2026
48644e8
Bump immutable from 5.0.3 to 5.1.5 in /frontend (#2098)
dependabot[bot] Mar 17, 2026
9069d66
Bump tar from 7.5.7 to 7.5.11 in /frontend (#2109)
dependabot[bot] Mar 17, 2026
ad8b7de
Bump django from 5.2.11 to 5.2.12 in /backend (#2097)
dependabot[bot] Mar 17, 2026
7f40bfb
Bump django-environ from 0.12.0 to 0.13.0 in /backend (#2090)
dependabot[bot] Mar 17, 2026
06c6a11
Bump storybook from 8.6.15 to 8.6.17 in /frontend (#2083)
dependabot[bot] Mar 17, 2026
cffdf5e
Bump minimatch from 3.1.2 to 3.1.5 in /frontend (#2085)
dependabot[bot] Mar 17, 2026
54ba645
Bump flatted from 3.3.3 to 3.4.2 in /frontend (#2118)
dependabot[bot] Mar 24, 2026
b95f4aa
docs fix:) (#2106)
Lidavic Mar 24, 2026
790203e
Bump ruff from 0.11.6 to 0.15.4 in /backend (#2091)
dependabot[bot] Mar 24, 2026
6de996e
Bump stylelint-config-standard from 34.0.0 to 40.0.0 in /frontend (#2…
dependabot[bot] Mar 24, 2026
ab4f0d9
Use Translations in Schema Messages (#2105)
aTrueYety Mar 24, 2026
bc2d8a8
Resolve "Add pagination to event admin page" (#2072)
aTrueYety Mar 24, 2026
3778018
Fix editing event (#2054)
hei98 Mar 25, 2026
dea2aa3
Bump picomatch from 2.3.1 to 2.3.2 in /frontend (#2121)
dependabot[bot] Mar 29, 2026
a10a05e
Bump requests from 2.32.5 to 2.33.0 in /backend (#2122)
dependabot[bot] Mar 29, 2026
4d4730f
Bump path-to-regexp from 8.2.0 to 8.4.0 in /frontend (#2123)
dependabot[bot] Mar 29, 2026
66dc9c4
Only show public events on homepage (#2116)
robines Mar 29, 2026
4bd6446
Create FormSchemaContext, use in FormLabel to mark required fields (#…
robines Apr 1, 2026
341ac41
"Connect to MDB" page tweaks (#2127)
robines Apr 1, 2026
31ede6e
Bump pytest-django from 4.11.1 to 4.12.0 in /backend (#2132)
dependabot[bot] Apr 1, 2026
c577f35
Bump gunicorn from 25.1.0 to 25.3.0 in /backend (#2135)
dependabot[bot] Apr 1, 2026
b48b3ec
Bump pygments from 2.19.1 to 2.20.0 in /backend (#2129)
dependabot[bot] Apr 1, 2026
c6925b5
Bump mypy from 1.19.1 to 1.20.0 in /backend (#2133)
dependabot[bot] Apr 1, 2026
06223c3
Bump djangorestframework from 3.16.1 to 3.17.1 in /backend (#2137)
dependabot[bot] Apr 1, 2026
c641706
LoginPage: Replace SamfForm with React Hook Form (#2125)
robines Apr 1, 2026
188eb73
SignUpPage: Replace SamfForm with React Hook Form (#2126)
robines Apr 1, 2026
b87072f
Use RHF Input component in ImagePicker search (#2120)
robines Apr 1, 2026
2370287
Bump @vitejs/plugin-react-swc from 4.2.2 to 4.3.0 in /frontend (#2136)
dependabot[bot] Apr 1, 2026
9062711
fix: event -> events (#2138)
BragonSB Apr 3, 2026
e9cc381
Make NavbarSamfThree look more alike samf3 navbar (#2128)
robines Apr 4, 2026
afc7fb8
Bump vite from 6.4.1 to 6.4.2 in /frontend (#2141)
dependabot[bot] Apr 7, 2026
ae26b83
Added delete image functionality
Madt2 Apr 7, 2026
7c15be3
Added return to edit image page on saving an event.
Madt2 Apr 9, 2026
f0a78c8
Removed hover effect and changed items to hyperlinks for better redir…
Madt2 Apr 9, 2026
249d73f
Changed the image upload to samf's component, added field for changin…
Madt2 Apr 9, 2026
f1a8cef
Removed title and tags info from image, and change the button to icon
Madt2 Apr 9, 2026
86ffc8d
Added return to image gallery on succsessful edit.
Madt2 Apr 9, 2026
449b839
Pipeline fixes
Madt2 Apr 9, 2026
b3d4b2d
Merge branch 'master' into tor/web-70-edit-and-delete-images-in-contr…
Madt2 Apr 16, 2026
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
104 changes: 69 additions & 35 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,50 @@ class Meta:
fields = '__all__'


def _parse_tag_string(tag_string: str | None) -> list[str]:
"""Parse comma-separated tag string into deduplicated list."""
if not tag_string:
return []
tag_names = [name.strip() for name in tag_string.split(',')]
tag_names = [name for name in tag_names if name]
seen = set()
result = []
for name in tag_names:
if name not in seen:
seen.add(name)
result.append(name)
return result


def _get_or_create_tags(tag_names: list[str]) -> list:
"""Get or create Tag objects from list of names."""
tags = []
for name in tag_names:
tag = Tag.objects.filter(name=name).first() or Tag.objects.create(name=name)
tags.append(tag)
return tags


def _update_image_file(instance: Image, file: File, validated_data: dict) -> None:
"""Update the image file on an instance."""
try:
img = PilImage.open(file)
img.verify()
except (UnidentifiedImageError, Exception) as e:
raise ValidationError(f'Invalid image file: {str(e)}') from e
if instance.image:
instance.image.delete(save=False)
original_name = getattr(file, 'name', None) or validated_data.get('title', instance.title)
instance.image = ImageFile(file, original_name)


class ImageSerializer(CustomBaseSerializer):
# Read only tags used in frontend.
tags = TagSerializer(many=True, read_only=True)
url = serializers.SerializerMethodField(method_name='get_url', read_only=True)

# Write only fields for posting new images.
file = serializers.FileField(write_only=True, required=True)
file = serializers.FileField(write_only=True, required=False)
# Comma separated tag string "tag_a,tag_b" is automatically parsed to list of tag models.
tag_string = serializers.CharField(write_only=True, allow_blank=True, required=False)

Expand All @@ -102,53 +139,50 @@ def validate(self, attributes: dict) -> dict:
raise serializers.ValidationError({'title': 'This field is required.'})

file = attributes.get('file')
if not file:
# File is required only for create operations (POST), not for updates (PUT/PATCH)
# self.instance will be None for creation, and non-None for updates
if not file and not self.instance:
raise serializers.ValidationError({'file': 'An image file is required.'})

# Validate that the uploaded file is a valid image
try:
file.seek(0)
PilImage.open(file).verify()
file.seek(0)
except UnidentifiedImageError as error:
raise serializers.ValidationError('Invalid image') from error
# Validate that the uploaded file is a valid image if one is provided
if file:
try:
file.seek(0)
PilImage.open(file).verify()
file.seek(0)
except UnidentifiedImageError as error:
raise serializers.ValidationError('Invalid image') from error

return attributes

def create(self, validated_data: dict) -> Image:
"""
Uses the write_only file field to create new image file.
Automatically finds/creates new tags based on comma-separated string.
Strips whitespace and drops empty tag names
and preserves the original filename when saving the image
"""
"""Create new image with file and tags."""
file = validated_data.pop('file')
raw_tag_string = validated_data.pop('tag_string', None)
if raw_tag_string is not None:
# Split on comma, strip whitespace and drop empties
tag_names = [name.strip() for name in raw_tag_string.split(',')]
tag_names = [name for name in tag_names if name]

# De-duplicate while preserving order
seen: set[str] = set()
unique_tag_names = []
for name in tag_names:
if name not in seen:
seen.add(name)
unique_tag_names.append(name)
tags = [Tag.objects.get_or_create(name=name)[0] for name in unique_tag_names]
else:
tags = []
# Preserve original filename if available; fallback to title
tag_names = _parse_tag_string(raw_tag_string)
tags = _get_or_create_tags(tag_names)
original_name = getattr(file, 'name', None) or validated_data.get('title')
image = Image.objects.create(
image=ImageFile(file, original_name),
**validated_data,
)
image = Image.objects.create(image=ImageFile(file, original_name), **validated_data)
image.tags.set(tags)
image.save()
return image

def update(self, instance: Image, validated_data: dict) -> Image:
"""Update image metadata and optionally the image file."""
if 'file' in validated_data:
_update_image_file(instance, validated_data.pop('file'), validated_data)

raw_tag_string = validated_data.pop('tag_string', None)
if raw_tag_string is not None:
tag_names = _parse_tag_string(raw_tag_string)
tags = _get_or_create_tags(tag_names)
instance.tags.set(tags)

for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance

def get_url(self, image: Image) -> str:
return image.image.url

Expand Down
8 changes: 8 additions & 0 deletions backend/samfundet/view/general_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
InformationPageSerializer,
UserGangSectionRoleSerializer,
)
from samfundet.models.event import Event
from samfundet.models.general import (
Tag,
Gang,
Expand Down Expand Up @@ -110,6 +111,13 @@ class ImageView(ModelViewSet):
filter_backends = [SearchFilter]
search_fields = ['title']

@action(detail=True, methods=['get'])
def linked_events(self, request: Request, pk: int | None = None) -> Response:
"""Get all events that use this image."""
image = self.get_object()
events = Event.objects.filter(image=image).values('id', 'title_en', 'title_nb', 'start_dt')
return Response(data=list(events))


# Image tags
class TagView(ModelViewSet):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import classNames from 'classnames';
import { type ReactElement, type ReactNode, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { Button, Form } from '~/Components';
import type { DropdownOption } from '~/Components/Dropdown/Dropdown';
Expand Down Expand Up @@ -33,6 +33,8 @@ import { TextStep } from './steps/TextStep';

export function EventCreatorAdminPage() {
const { t } = useTranslation();
const location = useLocation();
const reactNavigate = useNavigate();
const [event, setEvent] = useState<Partial<EventDto>>();
const [showSpinner, setShowSpinner] = useState<boolean>(true);
const { id } = useParams();
Expand Down Expand Up @@ -87,6 +89,16 @@ export function EventCreatorAdminPage() {
}
}, [id, t]);

// Handle navigation after successful edit
useEffect(() => {
if (editEventMutation.isSuccess && id) {
const from = location.state?.from;
if (from) {
reactNavigate(from);
}
}
}, [editEventMutation.isSuccess, id, location.state, reactNavigate]);

// ================================== //
// Creation Steps //
// ================================== //
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
@use 'src/constants' as *;

@use 'src/mixins' as *;

.editContainer {
display: flex;
flex-direction: column;
gap: 2rem;
padding: 2rem;
}

.imageSection {
@include rounded;
@include shadow-light;
overflow: hidden;
display: flex;
flex-direction: column;
}

.image {
width: 100%;
max-height: 400px;
object-fit: cover;
}

.uploadSection {
@include rounded;
@include shadow-light;
padding: 2rem;

h4 {
margin: 0 0 1rem 0;
font-size: 1.1em;
}
}

.uploadForm {
display: flex;
flex-direction: column;
gap: 1rem;
}

.fileInput {
padding: 0.5rem;
border: 1px solid gray;
border-radius: 4px;
cursor: pointer;

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}

.fileName {
font-size: 0.9em;
color: gray;
margin: 0;
}

.container {
padding: 2rem;
text-align: center;
}

.linkedEventsSection {
@include rounded;
@include shadow-light;
padding: 2rem;
background-color: #fff3cd;
border-left: 4px solid #ffc107;

h4 {
margin: 0 0 1rem 0;
font-size: 1.1em;
color: #333333;
}
}

.eventsList {
margin: 0 0 1rem 0;
padding-left: 2rem;
list-style-type: disc;

li {
margin-bottom: 0.5rem;
color: #555555;
}
}

.eventItem {
cursor: pointer;
transition: all 0.2s ease;
padding: 0.25rem 0.5rem;
margin-left: -0.5rem;
border-radius: 3px;
}

.warningText {
margin: 1rem 0 0 0;
font-size: 0.95em;
color: #855a00;
font-style: italic;
}
Loading
Loading