Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ venv
.idea
.junie
.cursor
.DS_Store
.DS_Store
node_modules
69 changes: 42 additions & 27 deletions backend/backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Member(AbstractBaseUser, PermissionsMixin):
is_staff (BooleanField): Indicates if the member can log into the admin site.
name (CharField): The name of the member.
ssn (CharField): The social security number of the member.
study (ForeignKey): A reference to the member's study program.
study_program (ForeignKey): A reference to the member's study program.
registration_year (CharField): The year the member started studying at the TekNat faculty.
status (CharField): The membership status of the member, with choices including 'unknown', 'nonmember', 'member', and 'alumnus'.
"""
Expand Down Expand Up @@ -64,25 +64,28 @@ class Member(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(
max_length=255,
verbose_name=_("Email"),
help_text=_("Enter an email address that you want to connect to this account."),
help_text=_(
"Enter an email address that you want to connect to this account."
),
)

verified_email = models.BooleanField(default=False)

phone_number = models.CharField(
max_length=20,
verbose_name=_("Phone number"),
help_text=_("Enter a phone number that you want to connect to this account."),
help_text=_(
"Enter a phone number that you want to connect to this account."),
)

is_superuser = models.BooleanField(
help_text=("Designates whether the user is a superuser")
)
help_text=("Designates whether the user is a superuser"))

is_staff = models.BooleanField(
_("Staff status"),
default=False,
help_text=_("Designates whether the user can log into the admin site."),
help_text=_(
"Designates whether the user can log into the admin site."),
)

# Required by AbstractBaseUser
Expand All @@ -91,8 +94,7 @@ class Member(AbstractBaseUser, PermissionsMixin):
default=True,
help_text=_(
"Designates whether this user should be treated as active. "
"Unselect this instead of deleting accounts."
),
"Unselect this instead of deleting accounts."),
)

name = models.CharField(
Expand All @@ -117,11 +119,11 @@ class Member(AbstractBaseUser, PermissionsMixin):
registration_year = models.CharField(
max_length=4,
verbose_name=_("Registration year"),
help_text=_("Enter the year you started studying at the TekNat " "faculty"),
help_text=_("Enter the year you started studying at the TekNat "
"faculty"),
validators=[
validators.RegexValidator(
regex=r"^20\d{2}$", message=_("Please enter a valid year")
)
validators.RegexValidator(regex=r"^20\d{2}$",
message=_("Please enter a valid year"))
],
blank=True,
)
Expand Down Expand Up @@ -165,7 +167,7 @@ def has_module_perms(self, app_label):
@staticmethod
def find_user_by_ssn(ssn):
"""
Checks if a user exists in our db
Find a user from our db by ssn
"""
ssn = ssn.strip()

Expand All @@ -182,6 +184,19 @@ def find_user_by_ssn(ssn):

return None

@staticmethod
def find_user_by_email(email):
"""
Find a user from our db by email
"""
email = email.strip().lower()

user = Member.objects.filter(email__iexact=email).first()
if user is not None:
return user

return None


class Position(models.Model):
"""
Expand Down Expand Up @@ -222,8 +237,10 @@ class Position(models.Model):

term_from = models.DateField(verbose_name=("Date of appointment"))
term_end = models.DateField(verbose_name=("End date of the appointment"))
comment_eng = models.TextField(verbose_name=("Comment in English"), blank=True)
comment_sv = models.TextField(verbose_name=("Comment in Swedish"), blank=True)
comment_eng = models.TextField(verbose_name=("Comment in English"),
blank=True)
comment_sv = models.TextField(verbose_name=("Comment in Swedish"),
blank=True)


class Appointment(models.Model):
Expand Down Expand Up @@ -332,7 +349,9 @@ class Reference(models.Model):
blank=False,
)

name = models.CharField(max_length=255, verbose_name=_("Name"), blank=False)
name = models.CharField(max_length=255,
verbose_name=_("Name"),
blank=False)

