Skip to content

Commit

Permalink
Merge branch 'master' into feature/fixauthenticated
Browse files Browse the repository at this point in the history
  • Loading branch information
magsyg authored Sep 26, 2023
2 parents 9e096a7 + e31ac3f commit 75be4e0
Show file tree
Hide file tree
Showing 73 changed files with 1,297 additions and 153 deletions.
2 changes: 2 additions & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ name = "pypi"

[scripts]
# See '/docs/pipenv.md'.
# Doesn't work for powershell.

"pipenv:install" = "pipenv install"
"pipenv:update" = "pipenv update"
"pipenv:sync" = "bash -c \"pipenv clean; pipenv sync --dev\""
Expand Down
16 changes: 16 additions & 0 deletions backend/root/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from contextvars import ContextVar

from django.http import HttpRequest


class Environment:
"""
Useful in eg. templates.
Expand All @@ -13,3 +18,14 @@ class Environment:

# Name of exposed csrf-token header in http traffic.
XCSRFTOKEN = 'X-CSRFToken'

# Name of cookie used for impersonation.
COOKIE_IMPERSONATED_USER_ID = 'impersonated_user_id'

# Name of attribute set on response for requested impersonation of user_id.
REQUESTED_IMPERSONATE_USER = 'requested_impersonate_user'

# This token can be imported anywhere to retrieve the values.
request_contextvar: ContextVar[HttpRequest] = ContextVar('request_contextvar', default=None)

AUTH_BACKEND = 'django.contrib.auth.middleware.AuthenticationMiddleware'
98 changes: 94 additions & 4 deletions backend/root/custom_classes/middlewares.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
from __future__ import annotations

import logging
import secrets
from contextvars import ContextVar

from django.http import HttpRequest, HttpResponse
from django.contrib.auth import login
from django.middleware.csrf import get_token

from root.constants import (
request_contextvar,
REQUESTED_IMPERSONATE_USER,
COOKIE_IMPERSONATED_USER_ID,
)

LOG = logging.getLogger(__name__)
from samfundet.models import User

# This token can be imported anywhere to retrieve the values.
request_contextvar: ContextVar[HttpRequest] = ContextVar('request_contextvar', default=None)
LOG = logging.getLogger('root.middlewares')


class RequestLogMiddleware:
Expand Down Expand Up @@ -41,3 +49,85 @@ def process_exception(self, request: HttpRequest, exception: Exception) -> None:
"""Log unhandled exceptions."""

LOG.error('Unhandled exception while processing request', exc_info=exception)


class ImpersonateUserMiddleware:

def __init__(self, get_response: HttpResponse) -> None:
self.get_response = get_response

def __call__(self, request: HttpRequest) -> HttpResponse:

### Handle impersonation before response ###
impersonate = request.get_signed_cookie(COOKIE_IMPERSONATED_USER_ID, default=None)
if impersonate is not None:
impersonated_user = User.objects.get(id=int(impersonate))
request.user = impersonated_user
request._force_auth_user = impersonated_user
request._force_auth_token = get_token(request)
LOG.info(f"EYOO DUDE YOUR'E NOT YOURSELF '{impersonated_user.username}'")
### End: Handle impersonation after response ###

# Handle response.
response: HttpResponse = self.get_response(request)

### Handle impersonation after response ###
if hasattr(response, REQUESTED_IMPERSONATE_USER):
impersonate_user_id = getattr(response, REQUESTED_IMPERSONATE_USER)
if impersonate_user_id is not None:
response.set_signed_cookie(COOKIE_IMPERSONATED_USER_ID, impersonate_user_id)
LOG.info(f'Now impersonating {impersonate_user_id}')
else:
response.delete_cookie(COOKIE_IMPERSONATED_USER_ID)
### End: Handle impersonation after response ###

return response


class ImpersonateUserMiddleware2:
"""wip Emil"""

def __init__(self, get_response: HttpResponse) -> None:
self.get_response = get_response

def __call__(self, request: HttpRequest) -> HttpResponse:
user: User = request.user
impersonated_user_id = request.get_signed_cookie(
key=COOKIE_IMPERSONATED_USER_ID,
default=None,
)

# TODO: if request.user.has_perm(perm=PERM.SAMFUNDET_IMPERSONATE) and impersonate_user:
# if user.is_superuser and impersonated_user_id:
if impersonated_user_id:
# Find user to impersonate.
impersonated_user = User.objects.get(id=int(impersonated_user_id))
# Keep actual user before it gets replaced.
impersonated_by = request.user

# Login (replaces request.user).
login(
request=request,
user=impersonated_user,
backend='django.contrib.auth.middleware.AuthenticationMiddleware',
)
# Set attr on current user to show impersonation.
impersonated_user._impersonated_by = impersonated_by
request.impersonated_by = impersonated_by
request.user = impersonated_user

# Handle response.
response = self.get_response(request)

### Handle impersonation after response ###
if hasattr(response, REQUESTED_IMPERSONATE_USER):
requested_impersonate_user_id = getattr(response, REQUESTED_IMPERSONATE_USER)

if requested_impersonate_user_id is not None:
response.set_signed_cookie(COOKIE_IMPERSONATED_USER_ID, requested_impersonate_user_id)
LOG.info(f'Now impersonating {requested_impersonate_user_id}')
else:
response.delete_cookie(COOKIE_IMPERSONATED_USER_ID)
### End: Handle impersonation after response ###

return response
4 changes: 2 additions & 2 deletions backend/root/custom_classes/request_context_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from django.http import HttpRequest

from .middlewares import request_contextvar
from root.constants import request_contextvar

LOG = logging.getLogger(__name__)
LOG = logging.getLogger('root.custom_classes')


class RequestContextFilter(logging.Filter):
Expand Down
2 changes: 2 additions & 0 deletions backend/root/management/commands/seed_scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
billig,
menu,
documents,
textitems,
example,
samf3,
recruitment,
Expand All @@ -32,6 +33,7 @@
('menu', menu.seed),
('documents', documents.seed),
('information_page', information_pages.seed),
('textitems', textitems.seed),
('blogposts', blogposts.seed),
('organization', oganizations.seed),
('recruitment', recruitment.seed),
Expand Down
44 changes: 44 additions & 0 deletions backend/root/management/commands/seed_scripts/textitems.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from samfundet.models.general import TextItem


def seed():
text_items = [
{
'key': 'welcome_message',
'text_nb': 'Velkommen til Studentersamfundet i Trondhjem!',
'text_en': 'Welcome to the Student Society in Trondheim!',
},
{
'key': 'upcoming_events',
'text_nb': 'Sjekk ut våre kommende arrangementer og konsertene vi har planlagt!',
'text_en': 'Check out our upcoming events and concerts we have planned!',
},
{
'key': 'join_us',
'text_nb': 'Bli medlem av Studentersamfundet og nyt godt av medlemsfordelene!',
'text_en': 'Join the Student Society and enjoy the benefits of membership!',
},
{
'key': 'volunteer',
'text_nb': 'Vil du bli frivillig? Bli med i vårt fantastiske team og bidra til studentkulturen i Trondheim!',
'text_en': 'Want to volunteer? Join our amazing team and contribute to the student culture in Trondheim!',
},
{
'key': 'about_us',
'text_nb': 'Studentersamfundet i Trondhjem er et kulturelt senter for studenter og en viktig del av studentlivet i Trondheim.',
'text_en': 'The Student Society in Trondheim is a cultural center for students and an essential part of student life in Trondheim.',
},
{
'key': 'contact_us',
'text_nb': 'Har du spørsmål eller ønsker å komme i kontakt med oss? Ikke nøl med å ta kontakt!',
'text_en': 'Do you have any questions or want to get in touch with us? Don"t hesitate to contact us!',
},
]

TextItem.objects.all().delete()
yield 0, 'Deleted old textitems'

for i, item in enumerate(text_items):
text_item, created = TextItem.objects.get_or_create(key=item['key'], text_nb=item['text_nb'], text_en=item['text_en'])
if created:
yield (100 * (i + 1) // len(text_items), f'Created {len(TextItem.objects.all())} textitems')
1 change: 1 addition & 0 deletions backend/root/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'root.custom_classes.middlewares.ImpersonateUserMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Expand Down
17 changes: 16 additions & 1 deletion backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
DO NOT WRITE IN THIS FILE, AS IT WILL BE OVERWRITTEN ON NEXT UPDATE.
THIS FILE WAS GENERATED BY: root.management.commands.generate_routes
LAST UPDATE: 2023-08-29 08:57:57.685488+00:00
LAST UPDATE: 2023-09-24 15:12:38.294245+00:00
"""

############################################################
Expand Down Expand Up @@ -344,6 +344,15 @@
admin__samfundet_organization_delete = 'admin:samfundet_organization_delete'
admin__samfundet_organization_change = 'admin:samfundet_organization_change'
adminsamfundetorganization__objectId = ''
admin__samfundet_interviewroom_permissions = 'admin:samfundet_interviewroom_permissions'
admin__samfundet_interviewroom_permissions_manage_user = 'admin:samfundet_interviewroom_permissions_manage_user'
admin__samfundet_interviewroom_permissions_manage_group = 'admin:samfundet_interviewroom_permissions_manage_group'
admin__samfundet_interviewroom_changelist = 'admin:samfundet_interviewroom_changelist'
admin__samfundet_interviewroom_add = 'admin:samfundet_interviewroom_add'
admin__samfundet_interviewroom_history = 'admin:samfundet_interviewroom_history'
admin__samfundet_interviewroom_delete = 'admin:samfundet_interviewroom_delete'
admin__samfundet_interviewroom_change = 'admin:samfundet_interviewroom_change'
adminsamfundetinterviewroom__objectId = ''
admin__samfundet_notification_changelist = 'admin:samfundet_notification_changelist'
admin__samfundet_notification_add = 'admin:samfundet_notification_add'
admin__samfundet_notification_history = 'admin:samfundet_notification_history'
Expand Down Expand Up @@ -404,6 +413,10 @@
samfundet__table_detail = 'samfundet:table-detail'
samfundet__text_item_list = 'samfundet:text_item-list'
samfundet__text_item_detail = 'samfundet:text_item-detail'
samfundet__interview_rooms_list = 'samfundet:interview_rooms-list'
samfundet__interview_rooms_detail = 'samfundet:interview_rooms-detail'
samfundet__infobox_list = 'samfundet:infobox-list'
samfundet__infobox_detail = 'samfundet:infobox-detail'
samfundet__key_value_list = 'samfundet:key_value-list'
samfundet__key_value_detail = 'samfundet:key_value-detail'
samfundet__organizations_list = 'samfundet:organizations-list'
Expand All @@ -424,12 +437,14 @@
samfundet__user = 'samfundet:user'
samfundet__groups = 'samfundet:groups'
samfundet__users = 'samfundet:users'
samfundet__impersonate = 'samfundet:impersonate'
samfundet__eventsperday = 'samfundet:eventsperday'
samfundet__eventsupcomming = 'samfundet:eventsupcomming'
samfundet__isclosed = 'samfundet:isclosed'
samfundet__home = 'samfundet:home'
samfundet__assign_group = 'samfundet:assign_group'
samfundet__recruitment_positions = 'samfundet:recruitment_positions'
samfundet__active_recruitment_positions = 'samfundet:active_recruitment_positions'
samfundet__applicants_without_interviews = 'samfundet:applicants_without_interviews/'
static__path = ''
media__path = ''
41 changes: 39 additions & 2 deletions backend/samfundet/admin.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
from django.urls import reverse
from django.contrib import admin
from django.utils.html import format_html
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import Permission, Group
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from guardian import models as guardian_models
from root.utils.routes import admin__samfundet_recruitmentadmission_change

