Skip to content

Commit

Permalink
Merge branch 'master' into feature/registration
Browse files Browse the repository at this point in the history
  • Loading branch information
magsyg authored Sep 21, 2023
2 parents 31b9170 + 3281422 commit fa831ca
Show file tree
Hide file tree
Showing 31 changed files with 699 additions and 23 deletions.
22 changes: 22 additions & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[scripts]
# See '/docs/pipenv.md'.
"pipenv:install" = "pipenv install"
"pipenv:update" = "pipenv update"
"pipenv:sync" = "bash -c \"pipenv clean; pipenv sync --dev\""
"pipenv:docker-install-dev" = "pipenv install --deploy --ignore-pipfile --dev" # 'deploy' means abort if outdated lock file. 'ignore-pipfile' means only install using the lock file. 'dev' means install dev dependencies.
"pipenv:docker-install-prod" = "pipenv install --deploy --ignore-pipfile" # 'deploy' means abort if outdated lock file. 'ignore-pipfile' means only install using the lock file.
"pipenv:outdated" = "pipenv update --outdated" # Show outdated dependencies.
"pipenv:graph" = "pipenv graph" # Show dependency graph.
"pipenv:where" = "pipenv --where" # Show location of virtual environment.
"pipenv:rm" = "pipenv --rm" # Completely remove virtual environment.
"pipenv:shell" = "pipenv shell" # Opens a shell within the virtual environment.
"mypy:run" = "pipenv run mypy --config-file mypy.ini ."
"migrations:verify" = "pipenv run python manage.py makemigrations --check --dry-run --noinput --verbosity 2"
"bandit:run" = "pipenv run bandit --recursive --ini .bandit ."
"flake8:run" = "pipenv run flake8 --config=.flake8 ."
"yapf:diff" = "pipenv run yapf --parallel --recursive --diff ." # Dry-run yapf on all files in the project.
"yapf:apply" = "pipenv run yapf --parallel --recursive -i ." # Applies yapf to all files in the project.
"pytest:run" = "pipenv run pytest"
"pipeline:run" = "bash -c \"pipenv run mypy:run && pipenv run yapf:diff && pipenv run migrations:verify && pipenv run bandit:run && pipenv run flake8:run && pipenv run pytest:run\""
"seed:run" = "pipenv run python manage.py seed"