phone_num = models.CharField(
max_length=20,
Expand Down Expand Up @@ -531,10 +550,8 @@ class Application(models.Model):
# ---- Application Information ------
cover_letter = models.TextField(
verbose_name=_("Cover Letter"),
help_text=_(
"""Present yourself and state why you are
who we are looking for"""
),
help_text=_("""Present yourself and state why you are
who we are looking for"""),
)
qualifications = models.TextField(
verbose_name=_("Qualifications"),
Expand All @@ -543,18 +560,16 @@ class Application(models.Model):
gdpr = models.BooleanField(
default=False,
verbose_name=("GDPR"),
help_text=_(
"""
help_text=_("""
I accept that my data is saved in accordance
with Uppsala Union of Engineering and Science Students integrity
policy that can be found within the link:
"""
),
"""),
)

decision_date = models.DateField(
verbose_name=_("Decision date"), null=True, blank=True
)
decision_date = models.DateField(verbose_name=_("Decision date"),
null=True,
blank=True)


class Role(models.Model):
Expand Down
File renamed without changes.
38 changes: 33 additions & 5 deletions backend/backend/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from .models import Position, Role, Member, Section, StudyProgram, Application, Reference

from .models import Application, Member, Position, Reference, Role
class SectionSerializer(ModelSerializer):
class Meta:
model = Section
fields = ["id", "abbreviation", "section_en", "section_sv"]
read_only_fields = ["id"]

class StudyProgramSerializer(ModelSerializer):
section = SectionSerializer(read_only=True)

class Meta:
model = StudyProgram
fields = ["id", "name_en", "name_sv", "section"]
read_only_fields = ["id"]

class StudyProgramSerializer(ModelSerializer):
"""StudyProgram without nested section for use in SectionWithProgramsSerializer"""
class Meta:
model = StudyProgram
fields = ["id", "name_en", "name_sv", "section"]
read_only_fields = ["id"]

class SectionWithProgramsSerializer(ModelSerializer):
"""Section with nested study programs"""
programs = StudyProgramSerializer(source="study_programs", many=True, read_only=True)

class Meta:
model = Section
fields = ["id", "abbreviation", "section_en", "section_sv", "programs"]
read_only_fields = ["id"]

class MemberSerializer(ModelSerializer):
"""
Expand All @@ -17,12 +45,12 @@ class MemberSerializer(ModelSerializer):
Creates a new member instance, sets the password, and sends a verification email.
Also invalidates any previous verification tokens for the user.
Returns the created member instance.

"""

study_program = StudyProgramSerializer(read_only=True)
class Meta:
model = Member
fields = ("ssn", "email", "password", "is_active", "is_staff", "verified_email")
fields = ("name", "phone_number", "study_program", "registration_year", "status", "ssn", "email", "password", "is_active", "is_staff", "verified_email")
extra_kwargs = {
"password": {"write_only": True},
# Debateable if we want to expose these fields
Expand All @@ -41,8 +69,8 @@ def create(self, validated_data):
user.set_password(password)
user.save()

# Email verification here

# TODO: Email verification here
return user


Expand Down
17 changes: 11 additions & 6 deletions backend/backend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter

from .views import ChangeEmailAPIView # Authentication
from .views import (
ApplicationViewSet,
ChangeEmailAPIView,
ChangePasswordAPIView,
EmailVerificationAPIView,
InitiatePasswordResetViewAPIView,
LoginAPIView,
LogoutAPIView,
MyAccountAPIView,
PasswordResetAPIView,
PositionViewSet,
ResendVerificationEmailAPIView,
SectionsAPIView,
SignupAPIView,
)

Expand All @@ -21,16 +23,15 @@
router.register("positions", PositionViewSet)
router.register("applications", ApplicationViewSet, basename="application")


urlpatterns = [
path("api/", include(router.urls)),
path("api/", include(router.urls)),
path("api/auth/login", LoginAPIView.as_view(), name="login"),
path("api/auth/logout", LogoutAPIView.as_view(), name="logout"),
path("api/auth/signup", SignupAPIView.as_view(), name="signup"),
path(
"api/auth/verify-email", EmailVerificationAPIView.as_view(), name="verify-email"
),
path("api/auth/verify-email",
EmailVerificationAPIView.as_view(),
name="verify-email"),
path(
"api/auth/resend-verification-email",
ResendVerificationEmailAPIView.as_view(),
Expand All @@ -47,5 +48,9 @@
ChangePasswordAPIView.as_view(),
name="change-password",
),
path("api/auth/change-email", ChangeEmailAPIView.as_view(), name="change-email"),
path("api/auth/change-email",
ChangeEmailAPIView.as_view(),
name="change-email"),
path('api/account/', MyAccountAPIView.as_view(), name='my-account'),
path('api/sections/', SectionsAPIView.as_view(), name='sections'),
]
83 changes: 78 additions & 5 deletions backend/backend/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet

from .email import send_password_reset_email, send_verification_email
from .models import Application, Position
from .models import Application, Position, Member, Section, StudyProgram
from .send_email import send_password_reset_email, send_verification_email
from .permissions import CanCreatePosition
from .serializers import (
ApplicationSerializer,
CreatePositionSerializer,
ListApplicationSerializer,
MemberSerializer,
PositionSerializer,
SectionWithProgramsSerializer,
)

# Create your views here.
Expand Down Expand Up @@ -56,7 +57,11 @@ def post(self, request):
email = request.data.get("email")
password = request.data.get("password")

user = authenticate(request, username=email, password=password)
member = Member.find_user_by_email(email)
if member is None:
return Response({"message": "Invalid credentials"}, status=401)

user = authenticate(request, username=member.ssn, password=password)

if user is not None:
if user.is_active and user.verified_email:
Expand Down Expand Up @@ -416,8 +421,6 @@ class ApplicationViewSet(ModelViewSet):
- Destroy: Delete own application (only if draft status)
"""

permission_classes = [IsAuthenticated]

def get_queryset(self):
"""Get all applications with optimized queries"""
queryset = Application.objects.select_related(
Expand Down Expand Up @@ -484,3 +487,73 @@ def list(self, request):
"my_positions": self.get_serializer(my_positions, many=True).data,
}
)

class MyAccountAPIView(APIView):
"""
MyAccount handles retrieving the authenticated user's account information.

Methods
-------
get(request)
Retrieve the authenticated user's account information.

Returns
-------
Responds with HTTP 200
When account information is retrieved successfully.
"""

permission_classes = [IsAuthenticated]

def get(self, request):
serializer = MemberSerializer(request.user)
return Response(serializer.data, status=200)

def post(self, request):
user = request.user

# Handle study_program update if 'program' is provided in request
if 'program' in request.data:
program_id = request.data.pop('program')
try:
program = StudyProgram.objects.get(id=program_id)
user.study_program = program
user.save()
except StudyProgram.DoesNotExist:
return Response(
{'program': ['Invalid study program ID']},
status=400
)

serializer = MemberSerializer(user, data=request.data, partial=True)

if serializer.is_valid():
serializer.save()
return Response({
'message': 'Account updated successfully',
'user': serializer.data
}, status=200)

return Response(serializer.errors, status=400)


class SectionsAPIView(APIView):
"""
SectionsAPIView returns all sections with their associated study programs.

Methods
-------
get(request)
Retrieve all sections with nested study programs.

Returns
-------
Responds with HTTP 200
When sections are retrieved successfully.
"""
permission_classes = [AllowAny]

def get(self, request):
sections = Section.objects.prefetch_related('study_programs').all()
serializer = SectionWithProgramsSerializer(sections, many=True)
return Response(serializer.data, status=200)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading