Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2fe9078
Integrated unfold admin & added important models fields to filter, di…
M97Chahboun Feb 11, 2024
c91f8ea
Fixed custom widgets conflict issue
M97Chahboun Feb 11, 2024
7000d5c
Revert database configurations
M97Chahboun Feb 13, 2024
c986c0c
Added django-unfold to pipfile
M97Chahboun Feb 13, 2024
b034ec7
Configured tailwind for use on admin custom widgets
M97Chahboun Feb 15, 2024
f8b5c77
format code
M97Chahboun Feb 15, 2024
c515aba
Merge branch 'main' of https://github.com/DataTalksClub/course-manage…
M97Chahboun Feb 15, 2024
08f419a
Revert static configuration
M97Chahboun Feb 15, 2024
d8db149
Fixed minor issues on course admin
M97Chahboun Feb 15, 2024
6e5fe70
Removed tailwind styling
M97Chahboun Feb 15, 2024
3a8eaf1
Merge branch 'main' of https://github.com/DataTalksClub/course-manage…
M97Chahboun Mar 22, 2024
d2c65ac
pipfile updated
alexeygrigorev Mar 23, 2024
7a70996
removed the account admin and modified the titles for the course admin
alexeygrigorev Mar 23, 2024
3df3029
Merge branch 'DataTalksClub:main' into main
M97Chahboun Mar 27, 2024
75df497
Added instructor to Course model
M97Chahboun Mar 27, 2024
61f73ec
Added Access control for admins / course instructors
M97Chahboun Mar 27, 2024
979ce37
Merge branch 'main' of https://github.com/DataTalksClub/course-manage…
M97Chahboun Apr 14, 2024
048c5cd
Added instructor field to course model
M97Chahboun Apr 14, 2024
4272fad
Updated course instructor field to manytomany field
M97Chahboun Apr 14, 2024
cab2860
Merge remote-tracking branch 'origin' into M97Chahboun/main
alexeygrigorev May 29, 2024
150a71a
Used ForeignKey instead of ManyToManyField
M97Chahboun Jun 27, 2024
7aedd6b
Merge branch 'main' of https://github.com/DataTalksClub/course-manage…
M97Chahboun Dec 3, 2024
32ad834
Added instructor add field migration
M97Chahboun Dec 3, 2024
8f7bbe9
Fixed add new user from admin issue
M97Chahboun Dec 3, 2024
3a650d9
Merge branch 'DataTalksClub:main' into main
M97Chahboun Jan 27, 2025
e72f2aa
Merge branch 'DataTalksClub:main' into main
M97Chahboun Oct 4, 2025
1e03046
Removed old admin setup
M97Chahboun Oct 4, 2025
4f0daab
Adds InstructorAccessMixin to limit access to objects based on instru…
M97Chahboun Oct 4, 2025
9636a28
InstructorAccessMixin Integration with admin models
M97Chahboun Oct 4, 2025
6507408
merge migrations
M97Chahboun Oct 4, 2025
666c0db
improved mixin
M97Chahboun Oct 4, 2025
2635a3b
Adds InstructorAccessMixin tests
M97Chahboun Oct 4, 2025
b7f74d1
Adds sidebar items
M97Chahboun Oct 4, 2025
45ba232
Merge branch 'main' into main
M97Chahboun Oct 6, 2025
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
9 changes: 7 additions & 2 deletions accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

from django import forms
from django.contrib import admin

from unfold.admin import ModelAdmin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser, Token
from unfold.forms import UserCreationForm, AdminPasswordChangeForm, UserChangeForm


class CustomUserAdmin(admin.ModelAdmin):
class CustomUserAdmin(UserAdmin, ModelAdmin):
search_fields = ["email"]
change_form_template = 'loginas/change_form.html'
form = UserChangeForm
add_form = UserCreationForm
change_password_form = AdminPasswordChangeForm


admin.site.register(CustomUser, CustomUserAdmin)
Expand Down
85 changes: 83 additions & 2 deletions course_management/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pathlib import Path

import dj_database_url
from django.templatetags.static import static
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _


Expand Down Expand Up @@ -319,4 +319,85 @@
"SITE_HEADER": _("Course Management"),
"SITE_TITLE": _("Course Management"),
"SITE_SYMBOL": "school",
}
"SIDEBAR": {
"show_search": True,
"show_all_applications": True,
"navigation": [
{
"title": _("Account"),
"items": [
{
"title": "Users",
"icon": "person",
"link": reverse_lazy(
"admin:accounts_customuser_changelist"
),
"permission": lambda request: request.user.is_superuser,
},
{
"title": "Groups",
"icon": "groups",
"link": reverse_lazy(
"admin:auth_group_changelist"
),
"permission": lambda request: request.user.is_superuser,
},
{
"title": "Tokens",
"icon": "key",
"link": reverse_lazy(
"admin:accounts_token_changelist"
),
"permission": lambda request: request.user.is_superuser,
},
{
"title": "Email addresses",
"icon": "email",
"link": reverse_lazy(
"admin:account_emailaddress_changelist"
),
"permission": lambda request: request.user.is_superuser,
},
],
},
{
"title": _("Courses"),
"items": [
{
"title": "Home",
"icon": "home",
"link": reverse_lazy("admin:index"),
},
{
"title": "Courses",
"icon": "school",
"link": reverse_lazy(
"admin:courses_course_changelist"
),
},
{
"title": "Homeworks",
"icon": "assignment",
"link": reverse_lazy(
"admin:courses_homework_changelist"
),
},
{
"title": "Projects",
"icon": "folder_open",
"link": reverse_lazy(
"admin:courses_project_changelist"
),
},
{
"title": "Review criterias",
"icon": "checklist",
"link": reverse_lazy(
"admin:courses_reviewcriteria_changelist"
),
},
],
},
],
},
}
1 change: 0 additions & 1 deletion courses/admin.py

