diff --git a/.gitignore b/.gitignore index 4ecc402..f910067 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ -.idea/ -Palto/db.sqlite3 +.idea +venv +media +static-collected +db.sqlite3 diff --git a/Dockerfile b/Dockerfile index 3f8dd8e..dd06acd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,8 +16,5 @@ RUN pip install -r requirements.txt # Expose the port on which your Django application will run EXPOSE 80 -# Set the working directory in the Django application -WORKDIR ./Palto - # Start the Django application ENTRYPOINT ["python", "manage.py", "runserver_plus", "0.0.0.0:80"] diff --git a/Palto/Palto/admin.py b/Palto/Palto/admin.py new file mode 100644 index 0000000..c64a972 --- /dev/null +++ b/Palto/Palto/admin.py @@ -0,0 +1,63 @@ +from django.contrib import admin + +from .models import (Department, StudentGroup, TeachingUnit, StudentCard, TeachingSession, Attendance, Absence, + AbsenceAttachment, User) + + +# Register your models here. +@admin.register(User) +class AdminUser(admin.ModelAdmin): + list_display = ("id", "username", "email", "first_name", "last_name", "is_staff") + search_fields = ("id", "username", "email", "first_name", "last_name", "is_staff") + list_filter = ("is_staff",) + + +@admin.register(Department) +class AdminDepartment(admin.ModelAdmin): + list_display = ("id", "name") + search_fields = ("id", "name") + + +@admin.register(StudentGroup) +class AdminStudentGroup(admin.ModelAdmin): + list_display = ("id", "name") + search_fields = ("id", "name") + + +@admin.register(TeachingUnit) +class AdminTeachingUnit(admin.ModelAdmin): + list_display = ("id", "name") + search_fields = ("id", "name") + + +@admin.register(StudentCard) +class AdminStudentCard(admin.ModelAdmin): + list_display = ("id", "uid", "owner") + search_fields = ("id", "uid", "owner") + readonly_fields = ("uid",) + + +@admin.register(TeachingSession) +class AdminTeachingSession(admin.ModelAdmin): + list_display = ("id", "start", "end", "duration", "teacher") + search_fields = ("id", "start", "end", "duration", "teacher") + list_filter = ("start", "duration") + + +@admin.register(Attendance) +class AdminAttendance(admin.ModelAdmin): + list_display = ("id", "date", "student") + search_fields = ("id", "date", "student") + list_filter = ("date",) + + +@admin.register(Absence) +class AdminAbsence(admin.ModelAdmin): + list_display = ("id", "message", "student") + search_fields = ("id", "message", "student") + + +@admin.register(AbsenceAttachment) +class AdminAbsenceAttachment(admin.ModelAdmin): + list_display = ("id", "content", "absence") + search_fields = ("id", "content", "absence") diff --git a/Palto/Palto/apps.py b/Palto/Palto/apps.py new file mode 100644 index 0000000..93c779b --- /dev/null +++ b/Palto/Palto/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class PaltoConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = "Palto.Palto" + label = "Palto" diff --git a/Palto/Palto/migrations/__init__.py b/Palto/Palto/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Palto/Palto/models.py b/Palto/Palto/models.py new file mode 100644 index 0000000..841a76f --- /dev/null +++ b/Palto/Palto/models.py @@ -0,0 +1,210 @@ +import uuid +from datetime import datetime, timedelta + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AbstractUser +from django.db import models + + +# Create your models here. +class User(AbstractUser): + """ + A user. + + Same as the base Django user, but the id is now an uuid instead of an int. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + + def __repr__(self): + return f"<{self.__class__.__name__} id={str(self.id)[:8]} username={self.username!r}>" + + +class Department(models.Model): + """ + A scholar department. + + For example, a same server can handle both a science department and a sport department. + ALl have their own managers, teachers and student. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + name: str = models.CharField(max_length=64) + mail: str = models.EmailField() + + managers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="managing_departments") + teachers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="teaching_departments") + students = models.ManyToManyField(to=get_user_model(), blank=True, related_name="studying_departments") + + def __repr__(self): + return f"<{self.__class__.__name__} id={str(self.id)[:8]} name={self.name!r}>" + + def __str__(self): + return self.name + + +class StudentGroup(models.Model): + """ + A student group. + + This make selecting multiple students with a specificity easier. + + For example, if students are registered to an English course, + putting them in a same group make them easier to select. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + name: str = models.CharField(max_length=128) + + students = models.ManyToManyField(to=get_user_model(), blank=True, related_name="student_groups") + + def __repr__(self): + return f"<{self.__class__.__name__} id={str(self.id)[:8]} name={self.name!r}>" + + def __str__(self): + return self.name + + +class TeachingUnit(models.Model): + """ + A teaching unit. + + This represents a unit that can be taught to groups of student. + + For example, Maths, English, French, Computer Science are all teaching units. + The registered groups are groups of student allowed to participate in these units. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + name: str = models.CharField(max_length=64) + + department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="teaching_units") + + managers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="managing_units") + teachers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="teaching_units") + student_groups = models.ManyToManyField(to=StudentGroup, blank=True, related_name="studying_units") + + def __repr__(self): + return f"<{self.__class__.__name__} id={str(self.id)[:8]} name={self.name!r}>" + + def __str__(self): + return self.name + + +class StudentCard(models.Model): + """ + A student card. + + This represents a student NFC card. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + uid: bytes = models.BinaryField(max_length=7) + + owner: User = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="student_cards") + + def __repr__(self): + return f"<{self.__class__.__name__} id={str(self.id)[:8]} owner={self.owner.username!r}>" + + +class TeachingSession(models.Model): + """ + A session of a teaching unit. + + For example, a session of English would be a single course of this unit. + + It references a teacher responsible for scanning the student cards, student attendances and student absences. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + start: datetime = models.DateTimeField() + duration: timedelta = models.DurationField() + note: str = models.TextField(blank=True) + + unit = models.ForeignKey(to=TeachingUnit, on_delete=models.CASCADE, related_name="sessions") + + group = models.ForeignKey(to=StudentGroup, on_delete=models.CASCADE, related_name="teaching_sessions") + teacher = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="teaching_sessions") + + def __repr__(self): + return f"<{self.__class__.__name__} id={str(self.id)[:8]} unit={self.unit.name!r} start={self.start}>" + + def __str__(self): + return f"{self.unit.name} ({self.start})" + + @property + def end(self) -> datetime: + return self.start + self.duration + + +class Attendance(models.Model): + """ + A student attendance to a session. + + When a student confirm his presence to a session, this is represented by this model. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + date: datetime = models.DateTimeField() + + student: User = models.ForeignKey( + to=get_user_model(), + on_delete=models.CASCADE, + related_name="attended_sessions" + ) + session: TeachingSession = models.ForeignKey( + to=TeachingSession, + on_delete=models.CASCADE, + related_name="attendances" + ) + + def __repr__(self): + return ( + f"<{self.__class__.__name__} " + f"id={str(self.id)[:8]} " + f"student={self.student.username} " + f"session={str(self.session.id)[:8]}" + f">" + ) + + +class Absence(models.Model): + """ + A student justified absence to a session. + + When a student signal his absence to a session, this is represented by this model. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + message: str = models.TextField() + + student: User = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="absented_sessions") + session: TeachingSession = models.ManyToManyField(to=TeachingSession, blank=True, related_name="absences") + + def __repr__(self): + return ( + f"<{self.__class__.__name__} " + f"id={str(self.id)[:8]} " + f"student={self.student.username} " + f"session={str(self.session.id)[:8]}" + f">" + ) + + def __str__(self): + return f"[{str(self.id)[:8]}] {self.student}" + + +class AbsenceAttachment(models.Model): + """ + An attachment to a student justified absence. + + The student can add additional files to justify his absence. + """ + + id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) + content = models.FileField(upload_to="absence/attachment/") + + absence = models.ForeignKey(to=Absence, on_delete=models.CASCADE, related_name="attachments") + + def __repr__(self): + return f"<{self.__class__.__name__} id={str(self.id)[:8]} content={self.content!r}>" diff --git a/Palto/Palto/tests.py b/Palto/Palto/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Palto/Palto/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Palto/Palto/views.py b/Palto/Palto/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/Palto/Palto/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/Palto/__init__.py b/Palto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Palto/Palto/asgi.py b/Palto/asgi.py similarity index 100% rename from Palto/Palto/asgi.py rename to Palto/asgi.py diff --git a/Palto/Palto/settings.py b/Palto/settings.py similarity index 94% rename from Palto/Palto/settings.py rename to Palto/settings.py index 7021961..7d3d7cb 100644 --- a/Palto/Palto/settings.py +++ b/Palto/settings.py @@ -50,6 +50,8 @@ INSTALLED_APPS = [ 'rest_framework', "debug_toolbar", 'django_extensions', + + "Palto.Palto", ] MIDDLEWARE = [ @@ -129,7 +131,13 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "static-collected/" + +# Media files + +MEDIA_URL = "media/" +MEDIA_ROOT = BASE_DIR / "media/" # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field @@ -145,3 +153,6 @@ REST_FRAMEWORK = { 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' ] } + +# User model +AUTH_USER_MODEL = "Palto.User" diff --git a/Palto/Palto/urls.py b/Palto/urls.py similarity index 68% rename from Palto/Palto/urls.py rename to Palto/urls.py index fb88d33..17dadbc 100644 --- a/Palto/Palto/urls.py +++ b/Palto/urls.py @@ -15,16 +15,27 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include +from django.urls import path, include, re_path +from django.views.static import serve + +from Palto import settings + urlpatterns = [ # Application # ... # API - path('rest/', include('rest_framework.urls')), # API REST + path('api/', include('rest_framework.urls')), # API REST # Debug path('admin/', admin.site.urls), # Admin page path("__debug__/", include("debug_toolbar.urls")), # Debug toolbar ] + + +if settings.DEBUG: + urlpatterns += [ + re_path(r'^media/(?P.*)$', serve, {'document_root': settings.MEDIA_ROOT}), + re_path(r'^static/(?P.*)$', serve, {'document_root': settings.STATIC_ROOT}), + ] diff --git a/Palto/Palto/wsgi.py b/Palto/wsgi.py similarity index 100% rename from Palto/Palto/wsgi.py rename to Palto/wsgi.py diff --git a/Palto/manage.py b/manage.py old mode 100755 new mode 100644 similarity index 100% rename from Palto/manage.py rename to manage.py