from root.custom_classes.admin_classes import (
CustomGuardedUserAdmin,
CustomGuardedGroupAdmin,
CustomGuardedModelAdmin,
)
from .models.event import (Event, EventGroup, EventRegistration)
from .models.recruitment import (Recruitment, RecruitmentPosition, RecruitmentAdmission)
from .models.recruitment import (Recruitment, RecruitmentPosition, RecruitmentAdmission, InterviewRoom)
from .models.general import (
Tag,
User,
Expand Down Expand Up @@ -488,6 +491,25 @@ class RecruitmentAdmin(CustomGuardedModelAdmin):
list_select_related = True


class RecruitmentAdmissionInline(admin.TabularInline):
"""
Inline admin interface for RecruitmentAdmission.
Displays a link to the detailed admin page of each admission along with its user and applicant priority.
"""
model = RecruitmentAdmission
extra = 0
readonly_fields = ['linked_admission_text', 'user', 'applicant_priority']
fields = ['linked_admission_text', 'user', 'applicant_priority']

def linked_admission_text(self, obj: RecruitmentAdmission) -> str:
"""
Returns a clickable link leading to the admin change page of the RecruitmentAdmission instance.
"""
url = reverse(admin__samfundet_recruitmentadmission_change, args=[obj.pk])
return format_html('<a href="{url}">{obj}</a>', url=url, obj=obj.admission_text)


@admin.register(RecruitmentPosition)
class RecruitmentPositionAdmin(CustomGuardedModelAdmin):
sortable_by = [
Expand All @@ -496,11 +518,17 @@ class RecruitmentPositionAdmin(CustomGuardedModelAdmin):
'gang',
'id',
]
list_display = ['name_nb', 'is_funksjonaer_position', 'gang', 'id']
list_display = ['name_nb', 'is_funksjonaer_position', 'gang', 'id', 'admissions_count']
search_fields = ['name_nb', 'is_funksjonaer_position', 'gang', 'id']
filter_horizontal = ['interviewers']
list_select_related = True

inlines = [RecruitmentAdmissionInline]

def admissions_count(self, obj: RecruitmentPosition) -> int:
count = obj.admissions.all().count()
return count


@admin.register(RecruitmentAdmission)
class RecruitmentAdmissionAdmin(CustomGuardedModelAdmin):
Expand Down Expand Up @@ -539,4 +567,13 @@ class OrganizationAdmin(CustomGuardedModelAdmin):
list_select_related = True


@admin.register(InterviewRoom)
class InterviewRoomAdmin(CustomGuardedModelAdmin):
list_filter = ['name', 'location', 'recruitment', 'gang', 'start_time', 'end_time']
list_display = ['name', 'location', 'recruitment', 'gang', 'start_time', 'end_time']
search_fields = ['name', 'location', 'recruitment__name', 'gang__name']
list_display_links = ['name', 'location']
list_select_related = ['recruitment', 'gang']


### End: Our models ###
18 changes: 17 additions & 1 deletion backend/samfundet/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from samfundet.contants import DEV_PASSWORD
from samfundet.models.billig import BilligEvent
from samfundet.models.event import Event, EventAgeRestriction, EventTicketType
from samfundet.models.recruitment import Recruitment, RecruitmentPosition
from samfundet.models.recruitment import Recruitment, RecruitmentPosition, RecruitmentAdmission
from samfundet.models.general import User, Image, InformationPage, Organization, Gang, BlogPost

import root.management.commands.seed_scripts.billig as billig_seed
Expand Down Expand Up @@ -251,3 +251,19 @@ def fixture_blogpost(fixture_image: Image) -> Iterator[BlogPost]:
)
yield blogpost
blogpost.delete()


@pytest.fixture
def fixture_recruitment_admission(fixture_user: User, fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment: Recruitment) -> Iterator[RecruitmentAdmission]:
admission = RecruitmentAdmission.objects.create(
admission_text='Test admission text',
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
user=fixture_user,
applicant_priority=1,
recruiter_priority=RecruitmentAdmission.PRIORITY_CHOICES[0][0],
recruiter_status=RecruitmentAdmission.STATUS_CHOICES[0][0],
)
yield admission
admission.delete()
Loading

0 comments on commit 75be4e0

Please sign in to comment.