This file was deleted.

22 changes: 20 additions & 2 deletions courses/admin/course.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Any
from django import forms
from django.contrib import admin
from django.http import HttpRequest
from django.utils import timezone
from unfold.admin import ModelAdmin, TabularInline
from unfold.widgets import (
Expand All @@ -9,6 +11,7 @@

from django.contrib import messages

from courses.mixin import InstructorAccessMixin
from courses.models import Course, ReviewCriteria
from courses.scoring import update_leaderboard

Expand Down Expand Up @@ -98,7 +101,22 @@ def duplicate_course(modeladmin, request, queryset):


@admin.register(Course)
class CourseAdmin(ModelAdmin):
class CourseAdmin(InstructorAccessMixin, ModelAdmin):
actions = [update_leaderboard_admin, duplicate_course]
inlines = [CriteriaInline]
list_display = ["title", "visible", "finished"]
list_display = ["title", "slug", "finished", "visible"]

instructor_field = "instructor"

def get_form(
self,
request: HttpRequest,
obj: Any | None = ...,
change: bool = ...,
**kwargs: Any,
) -> forms.ModelForm:
form = super().get_form(request, obj, change, **kwargs)
if not request.user.is_superuser:
form.base_fields["instructor"].initial = request.user
form.base_fields["instructor"].widget = forms.HiddenInput()
return form
5 changes: 4 additions & 1 deletion courses/admin/homework.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)
from django.contrib import messages

from courses.mixin import InstructorAccessMixin
from courses.models import Homework, Question, HomeworkState

from courses.scoring import (
Expand Down Expand Up @@ -95,7 +96,7 @@ def calculate_statistics_selected_homeworks(


@admin.register(Homework)
class HomeworkAdmin(ModelAdmin):
class HomeworkAdmin(InstructorAccessMixin, ModelAdmin):
inlines = [QuestionInline]
actions = [
score_selected_homeworks,
Expand All @@ -104,3 +105,5 @@ class HomeworkAdmin(ModelAdmin):
]
list_display = ["title", "course", "due_date", "state"]
list_filter = ["course__slug"]

instructor_field = "course__instructor"
12 changes: 9 additions & 3 deletions courses/admin/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.contrib import messages

from courses.mixin import InstructorAccessMixin
from courses.models import Project, ReviewCriteria, ProjectState

from courses.projects import (
Expand Down Expand Up @@ -77,7 +78,7 @@ def calculate_statistics_selected_projects(


@admin.register(Project)
class ProjectAdmin(ModelAdmin):
class ProjectAdmin(InstructorAccessMixin, ModelAdmin):
actions = [
assign_peer_reviews_for_project_admin,
score_projects_admin,
Expand All @@ -87,7 +88,12 @@ class ProjectAdmin(ModelAdmin):
list_display = ["title", "course", "state"]
list_filter = ["course__slug"]

instructor_field = "course__instructor"


@admin.register(ReviewCriteria)
class ReviewCriteriaAdmin(ModelAdmin):
pass
class ReviewCriteriaAdmin(InstructorAccessMixin, ModelAdmin):
list_display = ["course", "description", "review_criteria_type"]
list_filter = ["course"]

instructor_field = "course__instructor"
27 changes: 27 additions & 0 deletions courses/migrations/0019_course_instructor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.14 on 2024-12-03 19:49

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("courses", "0018_course_finished"),
]

operations = [
migrations.AddField(
model_name="course",
name="instructor",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
related_name="courses_teaching",
to=settings.AUTH_USER_MODEL,
),
preserve_default=False,
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generated by Django 5.2.4 on 2025-10-04 18:21

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
('courses', '0019_course_instructor'),
('courses', '0022_projectstatistics'),
]

operations = []
46 changes: 46 additions & 0 deletions courses/mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
class InstructorAccessMixin:
instructor_field = "instructor"

def get_queryset(self, request):
"""Filter queryset based on instructor field for non-superusers."""
qs = super().get_queryset(request)
if not request.user.is_superuser:
return qs.filter(**{self.instructor_field: request.user})
return qs

def formfield_for_foreignkey(
self, db_field, request, obj=None, **kwargs
):
"""
Filter foreign key querysets based on instructor field.
Supports both direct fields (e.g., 'instructor') and
related fields (e.g., 'course__instructor').
"""
# Parse the instructor_field to handle relationships
parts = self.instructor_field.split("__")
formfield = super().formfield_for_foreignkey(
db_field, request, **kwargs
)
# For direct instructor field on current model (no relationship traversal needed)
if len(parts) == 1:
return formfield

# For related fields (e.g., course__instructor)
# parts[0] is the foreign key field name, parts[1:] is the lookup path
fk_field_name = parts[0]
lookup_path = "__".join(parts[1:])

queryset = formfield.queryset

# Only apply filtering if this is the related foreign key field
if (
db_field.name == fk_field_name
and not request.user.is_superuser
):
kwargs["queryset"] = queryset.filter(
**{lookup_path: request.user}
)

return super().formfield_for_foreignkey(
db_field, request, **kwargs
)
1 change: 1 addition & 0 deletions courses/models/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@


class Course(models.Model):
instructor = models.ForeignKey(User, on_delete=models.CASCADE, related_name="courses_teaching")
slug = models.SlugField(unique=True, blank=False)
title = models.CharField(max_length=200)

Expand Down
Loading