[packages]
django = "*"
pytest = "*"
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')
23 changes: 22 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-17 17:48:38.961443+00:00
LAST UPDATE: 2023-09-04 15:22:16.450169+00:00
"""

############################################################
Expand Down Expand Up @@ -308,18 +308,36 @@
admin__samfundet_keyvalue_delete = 'admin:samfundet_keyvalue_delete'
admin__samfundet_keyvalue_change = 'admin:samfundet_keyvalue_change'
adminsamfundetkeyvalue__objectId = ''
admin__samfundet_recruitment_permissions = 'admin:samfundet_recruitment_permissions'
admin__samfundet_recruitment_permissions_manage_user = 'admin:samfundet_recruitment_permissions_manage_user'
admin__samfundet_recruitment_permissions_manage_group = 'admin:samfundet_recruitment_permissions_manage_group'
admin__samfundet_recruitment_changelist = 'admin:samfundet_recruitment_changelist'
admin__samfundet_recruitment_add = 'admin:samfundet_recruitment_add'
admin__samfundet_recruitment_history = 'admin:samfundet_recruitment_history'
admin__samfundet_recruitment_delete = 'admin:samfundet_recruitment_delete'
admin__samfundet_recruitment_change = 'admin:samfundet_recruitment_change'
adminsamfundetrecruitment__objectId = ''
admin__samfundet_recruitmentposition_permissions = 'admin:samfundet_recruitmentposition_permissions'
admin__samfundet_recruitmentposition_permissions_manage_user = 'admin:samfundet_recruitmentposition_permissions_manage_user'
admin__samfundet_recruitmentposition_permissions_manage_group = 'admin:samfundet_recruitmentposition_permissions_manage_group'
admin__samfundet_recruitmentposition_changelist = 'admin:samfundet_recruitmentposition_changelist'
admin__samfundet_recruitmentposition_add = 'admin:samfundet_recruitmentposition_add'
admin__samfundet_recruitmentposition_history = 'admin:samfundet_recruitmentposition_history'
admin__samfundet_recruitmentposition_delete = 'admin:samfundet_recruitmentposition_delete'
admin__samfundet_recruitmentposition_change = 'admin:samfundet_recruitmentposition_change'
adminsamfundetrecruitmentposition__objectId = ''
admin__samfundet_recruitmentadmission_permissions = 'admin:samfundet_recruitmentadmission_permissions'
admin__samfundet_recruitmentadmission_permissions_manage_user = 'admin:samfundet_recruitmentadmission_permissions_manage_user'
admin__samfundet_recruitmentadmission_permissions_manage_group = 'admin:samfundet_recruitmentadmission_permissions_manage_group'
admin__samfundet_recruitmentadmission_changelist = 'admin:samfundet_recruitmentadmission_changelist'
admin__samfundet_recruitmentadmission_add = 'admin:samfundet_recruitmentadmission_add'
admin__samfundet_recruitmentadmission_history = 'admin:samfundet_recruitmentadmission_history'
admin__samfundet_recruitmentadmission_delete = 'admin:samfundet_recruitmentadmission_delete'
admin__samfundet_recruitmentadmission_change = 'admin:samfundet_recruitmentadmission_change'
adminsamfundetrecruitmentadmission__objectId = ''
admin__samfundet_organization_permissions = 'admin:samfundet_organization_permissions'
admin__samfundet_organization_permissions_manage_user = 'admin:samfundet_organization_permissions_manage_user'
admin__samfundet_organization_permissions_manage_group = 'admin:samfundet_organization_permissions_manage_group'
admin__samfundet_organization_changelist = 'admin:samfundet_organization_changelist'
admin__samfundet_organization_add = 'admin:samfundet_organization_add'
admin__samfundet_organization_history = 'admin:samfundet_organization_history'
Expand Down Expand Up @@ -386,6 +404,8 @@
samfundet__table_detail = 'samfundet:table-detail'
samfundet__text_item_list = 'samfundet:text_item-list'
samfundet__text_item_detail = 'samfundet:text_item-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 Down Expand Up @@ -413,5 +433,6 @@
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 = ''
30 changes: 29 additions & 1 deletion backend/samfundet/admin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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,
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
47 changes: 45 additions & 2 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,25 @@ class Meta:
fields = '__all__'


class UserForRecruitmentSerializer(serializers.ModelSerializer):
recruitment_admission_ids = serializers.SerializerMethodField()

class Meta:
model = User
fields = [
'id',
'first_name',
'last_name',
'username',
'email',
'recruitment_admission_ids', # Add this to the fields list
]

def get_recruitment_admission_ids(self, obj: User) -> list[int]:
"""Return list of recruitment admission IDs for the user."""
return RecruitmentAdmission.objects.filter(user=obj).values_list('id', flat=True)


class RecruitmentPositionSerializer(serializers.ModelSerializer):

class Meta:
Expand All @@ -518,14 +537,38 @@ class Meta:
fields = [
'admission_text',
'recruitment_position',
'user',
'applicant_priority',
'interview_time',
'interview_location',
]

def create(self, validated_data: dict) -> RecruitmentAdmission:
recruitment_position = validated_data['recruitment_position']
recruitment = recruitment_position.recruitment
user = self.context['request'].user
applicant_priority = 1

recruitment_admission = RecruitmentAdmission.objects.create(
admission_text=validated_data.get('admission_text'),
recruitment_position=recruitment_position,
recruitment=recruitment,
user=user,
applicant_priority=applicant_priority,
interview_time=validated_data.get('interview_time'),
interview_location=validated_data.get('interview_location')
)

return recruitment_admission


class ApplicantInfoSerializer(serializers.ModelSerializer):

class Meta:
model = User
fields = ['id', 'first_name', 'last_name', 'email']


class RecruitmentAdmissionForGangSerializer(serializers.ModelSerializer):
user = ApplicantInfoSerializer(read_only=True)

class Meta:
model = RecruitmentAdmission
Expand Down
1 change: 1 addition & 0 deletions backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@
########## Recruitment ##########
path('recruitment-positions/', views.RecruitmentPositionsPerRecruitmentView.as_view(), name='recruitment_positions'),
path('active-recruitment-positions/', views.ActiveRecruitmentPositionsView.as_view(), name='active_recruitment_positions'),
path('applicants-without-interviews/', views.ApplicantsWithoutInterviewsView.as_view(), name='applicants_without_interviews/'),
]
31 changes: 31 additions & 0 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Type

from django.db.models import Count, Case, When
from django.contrib.auth import login, logout
from django.contrib.auth.models import Group
from django.db.models import QuerySet
Expand Down Expand Up @@ -78,6 +79,7 @@
FoodPreferenceSerializer,
UserPreferenceSerializer,
InformationPageSerializer,
UserForRecruitmentSerializer,
RecruitmentPositionSerializer,
RecruitmentAdmissionForGangSerializer,
RecruitmentAdmissionForApplicantSerializer,
Expand Down Expand Up @@ -448,6 +450,13 @@ class RecruitmentPositionView(ModelViewSet):
queryset = RecruitmentPosition.objects.all()


@method_decorator(ensure_csrf_cookie, 'dispatch')
class RecruitmentAdmissionView(ModelViewSet):
permission_classes = [AllowAny]
serializer_class = RecruitmentAdmissionForGangSerializer
queryset = RecruitmentAdmission.objects.all()


@method_decorator(ensure_csrf_cookie, 'dispatch')
class RecruitmentPositionsPerRecruitmentView(ListAPIView):
permission_classes = [AllowAny]
Expand All @@ -465,9 +474,30 @@ def get_queryset(self) -> Response:
return None


class ApplicantsWithoutInterviewsView(ListAPIView):
permission_classes = [AllowAny]
serializer_class = UserForRecruitmentSerializer

def get_queryset(self) -> QuerySet[User]:
"""
Optionally restricts the returned positions to a given recruitment,
by filtering against a `recruitment` query parameter in the URL.
"""
recruitment = self.request.query_params.get('recruitment', None)
if recruitment is None:
return User.objects.none() # Return an empty queryset instead of None

# Exclude users who have any admissions for the given recruitment that have an interview_time
users_without_interviews = User.objects.filter(admissions__recruitment=recruitment).annotate(
num_interviews=Count(Case(When(admissions__recruitment=recruitment, then='admissions__interview_time'), default=None, output_field=None))
).filter(num_interviews=0)
return users_without_interviews


class RecruitmentAdmissionForApplicantView(ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = RecruitmentAdmissionForApplicantSerializer
queryset = RecruitmentAdmission.objects.all()

def list(self, request: Request) -> Response:
"""
Expand Down Expand Up @@ -495,6 +525,7 @@ def list(self, request: Request) -> Response:
class RecruitmentAdmissionForGangView(ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = RecruitmentAdmissionForGangSerializer
queryset = RecruitmentAdmission.objects.all()

# TODO: User should only be able to edit the fields that are allowed

Expand Down
Loading

0 comments on commit fa831ca

Please sign in to comment.