Skip to content

Commit

Permalink
Merge branch 'master' of github.com:Samfundet/Samfundet4 into admissi…
Browse files Browse the repository at this point in the history
…on_page
  • Loading branch information
JohanneD committed Sep 28, 2023
2 parents 4709dab + 5bdfd15 commit 468d649
Show file tree
Hide file tree
Showing 59 changed files with 928 additions and 143 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
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
14 changes: 13 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-09-04 15:22:16.450169+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,8 @@
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'
Expand All @@ -426,6 +437,7 @@
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'
Expand Down
11 changes: 10 additions & 1 deletion backend/samfundet/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
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 @@ -567,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()
17 changes: 17 additions & 0 deletions backend/samfundet/migrations/0037_alter_user_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.3 on 2023-09-19 13:14

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0036_venue_slug'),
]

operations = [
migrations.AlterModelOptions(
name='user',
options={'permissions': [('debug', 'Can view debug mode'), ('impersonate', 'Can impersonate users')]},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.3 on 2023-09-24 15:26

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0037_alter_user_options'),
]

operations = [
migrations.AlterField(
model_name='recruitmentadmission',
name='interview_location',
field=models.CharField(blank=True, help_text='Where the intevjuee should wait', max_length=100, null=True),
),
migrations.CreateModel(
name='InterviewRoom',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Name of the room', max_length=255)),
('location', models.CharField(help_text='Physical location, eg. campus', max_length=255)),
('start_time', models.DateTimeField(help_text='Start time of availability')),
('end_time', models.DateTimeField(help_text='End time of availability')),
('gang', models.ForeignKey(blank=True, help_text='The gang that booked the room', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='samfundet.gang')),
('recruitment', models.ForeignKey(help_text='The recruitment that is recruiting', on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='samfundet.recruitment')),
],
),
migrations.AddField(
model_name='recruitmentadmission',
name='room',
field=models.ForeignKey(blank=True, help_text='Room where the interview is held', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interviews', to='samfundet.interviewroom'),
),
]
13 changes: 10 additions & 3 deletions backend/samfundet/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
# This is required for registering user model in auth
from .general import (
User,
Profile,
UserPreference,
Gang,
Image,
Profile,
UserPreference,
)

from .event import (
Event,
)

__all__ = ['User', 'Profile', 'UserPreference', 'Event', 'Gang', 'Image']
__all__ = [
'User',
'Gang',
'Event',
'Image',
'Profile',
'UserPreference',
]
11 changes: 11 additions & 0 deletions backend/samfundet/models/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class User(AbstractUser):
class Meta:
permissions = [
('debug', 'Can view debug mode'),
('impersonate', 'Can impersonate users'),
]

def has_perm(self, perm: str, obj: Optional[Model] = None) -> bool:
Expand All @@ -113,6 +114,16 @@ def has_perm(self, perm: str, obj: Optional[Model] = None) -> bool:
has_object_perm = super().has_perm(perm=perm, obj=obj)
return has_global_perm or has_object_perm

@property
def is_impersonated(self) -> bool:
return self._impersonated_by is not None

@property
def impersonated_by(self) -> User:
if not self.is_impersonated:
raise Exception('Real user not available unless currently impersonated.')
return self._impersonated_by


class UserPreference(models.Model):
"""Group all preferences and config per user."""
Expand Down
Loading

0 comments on commit 468d649

Please sign in to comment.