diff --git a/accounts/admin.py b/accounts/admin.py index 79b8cc4..51ecfbc 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -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) diff --git a/courses/admin.py b/courses/admin.py index df64c93..fab6c22 100644 --- a/courses/admin.py +++ b/courses/admin.py @@ -1 +1,217 @@ -from .admin import * \ No newline at end of file +from django import forms +from django.contrib import admin +from unfold.admin import ModelAdmin, TabularInline +from unfold.widgets import UnfoldAdminTextInputWidget, UnfoldAdminTextareaWidget +from django.contrib import messages + +from .models import ( + Course, + Homework, + Question, + Project, + ReviewCriteria, +) + +from .scoring import ( + score_homework_submissions, + update_leaderboard, + fill_correct_answers, +) + +from .projects import ( + assign_peer_reviews_for_project, + score_project, + ProjectActionStatus, +) + + +class QuestionForm(forms.ModelForm): + class Meta: + model = Question + fields = "__all__" + widgets = { + "text": UnfoldAdminTextInputWidget(attrs={"size": "60"}), + "possible_answers": UnfoldAdminTextareaWidget(attrs={"cols": 60, "rows": 4}), + "correct_answer": UnfoldAdminTextInputWidget(attrs={"size": "20"}), + } + + +class QuestionInline(TabularInline): + model = Question + form = QuestionForm + extra = 0 + + +def score_selected_homeworks(modeladmin, request, queryset): + for homework in queryset: + status, message = score_homework_submissions(homework.id) + if status: + modeladmin.message_user(request, message, level=messages.SUCCESS) + else: + modeladmin.message_user(request, message, level=messages.WARNING) + + +score_selected_homeworks.short_description = "Score selected homeworks" + + +def set_most_popular_as_correct(modeladmin, request, queryset): + for homework in queryset: + fill_correct_answers(homework) + modeladmin.message_user( + request, + f"Correct answer for {homework} set to most popular", + level=messages.SUCCESS, + ) + + +set_most_popular_as_correct.short_description = ( + "Set correct answers to most popular" +) + + +@admin.register(Homework) +class HomeworkAdmin(ModelAdmin): + inlines = [QuestionInline] + actions = [score_selected_homeworks, set_most_popular_as_correct] + list_display = ["title", "course", "due_date", "state"] + list_filter = ["course__slug"] + + def formfield_for_foreignkey(self, db_field, request, obj=None, **kwargs): + kwargs = handled_course_choices(db_field, kwargs, request) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + def get_queryset(self, request): + qs = super().get_queryset(request) + return get_queryset(request, qs, filter_field='course__instructor') + + +class CriteriaForm(forms.ModelForm): + class Meta: + model = ReviewCriteria + fields = "__all__" + widgets = { + "description": UnfoldAdminTextInputWidget(attrs={"size": "60"}), + "options": UnfoldAdminTextareaWidget( + attrs={"cols": 60, "rows": 4} + ) + } + +class CriteriaInline(TabularInline): + model = ReviewCriteria + form = CriteriaForm + extra = 0 + + +def update_leaderboard_admin(modeladmin, request, queryset): + for course in queryset: + update_leaderboard(course) + modeladmin.message_user( + request, + f"Leaderboard updated for course {course}", + level=messages.SUCCESS, + ) + + +update_leaderboard_admin.short_description = "Update leaderboard" + +@admin.register(Course) +class CourseAdmin(ModelAdmin): + actions = [update_leaderboard_admin] + inlines = [CriteriaInline] + list_display = ["title"] + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + return get_form(request, form) + + def get_queryset(self, request): + qs = super().get_queryset(request) + return get_queryset(request, qs) + +def assign_peer_reviews_for_project_admin( + modeladmin, request, queryset +): + for project in queryset: + status, message = assign_peer_reviews_for_project(project) + if status == ProjectActionStatus.OK: + modeladmin.message_user( + request, message, level=messages.SUCCESS + ) + else: + modeladmin.message_user( + request, message, level=messages.WARNING + ) + + +assign_peer_reviews_for_project_admin.short_description = ( + "Assign peer reviews" +) + + + +def score_projects_admin(modeladmin, request, queryset): + for project in queryset: + status, message = score_project(project) + if status == ProjectActionStatus.OK: + modeladmin.message_user( + request, message, level=messages.SUCCESS + ) + else: + modeladmin.message_user( + request, message, level=messages.WARNING + ) + + +score_projects_admin.short_description = "Score projects" + +@admin.register(Project) +class ProjectAdmin(ModelAdmin): + actions = [ + assign_peer_reviews_for_project_admin, + score_projects_admin, + ] + + list_display = ["title", "course", "state"] + list_filter = ["course__slug"] + + def get_queryset(self, request): + qs = super().get_queryset(request) + return get_queryset(request, qs, filter_field='course__instructor') + + def formfield_for_foreignkey(self, db_field, request, obj=None, **kwargs): + kwargs = handled_course_choices(db_field, kwargs, request) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + +@admin.register(ReviewCriteria) +class ReviewCriteriaAdmin(ModelAdmin): + + def get_queryset(self, request): + qs = super().get_queryset(request) + return get_queryset(request, qs, filter_field='course__instructor') + + def formfield_for_foreignkey(self, db_field, request, obj=None, **kwargs): + kwargs = handled_course_choices(db_field, kwargs, request) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + +def get_queryset(request, qs, filter_field="instructor"): + if request.user.is_superuser: + return qs + else: + return qs.filter(**{filter_field: request.user}) + + +def get_form(request, form): + if not request.user.is_superuser: + form.base_fields["instructor"].initial = request.user + form.base_fields["instructor"].widget = forms.HiddenInput() + return form + +def handled_course_choices(db_field, kwargs, request): + if db_field.name == "course": + kwargs["queryset"] = ( + Course.objects.all() + if request.user.is_superuser + else Course.objects.filter(instructor=request.user) + ) + return kwargs diff --git a/courses/migrations/0019_course_instructor.py b/courses/migrations/0019_course_instructor.py new file mode 100644 index 0000000..265cf1a --- /dev/null +++ b/courses/migrations/0019_course_instructor.py @@ -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, + ), + ] diff --git a/courses/models/course.py b/courses/models/course.py index ad8c55b..ea60aa6 100644 --- a/courses/models/course.py +++ b/courses/models/course.py @@ -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)