Skip to content

Commit

Permalink
Merge branch 'master' into robin/edit-interview-availability-page
Browse files Browse the repository at this point in the history
  • Loading branch information
robines committed Nov 16, 2024
2 parents cad3cb4 + 7f4b8b9 commit d12dd22
Show file tree
Hide file tree
Showing 101 changed files with 1,811 additions and 290 deletions.
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"yzhang.markdown-all-in-one",
"stylelint.vscode-stylelint",
"ms-python.mypy-type-checker",
"visualstudioexptteam.vscodeintellicode"
"visualstudioexptteam.vscodeintellicode",
"ms-vscode-remote.vscode-remote-extensionpack"
]
}
83 changes: 43 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,58 @@

<img src="./docs/splash.png"/>

## Documentation
## Introduction

- **[Technical Documentation](./docs/technical/README.md)**
- [Work Methodology](./docs/work-methodology.md)
- [Useful Commands](./docs/useful-commands.md)
- [Technologies used on Samf4 🤖](./docs/technical/Samf4Tech.md)
- [Project Specific Commands](./docs/docker-project-specific-commands.md)
- [Useful Docker aliases](./docs/docker-project-specific-commands.md)
- [🌐 API documentation](./docs/api-docs.md)
Samfundet4 is the latest and greatest iteration of samfundet.no. It's built using Django and React.


## Documentation Overview

> [!TIP]
> If you're new, start by going through the [Introduction to Samfundet4](./docs/introduction.md) guide.
## Installation
### Frontend

We have a script that handles all installation for you. To run the script, a Github Personal Access Token (PAT) is required.
You can make one here https://github.com/settings/tokens/new. Tick scopes `repo`, `read:org` and `admin:public_key`),
then store the token somewhere safe (Github will never show it again).
- [Creating react components (conventions)](./docs/technical/frontend/components.md)
- [Forms and schemas](./docs/technical/frontend/forms.md)
- [*Deprecated: SamfForm*](./docs/technical/frontend/samfform.md)
- [Cypress Setup Documentation](./docs/technical/frontend/cypress.md)
- [Data fetching and State management](./docs/technical/frontend/data-fetching.md)

Copy these commands (press button on the right-hand side of the block)
and run from the directory you would clone the project.
### Backend

```sh
# Interactive
read -s -p "Github PAT token: " TOKEN ; X_INTERACTIVE=y /bin/bash -c "$(curl -fsSL https://$TOKEN@raw.githubusercontent.com/Samfundet/Samfundet4/master/{bash_utils.sh,install.sh})" && . ~/.bash_profile && cd Samfundet4; unset TOKEN; unset X_INTERACTIVE;
```
- [🌐 API documentation](./docs/api-docs.md)
- [Billig (payment system)](./docs/technical/backend/billig.md)
- [Seed scripts](./docs/technical/backend/seed.md)
- [Role system](./docs/technical/backend/rolesystem.md)

### Other

- [Automatic Interview Scheduling](./docs/intervew-scheduling.md)

### Workflow

<details>
<summary>Non-interactive (show/hide)</summary>
- [Work Methodology](./docs/work-methodology.md)
- How to contribute to the project
- [Useful Commands](./docs/useful-commands.md)
- [Useful Docker aliases](./docs/docker-project-specific-commands.md)
- [Common error messages](./docs/common-errors.md)

```sh
# Non-interactive
read -s -p "Github PAT token: " TOKEN ; X_INTERACTIVE=n /bin/bash -c "$(curl -fsSL https://$TOKEN@raw.githubusercontent.com/Samfundet/Samfundet4/master/{bash_utils.sh,install.sh})" && . ~/.bash_profile && cd Samfundet4; unset TOKEN; unset X_INTERACTIVE;
```
### Pipelines & Deployment

<!--
cd ~/my-projects/test; rm -rf Samfundet4; read -s -p "Github PAT token: " TOKEN ; X_INTERACTIVE=y /bin/bash -c "$(curl -fsSL https://[email protected]/Samfundet/Samfundet4/master/{bash_utils.sh,install.sh})" && . ~/.bash_profile && cd Samfundet4; unset TOKEN; unset X_INTERACTIVE;
-->
</details>
- [Pipeline (mypy, Biome, tsc, ...)](./docs/technical/pipeline.md)

<details>
<summary>Flags explained (show/hide)</summary>
### Install

> - X_INTERACTIVE (y/n): determines how many prompts you receive before performing an action.
> curl:
> - -f: fail fast
> - -s: silent, no progress-meter
> - -S: show error on fail
> - -L: follow redirect
- Linux: [Docker](./docs/install/linux-docker.md)[Native](./docs/install/linux-native.md)
- MacOS: [Docker](./docs/install/mac-docker.md)[Native](./docs/install/mac-native.md)
- Windows: [Docker](./docs/install/windows-docker.md)[WSL](./docs/install/windows-wsl.md)
- [Install script](./docs/install/install-script.md)
- [Post-install instructions](./docs/install/post-install.md)

</details>
### Editor configuration

<br>
<br>
<br>
* [JetBrains (WebStorm, PyCharm, etc...)](./docs/editors/jetbrains.md)
* [VS Code](./docs/editors/vscode.md)
* [Vim/Neovim](./docs/editors/vim.md)
* [Emacs](./docs/editors/emacs.md)
3 changes: 3 additions & 0 deletions backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@
samfundet__merch_detail = 'samfundet:merch-detail'
samfundet__role_list = 'samfundet:role-list'
samfundet__role_detail = 'samfundet:role-detail'
samfundet__role_users = 'samfundet:role-users'
samfundet__recruitment_list = 'samfundet:recruitment-list'
samfundet__recruitment_detail = 'samfundet:recruitment-detail'
samfundet__recruitment_gangs = 'samfundet:recruitment-gangs'
Expand All @@ -564,6 +565,7 @@
samfundet__interview_list = 'samfundet:interview-list'
samfundet__interview_detail = 'samfundet:interview-detail'
samfundet__api_root = 'samfundet:api-root'
samfundet__api_root = 'samfundet:api-root'
samfundet__schema = 'samfundet:schema'
samfundet__swagger_ui = 'samfundet:swagger_ui'
samfundet__redoc = 'samfundet:redoc'
Expand Down Expand Up @@ -607,5 +609,6 @@
samfundet__recruitment_availability = 'samfundet:recruitment_availability'
samfundet__feedback = 'samfundet:feedback'
samfundet__purchase_feedback = 'samfundet:purchase_feedback'
samfundet__gang_application_stats = 'samfundet:gang-application-stats'
static__path = ''
media__path = ''
57 changes: 56 additions & 1 deletion backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from root.constants import PHONE_NUMBER_REGEX
from root.utils.mixins import CustomBaseSerializer

from .models.role import Role
from .models.role import Role, UserOrgRole, UserGangRole, UserGangSectionRole
from .models.event import Event, EventGroup, EventCustomTicket, PurchaseFeedbackModel, PurchaseFeedbackQuestion, PurchaseFeedbackAlternative
from .models.billig import BilligEvent, BilligPriceGroup, BilligTicketGroup
from .models.general import (
Expand All @@ -40,6 +40,7 @@
KeyValue,
MenuItem,
TextItem,
GangSection,
Reservation,
ClosedPeriod,
FoodCategory,
Expand Down Expand Up @@ -426,6 +427,12 @@ class Meta:
fields = '__all__'


class GangSectionSerializer(CustomBaseSerializer):
class Meta:
model = GangSection
fields = '__all__'


class RecruitmentGangSerializer(CustomBaseSerializer):
recruitment_positions = serializers.SerializerMethodField(method_name='get_positions_count', read_only=True)

Expand Down Expand Up @@ -499,6 +506,54 @@ class Meta:
fields = '__all__'


class UserOrgRoleSerializer(CustomBaseSerializer):
user = UserSerializer()
org_role = serializers.SerializerMethodField()

class Meta:
model = UserOrgRole
fields = ('user', 'org_role')

def get_org_role(self, obj: UserOrgRole) -> dict:
return {
'created_at': obj.created_at,
'created_by': UserSerializer(obj.created_by).data,
'organization': OrganizationSerializer(obj.obj).data,
}


class UserGangRoleSerializer(CustomBaseSerializer):
user = UserSerializer()
gang_role = serializers.SerializerMethodField()

class Meta:
model = UserGangRole
fields = ('user', 'gang_role')

def get_gang_role(self, obj: UserGangRole) -> dict:
return {
'created_at': obj.created_at,
'created_by': UserSerializer(obj.created_by).data,
'gang': GangSerializer(obj.obj).data,
}


class UserGangSectionRoleSerializer(CustomBaseSerializer):
user = UserSerializer()
section_role = serializers.SerializerMethodField()

class Meta:
model = UserGangSectionRole
fields = ('user', 'section_role')

def get_section_role(self, obj: UserGangSectionRole) -> dict:
return {
'created_at': obj.created_at,
'created_by': UserSerializer(obj.created_by).data,
'section': GangSectionSerializer(obj.obj).data,
}


class SaksdokumentSerializer(CustomBaseSerializer):
# Read only url file path used in frontend
url = serializers.SerializerMethodField(method_name='get_url', read_only=True)
Expand Down
1 change: 1 addition & 0 deletions backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,5 @@
path('recruitment/<int:id>/availability/', views.RecruitmentAvailabilityView.as_view(), name='recruitment_availability'),
path('feedback/', views.UserFeedbackView.as_view(), name='feedback'),
path('purchase-feedback/', views.PurchaseFeedbackView.as_view(), name='purchase_feedback'),
path('recruitment/<int:recruitment_id>/gang/<int:gang_id>/stats/', views.GangApplicationCountView.as_view(), name='gang-application-stats'),
]
9 changes: 9 additions & 0 deletions backend/samfundet/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ def event_query(*, query: QueryDict, events: QuerySet[Event] = None) -> QuerySet
return events


def user_query(*, query: QueryDict, users: QuerySet[User] = None) -> QuerySet[User]:
if not users:
users = User.objects.all()
search = query.get('search', None)
if search:
users = users.filter(Q(username__icontains=search) | Q(first_name__icontains=search) | Q(last_name__icontains=search))
return users


def generate_timeslots(start_time: datetime.time, end_time: datetime.time, interval_minutes: int) -> list[str]:
# Convert from datetime.time objects to datetime.datetime
start_datetime = datetime.datetime.combine(datetime.datetime.today(), start_time)
Expand Down
47 changes: 45 additions & 2 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import hmac
import hashlib
from typing import Any
from itertools import chain

from guardian.shortcuts import get_objects_for_user

Expand Down Expand Up @@ -38,9 +39,9 @@
REQUESTED_IMPERSONATE_USER,
)

from .utils import event_query, generate_timeslots, get_occupied_timeslots_from_request
from .utils import user_query, event_query, generate_timeslots, get_occupied_timeslots_from_request
from .homepage import homepage
from .models.role import Role
from .models.role import Role, UserOrgRole, UserGangRole, UserGangSectionRole
from .serializers import (
TagSerializer,
GangSerializer,
Expand All @@ -67,11 +68,13 @@
EventGroupSerializer,
PermissionSerializer,
RecruitmentSerializer,
UserOrgRoleSerializer,
ClosedPeriodSerializer,
FoodCategorySerializer,
OrganizationSerializer,
SaksdokumentSerializer,
UserFeedbackSerializer,
UserGangRoleSerializer,
InterviewRoomSerializer,
FoodPreferenceSerializer,
UserPreferenceSerializer,
Expand All @@ -82,6 +85,7 @@
ReservationCheckSerializer,
UserForRecruitmentSerializer,
RecruitmentPositionSerializer,
UserGangSectionRoleSerializer,
RecruitmentStatisticsSerializer,
RecruitmentForRecruiterSerializer,
RecruitmentSeparatePositionSerializer,
Expand Down Expand Up @@ -133,6 +137,7 @@
Recruitment,
InterviewRoom,
OccupiedTimeslot,
RecruitmentGangStat,
RecruitmentPosition,
RecruitmentStatistics,
RecruitmentApplication,
Expand Down Expand Up @@ -324,6 +329,22 @@ class RoleView(ModelViewSet):
serializer_class = RoleSerializer
queryset = Role.objects.all()

@action(detail=True, methods=['get'])
def users(self, request: Request, pk: int) -> Response:
role = get_object_or_404(Role, id=pk)

org_roles = UserOrgRole.objects.filter(role=role).select_related('user', 'obj')
gang_roles = UserGangRole.objects.filter(role=role).select_related('user', 'obj')
section_roles = UserGangSectionRole.objects.filter(role=role).select_related('user', 'obj')

org_data = UserOrgRoleSerializer(org_roles, many=True).data
gang_data = UserGangRoleSerializer(gang_roles, many=True).data
section_data = UserGangSectionRoleSerializer(section_roles, many=True).data

combined = list(chain(org_data, gang_data, section_data))

return Response(combined)


# =============================== #
# Sulten #
Expand Down Expand Up @@ -472,6 +493,10 @@ class AllUsersView(ListAPIView):
serializer_class = UserSerializer
queryset = User.objects.all()

def get(self, request: Request) -> Response:
users = user_query(query=request.query_params)
return Response(data=UserSerializer(users, many=True).data)


class ImpersonateView(APIView):
permission_classes = [IsAuthenticated] # TODO: Permission check.
Expand Down Expand Up @@ -1342,3 +1367,21 @@ def post(self, request: Request) -> Response:
form=purchase_model,
)
return Response(status=status.HTTP_201_CREATED, data={'message': 'Feedback submitted successfully!'})


class GangApplicationCountView(APIView):
permission_classes = [IsAuthenticated]

def get(self, request: Request, recruitment_id: int, gang_id: int) -> Response:
# Get total applications from RecruitmentGangStat
gang_stat = get_object_or_404(RecruitmentGangStat, gang_id=gang_id, recruitment_stats__recruitment_id=recruitment_id)

return Response(
{
'total_applications': gang_stat.application_count,
'total_applicants': gang_stat.applicant_count,
'average_priority': gang_stat.average_priority,
'total_accepted': gang_stat.total_accepted,
'total_rejected': gang_stat.total_rejected,
}
)
12 changes: 8 additions & 4 deletions docs/api-docs.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[**&larr; Back: Documentation Overview**](../README.md#documentation-overview)

# API docs

API docs are generated by [drf-spectacular](https://drf-spectacular.readthedocs.io/en/latest/readme.html).
Expand All @@ -6,14 +8,16 @@ API documentation is available as two different interfaces:

[Swagger-UI](http://localhost:8000/schema/swagger-ui/#/) or [Redoc](http://localhost:8000/schema/redoc/)



🐋 _When backend server is running_

## API schema file

If you want a schema file for the API you can go to [http://localhost:8000/schema/](http://localhost:8000/schema/).

A schema file will be downloaded which can be used for multiple purposes, like sharing API documentation, or to generate code for recreating or testing the API.
A schema file will be downloaded which can be used for multiple purposes, like sharing API documentation, or to generate
code for recreating or testing the API.

> 💡 Note: You might encounter some error messages during this process. These errors are typically related to drf-spectacular not being able to parse certain views in views.py. However, the tool will still attempt to generate the documentation, though the results might not be fully comprehensive.
> [!NOTE]
> You might encounter some error messages during this process. These errors are typically related to drf-spectacular not
> being able to parse certain views in views.py. However, the tool will still attempt to generate the documentation,
> though the results might not be fully comprehensive.
4 changes: 3 additions & 1 deletion docs/common-errors.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[**&larr; Back: Documentation Overview**](../README.md#documentation-overview)

# Common error messages

## Rule of thumb
Expand All @@ -20,4 +22,4 @@ exec /app/entrypoint.sh: no such file or directory
Cannot connect to the Docker daemon at ../../.../default/docker.sock. Is the docker daemon running?
```
### Fix:
Make sure docker desktop is running (Windows) or run `colima start`on Mac.
Make sure docker desktop is running (Windows) or run `colima start` (or start Docker Desktop) on Mac.
3 changes: 0 additions & 3 deletions docs/dependencies.md

This file was deleted.

Loading

0 comments on commit d12dd22

Please sign in to comment.