From 1cb926153d0ea0648efbe816935305c62316d225 Mon Sep 17 00:00:00 2001 From: Sofya Anokhovskaya Date: Mon, 26 May 2025 14:54:57 +0300 Subject: [PATCH 1/6] fix: getting site id from settings not request --- apps/admission/api/views.py | 3 +- apps/admission/views.py | 22 +++++------ apps/core/context_processors.py | 8 ++++ apps/core/jinja2/globals.py | 7 ++++ apps/core/models.py | 11 +++--- apps/courses/views/meta_course.py | 4 +- apps/courses/views/misc.py | 7 ++-- apps/courses/views/mixins.py | 7 ++-- apps/learning/api/views.py | 4 +- apps/learning/gradebook/views.py | 6 ++- apps/learning/invitation/views.py | 9 +++-- apps/learning/study/views.py | 36 ++++++++++++++---- apps/learning/teaching/views/__init__.py | 4 +- apps/learning/teaching/views/assignments.py | 3 +- apps/learning/views/enrollment.py | 12 +++--- apps/learning/views/icalendar.py | 3 +- apps/library/views.py | 10 +++-- apps/staff/api/views.py | 18 +++------ apps/staff/forms.py | 41 +++++++++++---------- apps/staff/views/enrolees_selection.py | 20 +++++----- apps/staff/views/views.py | 25 ++++++------- apps/stats/views.py | 4 +- apps/users/filters.py | 16 ++------ apps/users/views.py | 15 ++++---- lms/filters.py | 9 +++-- lms/settings/base.py | 36 +++++++++++++++--- lms/views.py | 4 +- 27 files changed, 207 insertions(+), 137 deletions(-) diff --git a/apps/admission/api/views.py b/apps/admission/api/views.py index ef718ab129..f58b6f5d49 100644 --- a/apps/admission/api/views.py +++ b/apps/admission/api/views.py @@ -8,6 +8,7 @@ from rest_framework.views import APIView from django.conf import settings +from django.contrib.sites.models import Site from django.db.models import Q from django.http import Http404 @@ -133,7 +134,7 @@ def post(self, request: HttpRequest, *args, **kwargs): send_email_verification_code( email_to=serializer.validated_data["email"], - site=request.site, + site=Site.objects.get(pk=settings.SITE_ID), applicant=acceptance.applicant, ) diff --git a/apps/admission/views.py b/apps/admission/views.py index 776bb8f5a1..697a41f1f2 100644 --- a/apps/admission/views.py +++ b/apps/admission/views.py @@ -620,18 +620,18 @@ class RegisterApplicantsForOlympiadView(CuratorOnlyMixin, generic.View): """ Register applicants with status PERMIT_TO_OLYMPIAD in the olympiad contest. """ - + def get_campaign(self, campaign_id): """Get campaign by ID.""" return get_object_or_404(Campaign, pk=campaign_id) - + def get_applicants(self, campaign): """Get applicants with status PERMIT_TO_OLYMPIAD.""" return Applicant.objects.filter( campaign=campaign, status=ApplicantStatuses.PERMIT_TO_OLYMPIAD ) - + def create_olympiad_records(self, applicants): """Create Olympiad records for applicants who don't have one.""" created_count = 0 @@ -643,7 +643,7 @@ def create_olympiad_records(self, applicants): Olympiad.objects.create(applicant=applicant) created_count += 1 return created_count - + def register_in_contest(self, api, applicants, request): """Register applicants in the contest.""" registered_count = 0 @@ -661,24 +661,24 @@ def register_in_contest(self, api, applicants, request): f"Error registering applicant {olympiad.applicant_id} in olympiad contest: {e}" ) return registered_count - + def get(self, request, campaign_id): campaign = self.get_campaign(campaign_id) - + # Get applicants and create Olympiad records applicants = self.get_applicants(campaign) created_count = self.create_olympiad_records(applicants) - + # Register applicants in the contest api = YandexContestAPI(access_token=campaign.access_token, refresh_token=campaign.refresh_token) registered_count = self.register_in_contest(api, applicants, request) - + # Add success message messages.success( request, _("Created {} olympiad records and registered {} applicants in the contest.").format(created_count, registered_count) ) - + return redirect("admission:applicants:list") @@ -1124,7 +1124,7 @@ def get_redirect_url(self, *args, **kwargs): class BranchFromURLViewMixin: """ This view mixin sets `branch` attribute to the request object based on - `request.site` and non-empty `branch_code` url named argument + `settings.SITE_ID` and non-empty `branch_code` url named argument """ def setup(self, request, *args, **kwargs): @@ -1135,7 +1135,7 @@ def setup(self, request, *args, **kwargs): f"{self.__class__} is subclass of {self.__class__.__name__} but " f"`branch_code` view keyword argument is not specified or empty" ) - request.branch = Branch.objects.get_by_natural_key(branch_code, request.site.id) + request.branch = Branch.objects.get_by_natural_key(branch_code, settings.SITE_ID) class InterviewResultsView( diff --git a/apps/core/context_processors.py b/apps/core/context_processors.py index 245d53d1f6..d2eeeeaedf 100644 --- a/apps/core/context_processors.py +++ b/apps/core/context_processors.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.sites.models import Site def common_context(request): @@ -19,3 +20,10 @@ def js_config(request): "CSRF_COOKIE_NAME": settings.CSRF_COOKIE_NAME, "SENTRY_DSN": settings.SENTRY_DSN, } + + +def site_context(request): + """Add site object to the context for all views.""" + return { + "site": Site.objects.get(pk=settings.SITE_ID) + } diff --git a/apps/core/jinja2/globals.py b/apps/core/jinja2/globals.py index f07abfd979..3b32086c52 100644 --- a/apps/core/jinja2/globals.py +++ b/apps/core/jinja2/globals.py @@ -1,6 +1,8 @@ from crispy_forms.utils import render_crispy_form +from django.conf import settings from django.contrib.messages import DEFAULT_LEVELS, get_messages +from django.contrib.sites.models import Site from core.menu import Menu from jinja2 import pass_context @@ -40,3 +42,8 @@ def generate_menu(menu_name, request, root_id=None): @pass_context def crispy(context, form): return render_crispy_form(form, context=context) + + +def site_context(): + """Returns the site object based on settings.SITE_ID.""" + return Site.objects.get(pk=settings.SITE_ID) diff --git a/apps/core/models.py b/apps/core/models.py index 60b6dfd09e..eb691762a0 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -12,6 +12,7 @@ from django.utils.encoding import force_bytes, force_str, smart_str from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ +from django.contrib.sites.models import Site from core.db.fields import TimeZoneField from core.db.models import ConfigurationModel @@ -63,7 +64,7 @@ def get_current(self, request=None) -> "SiteConfiguration": """ Return the current site configuration based on the SITE_ID in the project's settings. If SITE_ID isn't defined, return the site - configuration matching ``request.site``. + configuration matching ``settings.SITE_ID``. The ``SiteConfiguration`` object is cached the first time it's retrieved from the database. @@ -71,7 +72,7 @@ def get_current(self, request=None) -> "SiteConfiguration": if getattr(settings, 'SITE_ID', None): site_id = settings.SITE_ID elif request: - site_id = request.site.pk + site_id = settings.SITE_ID else: raise ImproperlyConfigured( "Set the SITE_ID setting or pass a request to " @@ -231,16 +232,16 @@ def for_site(self, site_id: int, all=False) -> List["Branch"]: def get_current(self, request, site_id: int = settings.SITE_ID): """ - Returns the Branch based on the subdomain of the `request.site`, where + Returns the Branch based on the subdomain of the `settings.SITE_ID`, where subdomain is a branch code (e.g. nsk.example.com) If request is not provided, returns the Branch based on the DEFAULT_BRANCH_CODE value in the project's settings. """ - sub_domain = request.get_host().lower().rsplit(request.site.domain, 1)[0][:-1] + sub_domain = request.get_host().lower().rsplit(Site.objects.get(id=settings.SITE_ID).domain, 1)[0][:-1] branch_code = sub_domain or settings.DEFAULT_BRANCH_CODE if branch_code == "www": branch_code = settings.DEFAULT_BRANCH_CODE - key = BranchNaturalKey(code=branch_code, site_id=request.site.id) + key = BranchNaturalKey(code=branch_code, site_id=settings.SITE_ID) if key not in BRANCH_CACHE: BRANCH_CACHE[key] = self.get(code=key.code, site_id=key.site_id) return BRANCH_CACHE[key] diff --git a/apps/courses/views/meta_course.py b/apps/courses/views/meta_course.py index 1f0c88c29c..654960bfba 100644 --- a/apps/courses/views/meta_course.py +++ b/apps/courses/views/meta_course.py @@ -1,5 +1,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.views import generic +from django.conf import settings +from django.contrib.sites.models import Site from auth.mixins import PermissionRequiredMixin from courses.forms import MetaCourseForm @@ -17,7 +19,7 @@ class MetaCourseDetailView(LoginRequiredMixin, generic.DetailView): def get_context_data(self, **kwargs): courses = (Course.objects .filter(meta_course=self.object) - .available_on_site(self.request.site) + .available_on_site(Site.objects.get(pk=settings.SITE_ID)) .select_related("meta_course", "semester", "main_branch") .order_by('-semester__index')) context = { diff --git a/apps/courses/views/misc.py b/apps/courses/views/misc.py index b1fb7e17fd..9e30208a60 100644 --- a/apps/courses/views/misc.py +++ b/apps/courses/views/misc.py @@ -2,6 +2,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.views import generic +from django.conf import settings from core.models import Branch, Location from courses.models import Course, CourseTeacher @@ -19,7 +20,7 @@ def get_queryset(self, *args, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - branches = Branch.objects.for_site(site_id=self.request.site.pk) + branches = Branch.objects.for_site(site_id=settings.SITE_ID) min_established = min(b.established for b in branches) # FIXME: move to service method and test courses = (Course.objects @@ -32,10 +33,10 @@ def get_context_data(self, **kwargs): teachers=self.object.pk) .select_related('semester', 'meta_course', 'main_branch') .order_by('-semester__index')) - + if not self.request.user.has_permission_to_drafts: courses = courses.is_published() - + context['courses'] = courses return context diff --git a/apps/courses/views/mixins.py b/apps/courses/views/mixins.py index 84abe16a9e..71492005d0 100644 --- a/apps/courses/views/mixins.py +++ b/apps/courses/views/mixins.py @@ -4,6 +4,7 @@ from django.conf import settings from django.http import Http404 from django.shortcuts import get_object_or_404 +from django.contrib.sites.models import Site from core.exceptions import Redirect from core.urls import reverse @@ -39,7 +40,7 @@ def setup(self, request, *args, **kwargs): meta_course__slug=kwargs['course_slug'], semester__type=kwargs['semester_type'], semester__year=kwargs['semester_year']) - .available_on_site(request.site) + .available_on_site(Site.objects.get(pk=settings.SITE_ID)) .order_by('pk') ) @@ -52,7 +53,7 @@ def get_course_queryset(self): class CoursePublicURLParamsMixin(CourseURLParamsMixinBase): """ This mixin helps to retrieve course made by the current site (where - main branch is related to the `request.site`), `RE_COURSE_PUBLIC_URI` + main branch is related to the `settings.SITE_ID`), `RE_COURSE_PUBLIC_URI` friendly URL prefix contains all the required parameters for this. Returns 404 in case course is not found, otherwise sets `course` attribute to the view instance. @@ -103,7 +104,7 @@ def setup(self, request, *args, **kwargs): courses = list(self.get_course_queryset() .filter(main_branch__code=main_branch_code, main_branch__active=True, - main_branch__site=request.site) + main_branch__site=Site.objects.get(pk=settings.SITE_ID)) .order_by('pk')) if not courses: raise Http404 diff --git a/apps/learning/api/views.py b/apps/learning/api/views.py index 2b4ebdd3b7..7ee2b5ea8c 100644 --- a/apps/learning/api/views.py +++ b/apps/learning/api/views.py @@ -10,6 +10,8 @@ from rest_framework.response import Response from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from django.contrib.sites.models import Site from api.authentication import TokenAuthentication from api.mixins import ApiErrorsMixin @@ -109,7 +111,7 @@ def get(self, request: AuthenticatedAPIRequest, **kwargs: Any): queryset = (Enrollment.active .select_related('student') .filter(course=self.course, - course__main_branch__site=request.site)) + course__main_branch__site=Site.objects.get(pk=settings.SITE_ID))) data = self.OutputSerializer(queryset, many=True).data return Response(data) diff --git a/apps/learning/gradebook/views.py b/apps/learning/gradebook/views.py index c795965314..c377d2d3de 100644 --- a/apps/learning/gradebook/views.py +++ b/apps/learning/gradebook/views.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from django.contrib import messages +from django.conf import settings from django.core.exceptions import PermissionDenied, ValidationError from django.db.models import Prefetch, Q, Count from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect @@ -14,6 +15,7 @@ from django.utils.translation import gettext_lazy as _ from django.views import View, generic from django.views.generic.base import TemplateResponseMixin +from django.contrib.sites.models import Site from api.views import APIBaseView from auth.mixins import PermissionRequiredMixin, RolePermissionRequiredMixin @@ -58,7 +60,7 @@ class GradeBookListBaseView(generic.ListView): def get_course_queryset(self): return (Course.objects - .available_on_site(self.request.site) + .available_on_site(Site.objects.get(pk=settings.SITE_ID)) .select_related("meta_course", "main_branch") .order_by("meta_course__name")) @@ -202,7 +204,7 @@ def get_context_data(self, form: BaseGradebookForm, .select_related('semester', 'meta_course', 'main_branch')) context['course_offering_list'] = courses context['user_type'] = self.user_type - + is_recredited_filter = Q(grade=GradeTypes.RE_CREDIT) | Q(is_grade_recredited=True) context.update( Enrollment.active.filter(course=self.course).aggregate( diff --git a/apps/learning/invitation/views.py b/apps/learning/invitation/views.py index c28156606d..a924bb4768 100644 --- a/apps/learning/invitation/views.py +++ b/apps/learning/invitation/views.py @@ -12,6 +12,7 @@ from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ +from django.contrib.sites.models import Site from courses.models import Semester @@ -95,7 +96,7 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) elif self.invitation.semester != Semester.get_current(): return HttpResponseForbidden(_("Invitation is outdated")) - elif has_other_active_invited_profile(request.user, request.site, self.invitation): + elif has_other_active_invited_profile(request.user, Site.objects.get(pk=settings.SITE_ID), self.invitation): return HttpResponseForbidden(_("You already have other active invitation in this semester")) return super().dispatch(request, *args, **kwargs) @@ -106,7 +107,7 @@ def get(self, request, *args, **kwargs): kwargs={"token": self.invitation.token}, subdomain=settings.LMS_SUBDOMAIN) return HttpResponseRedirect(redirect_to=login_url) - if not is_student_profile_valid(request.user, request.site): + if not is_student_profile_valid(request.user, Site.objects.get(pk=settings.SITE_ID)): redirect_to = reverse("invitation:complete_profile", kwargs={"token": self.invitation.token}, subdomain=settings.LMS_SUBDOMAIN) @@ -237,7 +238,7 @@ class InvitationCompleteProfileView(InvitationURLParamsMixin, def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() - if is_student_profile_valid(request.user, request.site): + if is_student_profile_valid(request.user, Site.objects.get(pk=settings.SITE_ID)): return HttpResponseRedirect(self.invitation.get_absolute_url()) return super().dispatch(request, *args, **kwargs) @@ -254,7 +255,7 @@ def get_object(self): def form_valid(self, form): self.object = form.save(commit=False) - complete_student_profile(self.object, self.request.site, self.invitation) + complete_student_profile(self.object, Site.objects.get(pk=settings.SITE_ID), self.invitation) return HttpResponseRedirect(self.invitation.get_absolute_url()) def get_context_data(self, **kwargs): diff --git a/apps/learning/study/views.py b/apps/learning/study/views.py index 43c52b15ef..cac6df76ee 100644 --- a/apps/learning/study/views.py +++ b/apps/learning/study/views.py @@ -7,11 +7,13 @@ from django.apps import apps from django.contrib import messages +from django.conf import settings from django.db import transaction from django.db.models import Prefetch, Q from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.utils.translation import gettext_lazy as _ from django.views import generic +from django.contrib.sites.models import Site from auth.mixins import PermissionRequiredMixin from core import comment_persistence @@ -62,7 +64,7 @@ class CalendarFullView(PermissionRequiredMixin, MonthEventsCalendarView): def get_events(self, month_period: MonthPeriod, **kwargs) -> Iterable: start_date, end_date = extended_month_date_range(month_period, expand=1) user = self.request.user - student_profile = get_student_profile(user, self.request.site) + student_profile = get_student_profile(user, Site.objects.get(pk=settings.SITE_ID)) branches = [student_profile.branch_id] return get_all_calendar_events(branch_list=branches, start_date=start_date, end_date=end_date, time_zone=user.time_zone) @@ -79,7 +81,7 @@ class CalendarPersonalView(CalendarFullView): def get_events(self, month_period: MonthPeriod, **kwargs) -> Iterable: start_date, end_date = extended_month_date_range(month_period, expand=1) student_profile = get_student_profile(self.request.user, - self.request.site) + Site.objects.get(pk=settings.SITE_ID)) if not student_profile: return [] return get_student_calendar_events(student_profile=student_profile, @@ -301,7 +303,7 @@ def get_context_data(self, **kwargs): # Get current term course offerings available in student branch, # courses in this term available via invitation # and all courses that student enrolled in - student_profile = get_student_profile(auth_user, self.request.site) + student_profile = get_student_profile(auth_user, Site.objects.get(pk=settings.SITE_ID)) current_term = get_current_term_pair(auth_user.time_zone) current_term_index = current_term.index enrolled_in = Q(id__in=list(student_enrollments)) @@ -358,10 +360,15 @@ class UsefulListView(PermissionRequiredMixin, generic.ListView): def get_queryset(self): return (InfoBlock.objects - .for_site(self.request.site) + .for_site(Site.objects.get(pk=settings.SITE_ID)) .with_tag(CurrentInfoBlockTags.USEFUL) .order_by("sort")) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['site'] = Site.objects.get(pk=settings.SITE_ID) + return context + class InternshipListView(PermissionRequiredMixin, generic.ListView): context_object_name = "faq" @@ -371,10 +378,15 @@ class InternshipListView(PermissionRequiredMixin, generic.ListView): def get_queryset(self): return (InfoBlock.objects - .for_site(self.request.site) + .for_site(Site.objects.get(pk=settings.SITE_ID)) .with_tag(CurrentInfoBlockTags.INTERNSHIP) .order_by("sort")) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['site'] = Site.objects.get(pk=settings.SITE_ID) + return context + class HonorCodeView(generic.ListView): context_object_name = "faq" @@ -382,10 +394,15 @@ class HonorCodeView(generic.ListView): def get_queryset(self): return (InfoBlock.objects - .for_site(self.request.site) + .for_site(Site.objects.get(pk=settings.SITE_ID)) .with_tag(CurrentInfoBlockTags.HONOR_CODE) .order_by("sort")) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['site'] = Site.objects.get(pk=settings.SITE_ID) + return context + class ProgramsView(generic.ListView): context_object_name = "faq" @@ -393,6 +410,11 @@ class ProgramsView(generic.ListView): def get_queryset(self): return (InfoBlock.objects - .for_site(self.request.site) + .for_site(Site.objects.get(pk=settings.SITE_ID)) .with_tag(CurrentInfoBlockTags.PROGRAMS) .order_by("sort")) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['site'] = Site.objects.get(pk=settings.SITE_ID) + return context diff --git a/apps/learning/teaching/views/__init__.py b/apps/learning/teaching/views/__init__.py index f8b53733ae..8877aa1bb5 100644 --- a/apps/learning/teaching/views/__init__.py +++ b/apps/learning/teaching/views/__init__.py @@ -6,6 +6,8 @@ from django.db.models import Prefetch, Q from django.shortcuts import get_object_or_404 from django.views import generic +from django.conf import settings +from django.contrib.sites.models import Site from auth.mixins import PermissionRequiredMixin from core.db.utils import normalize_score @@ -173,6 +175,6 @@ class TeachingUsefulListView(PermissionRequiredMixin, generic.ListView): def get_queryset(self): return (InfoBlock.objects - .for_site(self.request.site) + .for_site(Site.objects.get(pk=settings.SITE_ID)) .with_tag(CurrentInfoBlockTags.TEACHERS_USEFUL) .order_by("sort")) diff --git a/apps/learning/teaching/views/assignments.py b/apps/learning/teaching/views/assignments.py index 92fe77db8e..d725cfede9 100644 --- a/apps/learning/teaching/views/assignments.py +++ b/apps/learning/teaching/views/assignments.py @@ -20,6 +20,7 @@ from django.views import generic from django.views.generic.edit import BaseUpdateView from django.utils.translation import gettext_lazy as _ +from django.contrib.sites.models import Site from auth.mixins import PermissionRequiredMixin from core import comment_persistence @@ -137,7 +138,7 @@ def __init__(self, courses: List[Course], **kwargs): def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: teacher = self.request.user courses = list(get_teacher_not_spectator_courses(teacher) - .filter(main_branch__site=self.request.site) + .filter(main_branch__site=Site.objects.get(pk=settings.SITE_ID)) .order_by("-semester__index", "meta_course__name")) if not courses: return {} diff --git a/apps/learning/views/enrollment.py b/apps/learning/views/enrollment.py index 410415079c..c98afa4b89 100644 --- a/apps/learning/views/enrollment.py +++ b/apps/learning/views/enrollment.py @@ -6,6 +6,8 @@ from django.utils.translation import gettext_lazy as _ from django.views import generic from django.views.generic import FormView +from django.conf import settings +from django.contrib.sites.models import Site from auth.mixins import PermissionRequiredMixin from core.exceptions import Redirect @@ -32,7 +34,7 @@ class CourseEnrollView(CourseURLParamsMixin, PermissionRequiredMixin, FormView): permission_required = EnrollInCourse.name def get_permission_object(self): - site = self.request.site + site = Site.objects.get(pk=settings.SITE_ID) student_profile = self.request.user.get_student_profile(site) return EnrollPermissionObject(self.course, student_profile) @@ -63,7 +65,7 @@ def form_valid(self, form): reason_entry = form.cleaned_data.get("reason", "").strip() type = form.cleaned_data["type"].strip() user = self.request.user - student_profile = user.get_student_profile(self.request.site) + student_profile = user.get_student_profile(Site.objects.get(pk=settings.SITE_ID)) try: student_group = StudentGroupService.resolve(self.course, student_profile=student_profile) @@ -131,7 +133,7 @@ class CourseInvitationEnrollView(PermissionRequiredMixin, permission_required = EnrollInCourseByInvitation.name def get_permission_object(self): - site = self.request.site + site = Site.objects.get(pk=settings.SITE_ID) student_profile = self.request.user.get_student_profile(site) return InvitationEnrollPermissionObject(self.course_invitation, student_profile) @@ -144,7 +146,7 @@ def has_permission(self) -> bool: invitation = self.course_invitation.invitation raise Redirect(to=invitation.get_absolute_url()) return has_perm - + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['enrollment_type'] = self.course_invitation.enrollment_type @@ -166,7 +168,7 @@ def form_valid(self, form): type = form.cleaned_data["type"].strip() invitation = self.course_invitation.invitation user = self.request.user - student_profile = user.get_student_profile(self.request.site) + student_profile = user.get_student_profile(Site.objects.get(pk=settings.SITE_ID)) try: resolved_group = StudentGroupService.resolve(self.course, student_profile=student_profile, diff --git a/apps/learning/views/icalendar.py b/apps/learning/views/icalendar.py index 8944e38ecd..e97c397948 100644 --- a/apps/learning/views/icalendar.py +++ b/apps/learning/views/icalendar.py @@ -8,6 +8,7 @@ from django.shortcuts import get_object_or_404 from django.utils import timezone from django.views import generic +from django.contrib.sites.models import Site from admission.selectors import get_ongoing_interviews from learning.icalendar import ( @@ -31,7 +32,7 @@ class ICalendarMeta(NamedTuple): class UserICalendarView(generic.base.View): def get(self, request, *args, **kwargs): user = self.get_user() - site = self.request.site + site = Site.objects.get(pk=settings.SITE_ID) url_builder = request.build_absolute_uri product_id = f"-//{site.name} Calendar//{site.domain}//" tz = user.time_zone or settings.DEFAULT_TIMEZONE diff --git a/apps/library/views.py b/apps/library/views.py index 868b37bffb..e5b743643c 100644 --- a/apps/library/views.py +++ b/apps/library/views.py @@ -1,9 +1,11 @@ from dal import autocomplete from vanilla import DetailView, ListView +from django.conf import settings from auth.mixins import PermissionRequiredMixin from learning.permissions import ViewLibrary from users.mixins import CuratorOnlyMixin +from django.contrib.sites.models import Site from .models import BookTag, Borrow, Stock @@ -27,13 +29,13 @@ class BookListView(PermissionRequiredMixin, ListView): def get_queryset(self): qs = (Stock.objects .select_related("branch", "book") - .filter(branch__site=self.request.site, + .filter(branch__site=Site.objects.get(pk=settings.SITE_ID), branch__active=True) .prefetch_related("borrows", "borrows__student")) # Students can see books from there branch only user = self.request.user if not user.is_curator: - student_profile = user.get_student_profile(self.request.site) + student_profile = user.get_student_profile(Site.objects.get(pk=settings.SITE_ID)) qs = qs.filter(branch_id=student_profile.branch_id) return qs @@ -53,14 +55,14 @@ class BookDetailView(PermissionRequiredMixin, DetailView): def get_queryset(self): qs = (Stock.objects - .filter(branch__site=self.request.site, + .filter(branch__site=Site.objects.get(pk=settings.SITE_ID), branch__active=True) .select_related("book") .prefetch_related("borrows")) # Students can see books from there branch only user = self.request.user if not user.is_curator: - student_profile = user.get_student_profile(self.request.site) + student_profile = user.get_student_profile(Site.objects.get(pk=settings.SITE_ID)) assert student_profile qs = qs.filter(branch_id=student_profile.branch_id) return qs diff --git a/apps/staff/api/views.py b/apps/staff/api/views.py index 927e215c50..d5402694c5 100644 --- a/apps/staff/api/views.py +++ b/apps/staff/api/views.py @@ -5,12 +5,13 @@ from rest_framework.generics import ListAPIView from rest_framework.pagination import LimitOffsetPagination from rest_framework.response import Response +from django.conf import settings +from django.contrib.sites.models import Site from api.permissions import CuratorAccessPermission from api.views import APIBaseView from core.http import HttpRequest from learning.api.serializers import StudentProfileSerializer -from learning.models import GraduateProfile from users.filters import StudentFilter from users.models import StudentProfile from users.services import create_graduate_profiles @@ -27,23 +28,16 @@ class StudentSearchJSONView(ListAPIView): filterset_class = StudentFilter class OutputSerializer(StudentProfileSerializer): - graduation_year = serializers.SerializerMethodField() - class Meta(StudentProfileSerializer.Meta): - fields = ('pk', 'short_name', 'user_id', 'graduation_year') - - def get_graduation_year(self, obj): - if hasattr(obj, 'graduate_profile'): - return obj.graduate_profile.graduation_year - return None + fields = ('pk', 'short_name', 'user_id') def get_serializer_class(self): return self.OutputSerializer def get_queryset(self): return (StudentProfile.objects - .filter(site=self.request.site) - .select_related('user', 'graduate_profile') + .filter(site=Site.objects.get(id=settings.SITE_ID)) + .select_related('user') .only('user__username', 'user__first_name', 'user__last_name', 'user_id') .order_by('user__last_name', @@ -62,6 +56,6 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any): serializer.is_valid(raise_exception=True) graduated_on = serializer.validated_data['graduated_on'] - create_graduate_profiles(request.site, graduated_on, created_by=request.user) + create_graduate_profiles(Site.objects.get(id=settings.SITE_ID), graduated_on, created_by=request.user) return Response(status=status.HTTP_201_CREATED) diff --git a/apps/staff/forms.py b/apps/staff/forms.py index c878700341..963e64ab34 100644 --- a/apps/staff/forms.py +++ b/apps/staff/forms.py @@ -9,6 +9,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.utils import timezone +from django.contrib.sites.models import Site from core.urls import reverse from core.widgets import DateInputTextWidget @@ -74,7 +75,7 @@ def clean(self): class BadgeNumberFromCSVForm(forms.Form): csv_file = forms.FileField(label=_('CSV file'), required=True) - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper(self) @@ -93,7 +94,7 @@ def clean_csv_file(self): reader = csv.DictReader(decoded_file) except Exception as e: raise ValidationError(_(f"File read error: {str(e)}")) - + headers = reader.fieldnames required_columns = {"Почта", "Номер пропуска"} if not required_columns.issubset(set(headers)): @@ -109,12 +110,12 @@ def __init__(self, *args, **kwargs): # Get all years where there are graduates self.graduated_years = self.get_graduated_years() year_choices = [(year, str(year)) for year in self.graduated_years] - + self.fields['graduated_year'] = forms.ChoiceField( label=_("Year of Graduation"), choices=year_choices ) - + self.helper = FormHelper(self) self.helper.form_action = reverse("staff:export_for_electronic_diplomas") self.helper.layout = Layout( @@ -131,13 +132,13 @@ def get_graduated_years(self): # Get distinct year_of_curriculum values from StudentProfile # Optimize by only selecting the year_of_curriculum field and filtering by site current_year = timezone.now().year - + curriculum_years = StudentProfile.objects.filter( - site=self.request.site, + site=Site.objects.get(pk=settings.SITE_ID), year_of_curriculum__lte=current_year-2, status="" ).distinct('year_of_curriculum').values_list('year_of_curriculum', flat=True) - + return sorted([year + 2 for year in curriculum_years], reverse=True) def clean(self): @@ -147,7 +148,7 @@ def clean(self): raise ValidationError(_("Not supported graduation year")) except (ValueError, TypeError): raise ValidationError(_("Invalid year format")) - + class ConfirmSendLettersForm(forms.Form): """ @@ -156,13 +157,13 @@ class ConfirmSendLettersForm(forms.Form): """ email_template_id = forms.CharField(widget=forms.HiddenInput()) scheduled_time = forms.CharField(widget=forms.HiddenInput(), required=False) - + # These fields are just for display, not for actual form submission base_info_display = forms.CharField( label="", required=False, widget=forms.TextInput(attrs={ - 'readonly': 'readonly', + 'readonly': 'readonly', 'style': 'border: none; background-color: #f8f9fa; font-weight: bold; font-size: 1.2em; padding: 10px; border-radius: 5px;' }) ) @@ -182,15 +183,15 @@ class ConfirmSendLettersForm(forms.Form): required=False, widget=forms.TextInput(attrs={'readonly': 'readonly', 'style': 'border: none; background-color: #f8f9fa;'}) ) - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self.helper = FormHelper(self) self.helper.form_action = reverse("staff:send_letters") self.helper.layout = Layout( - + Fieldset(_('Confirmation'), Row( Div('base_info_display', css_class="col-xs-12"), @@ -223,7 +224,7 @@ def __init__(self, *args, **kwargs): ), ) ) - + def get_emails(self): """ Get all emails from the form. @@ -287,7 +288,7 @@ class SendLettersForm(forms.Form): widget=forms.HiddenInput(), required=False ) - + def clean_scheduled_time(self): scheduled_time = self.cleaned_data.get('scheduled_time') if scheduled_time: @@ -301,23 +302,23 @@ def clean_scheduled_time(self): scheduled_time = self.tz.localize(naive_dt) else: scheduled_time = timezone.make_aware(scheduled_time, self.tz) - + return scheduled_time - + def clean_status(self): """Convert 'studying' back to empty string.""" statuses = self.cleaned_data.get('status', []) return ["" if s == "studying" else s for s in statuses] - + def __init__(self, *args, **kwargs): self.request = kwargs.pop('request', None) super().__init__(*args, **kwargs) - + self.tz = self.request.user.time_zone if self.request.user.time_zone else settings.DEFAULT_TIMEZONE local_time = timezone.localtime(timezone.now(), self.tz) self.fields['scheduled_time'].initial = local_time.strftime('%d.%m.%Y %H:%M') self.fields['scheduled_time'].help_text = f"Временная зона {getattr(self.tz, 'zone', str(self.tz))} {datetime.now(self.tz).strftime('%z')[:3]}" - + self.helper = FormHelper(self) self.helper.form_action = reverse("staff:confirm_send_letters") self.helper.layout = Layout( diff --git a/apps/staff/views/enrolees_selection.py b/apps/staff/views/enrolees_selection.py index 9b43e89dfa..b24e8d270b 100644 --- a/apps/staff/views/enrolees_selection.py +++ b/apps/staff/views/enrolees_selection.py @@ -6,6 +6,8 @@ from django.utils.translation import gettext_lazy as _ from django.views import generic from django.utils import timezone +from django.conf import settings +from django.contrib.sites.models import Site import core.utils from core.utils import bucketize @@ -24,7 +26,7 @@ class EnroleesSelectionListView(CuratorOnlyMixin, generic.ListView): def get_course_queryset(self): return (Course.objects - .available_on_site(self.request.site) + .available_on_site(Site.objects.get(pk=settings.SITE_ID)) # TODO add selection_avaliable filter .select_related("meta_course", "main_branch") .order_by("meta_course__name")) @@ -32,7 +34,7 @@ def get_course_queryset(self): def get_term_threshold(self): latest_term = Semester.objects.order_by("-index").first() return latest_term.index - + def get_queryset(self): return (Semester.objects .filter(index__lte=self.get_term_threshold()) @@ -68,7 +70,7 @@ def get_context_data(self, **kwargs): "semester_list": semester_list } return context - + class EnroleesSelectionCSVView(CuratorOnlyMixin, CourseURLParamsMixin, generic.base.View): @@ -77,22 +79,22 @@ class EnroleesSelectionCSVView(CuratorOnlyMixin, CourseURLParamsMixin, GradeTypes.GOOD: 4, GradeTypes.EXCELLENT: 5 } - + def calculate_average_grades(self, students): for student in students: enrollments = [enrollment for profile in student.official_profiles for enrollment in profile.enrollment_set.all()] - numeric_satisfactory_grades = [self.grade_to_numeric[enrollment.grade] for enrollment in enrollments + numeric_satisfactory_grades = [self.grade_to_numeric[enrollment.grade] for enrollment in enrollments if enrollment.grade in self.grade_to_numeric and enrollment.course.is_visible_in_certificates] if not numeric_satisfactory_grades: yield student.id, "" else: yield student.id, round(statistics.fmean(numeric_satisfactory_grades), 3) - + def get(self, request, *args, **kwargs): enrollments = Enrollment.active.filter(course=self.course).select_related("student", "student_profile__branch", "student_profile__partner").order_by("student") student_profile_queryset = (StudentProfile.objects - .filter(site=request.site, + .filter(site=Site.objects.get(pk=settings.SITE_ID), type__in=[StudentTypes.REGULAR, StudentTypes.PARTNER], status__ne=StudentStatuses.EXPELLED) .order_by('year_of_admission', '-pk') @@ -101,7 +103,7 @@ def get(self, request, *args, **kwargs): users = (User.objects .filter(pk__in=enrollments.values_list("student__id", flat=True)) .prefetch_related(Prefetch("student_profiles", queryset=student_profile_queryset, to_attr="official_profiles"))) - + average_grades_map = dict(self.calculate_average_grades(users)) response = HttpResponse(content_type='text/csv; charset=utf-8') filename = "{}-{}-{}-enrolees-selection.csv".format(kwargs['course_slug'], @@ -131,4 +133,4 @@ def get(self, request, *args, **kwargs): student_profile.branch.name, student_profile.get_type_display(), student_profile.partner, student_profile.year_of_curriculum, enrollment.get_type_display(), enrollment.reason_entry, average_grades_map[student.id] ]) - return response \ No newline at end of file + return response diff --git a/apps/staff/views/views.py b/apps/staff/views/views.py index 15f47b45ed..29eed4a762 100644 --- a/apps/staff/views/views.py +++ b/apps/staff/views/views.py @@ -18,6 +18,7 @@ from django.shortcuts import get_list_or_404, get_object_or_404 from django.utils.translation import gettext_lazy as _ from django.views import View, generic +from django.contrib.sites.models import Site import core.utils from admission.models import Campaign, Interview @@ -108,7 +109,7 @@ def get_context_data(self, **kwargs): "branches": {b.pk: b.name for b in branches}, "curriculum_years": ( StudentProfile.objects.filter( - site=self.request.site, year_of_curriculum__isnull=False + site=Site.objects.get(pk=settings.SITE_ID), year_of_curriculum__isnull=False ) .values_list("year_of_curriculum", flat=True) .order_by("year_of_curriculum") @@ -116,7 +117,7 @@ def get_context_data(self, **kwargs): ), "admission_years": ( StudentProfile.objects.filter( - site=self.request.site, year_of_admission__isnull=False + site=Site.objects.get(pk=settings.SITE_ID), year_of_admission__isnull=False ) .values_list("year_of_admission", flat=True) .order_by("year_of_admission") @@ -130,17 +131,13 @@ def get_context_data(self, **kwargs): "is_paid_basis": [("1", "Да"), ("0", "Нет")], "uni_graduation_year": ( StudentProfile.objects.filter( - site=self.request.site, + site=Site.objects.get(pk=settings.SITE_ID), graduation_year__isnull=False, graduate_without_diploma=True ) .values_list("graduation_year", flat=True) .order_by("graduation_year") .distinct() - ), - "graduation_years": ( - GraduateProfile.objects.filter( - student_profile__site=settings.SITE_ID).values_list("graduation_year", flat=True).order_by("graduation_year").distinct() ) } return context @@ -158,7 +155,7 @@ def get_context_data(self, **kwargs): badge_number_from_csv_form = BadgeNumberFromCSVForm() send_letters_form = SendLettersForm(request=self.request) official_diplomas_dates = ( - GraduateProfile.objects.for_site(self.request.site) + GraduateProfile.objects.for_site(Site.objects.get(pk=settings.SITE_ID)) .with_official_diploma() .distinct("diploma_issued_on") .order_by("-diploma_issued_on") @@ -679,7 +676,7 @@ def autofail_ungraded(request): if not request.user.is_curator: return HttpResponseForbidden() try: - graded = call_command("autofail_ungraded", request.site) + graded = call_command("autofail_ungraded", Site.objects.get(pk=settings.SITE_ID)) messages.success( request, f"Операция выполнена успешно.
" f"Выставлено незачетов: {graded}" ) @@ -696,7 +693,7 @@ def create_alumni_profiles(request: HttpRequest): form = GraduationForm(data=request.POST) if form.is_valid(): graduated_on = form.cleaned_data["graduated_on"] - create_graduate_profiles(request.site, graduated_on, created_by=request.user) + create_graduate_profiles(Site.objects.get(pk=settings.SITE_ID), graduated_on, created_by=request.user) messages.success(request, "Операция выполнена успешно") else: messages.error(request, "Неверный формат даты выпуска") @@ -768,7 +765,7 @@ def export_for_electronic_diplomas_view(request: HttpRequest): if form.is_valid(): graduated_year = int(form.cleaned_data["graduated_year"]) - return ElectronicDiplomaExportService.generate_export(request.site, graduated_year) + return ElectronicDiplomaExportService.generate_export(Site.objects.get(pk=settings.SITE_ID), graduated_year) else: for field, error_as_list in form.errors.items(): label = form.fields[field].label if field in form.fields else field @@ -853,7 +850,7 @@ def get_context_data(self, **kwargs): day = int(self.kwargs["day"]) date = datetime.date(year, month, day) graduate_profiles = get_list_or_404( - GraduateProfile.objects.for_site(self.request.site) + GraduateProfile.objects.for_site(Site.objects.get(pk=settings.SITE_ID)) .with_official_diploma() .filter(diploma_issued_on=date) .select_related("student_profile__user") @@ -878,7 +875,7 @@ def get(self, request, year, month, day, *args, **kwargs): diploma_issued_on = datetime.date(int(year), int(month), int(day)) report = OfficialDiplomasReport(diploma_issued_on) site_aware_queryset = report.get_queryset().filter( - branch__site=self.request.site + branch__site=Site.objects.get(pk=settings.SITE_ID) ) if not site_aware_queryset.count(): raise Http404 @@ -894,7 +891,7 @@ class OfficialDiplomasTeXView(CuratorOnlyMixin, generic.TemplateView): def get_context_data(self, year, month, day, **kwargs): diploma_issued_on = datetime.date(int(year), int(month), int(day)) report = OfficialDiplomasReport(diploma_issued_on) - student_profiles = report.get_queryset().filter(branch__site=self.request.site) + student_profiles = report.get_queryset().filter(branch__site=Site.objects.get(pk=settings.SITE_ID)) students = (sp.user for sp in student_profiles) courses_qs = report.get_courses_queryset(students).annotate( classes_total=Count("courseclass") diff --git a/apps/stats/views.py b/apps/stats/views.py index 3433619f95..9d0997668f 100644 --- a/apps/stats/views.py +++ b/apps/stats/views.py @@ -31,7 +31,7 @@ class StatsLearningView(CuratorOnlyMixin, generic.TemplateView): def get_context_data(self, **kwargs): context = super(StatsLearningView, self).get_context_data(**kwargs) # Terms grouped by year - branches = Branch.objects.for_site(site_id=self.request.site.pk) + branches = Branch.objects.for_site(site_id=settings.SITE_ID) min_established = min(b.established for b in branches) term_start = get_term_index(min_established, SemesterTypes.AUTUMN) terms = (Semester.objects.only("pk", "type", "year") @@ -154,7 +154,7 @@ def get_interviewers(self): # TODO: move to learning or users app -# TODO SEEMS LIKE DEPRICATED +# TODO SEEMS LIKE DEPRICATED class AlumniStats(APIView): def get(self, request, graduation_year, format=None): filters = (Q(status=StudentStatuses.GRADUATE) & diff --git a/apps/users/filters.py b/apps/users/filters.py index 0ef680ba80..d37e9741a9 100644 --- a/apps/users/filters.py +++ b/apps/users/filters.py @@ -3,6 +3,8 @@ from django.db.models import Case, Count, F, Q, Value, When from django.forms import SelectMultiple +from django.conf import settings +from django.contrib.sites.models import Site from core.filters import CharInFilter, NumberInFilter from learning.settings import GradeTypes, StudentStatuses @@ -60,22 +62,19 @@ class StudentFilter(FilterSet): is_paid_basis = NumberInFilter(field_name='is_paid_basis') uni_graduation_year = NumberInFilter(label='University graduation year', method='uni_graduation_year_filter') - graduation_years = NumberInFilter(label='Graduation Year', - method='graduation_year_filter') class Meta: model = StudentProfile fields = ("name", "branches", "profile_types", "year_of_curriculum", "year_of_admission", "types", "status", "cnt_enrollments", - "academic_disciplines", "partners", "is_paid_basis", "uni_graduation_year", - "graduation_years") + "academic_disciplines", "partners", "is_paid_basis", "uni_graduation_year") @property def qs(self): if not self.form.changed_data: return self.queryset.none() - return super().qs.filter(site=self.request.site) + return super().qs.filter(site=Site.objects.get(pk=settings.SITE_ID)) def courses_filter(self, queryset, name, value): value_list = value.split(u',') @@ -114,13 +113,6 @@ def uni_graduation_year_filter(self, queryset, name, value): condition |= Q(graduate_without_diploma=True) return queryset.filter(condition) - def graduation_year_filter(self, queryset, name, value): - value_list = [int(v) for v in value if v != 0] - condition = Q(graduate_profile__graduation_year__in=value_list) - if any(year == 0 for year in value): - condition |= Q(graduate_profile__isnull=False) - return queryset.filter(condition) - def status_filter(self, queryset, name, value): value_list = value.split(u',') value_list = [v for v in value_list if v] diff --git a/apps/users/views.py b/apps/users/views.py index 52fb271d0a..0119678148 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -21,6 +21,7 @@ from django.utils.translation import gettext_lazy as _ from django.views import generic from django.views.decorators.csrf import csrf_exempt, csrf_protect +from django.contrib.sites.models import Site from api.views import APIBaseView from apps.courses.utils import date_to_term_pair @@ -70,7 +71,7 @@ def get_queryset(self, *args, **kwargs): # Limit results on compsciclub.ru if hasattr(self.request, "branch"): filters.append(Q(main_branch=self.request.branch)) - site_courses_queryset = get_site_courses(site=self.request.site, filters=filters) + site_courses_queryset = get_site_courses(site=Site.objects.get(pk=settings.SITE_ID), filters=filters) prefetch_list = [ Prefetch('teaching_set', queryset=site_courses_queryset), Prefetch('shadcourserecord_set', queryset=shad_courses_queryset), @@ -121,7 +122,7 @@ def get_context_data(self, **kwargs): "can_view_assignments": can_view_assignments, "can_view_course_icons": can_view_course_icons, "yandex_oauth_url": reverse('auth:users:yandex_begin'), - "is_yds_site": self.request.site.pk == settings.YDS_SITE_ID + "is_yds_site": settings.SITE_ID == settings.YDS_SITE_ID } enrollments = profile_user.enrollment_set.all().select_related("student_profile__invitation") for enrollment in enrollments: @@ -180,7 +181,7 @@ def get_context_data(self, **kwargs): enrollments=queryset) if can_view_student_profiles: student_profiles = get_student_profiles(user=profile_user, - site=self.request.site, + site=Site.objects.get(pk=settings.SITE_ID), fetch_graduate_profile=True, fetch_status_history=True, fetch_invitation=True, @@ -227,7 +228,7 @@ def is_form_allowed(self, user, obj): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) graduate_profile = get_graduate_profile_compat(self.object, - self.request.site) + Site.objects.get(pk=settings.SITE_ID)) if graduate_profile: context["testimonial_form"] = TestimonialForm( instance=graduate_profile) @@ -238,7 +239,7 @@ def form_valid(self, form): with transaction.atomic(): self.object = form.save() graduate_profile = get_graduate_profile_compat(self.object, - self.request.site) + Site.objects.get(pk=settings.SITE_ID)) if graduate_profile: testimonial_form = TestimonialForm(instance=graduate_profile, data=self.request.POST) @@ -295,7 +296,7 @@ class CertificateOfParticipationCreateView(PermissionRequiredMixin, def form_valid(self, form): user = get_object_or_404(User.objects.filter(pk=self.kwargs['user_id'])) - student_profile = get_student_profile(user=user, site=self.request.site, profile_type=StudentTypes.REGULAR) + student_profile = get_student_profile(user=user, site=Site.objects.get(pk=settings.SITE_ID), profile_type=StudentTypes.REGULAR) if student_profile is None: messages.error(self.request, "Профиль обычного студента не найден.") return redirect(self.get_success_url()) @@ -339,7 +340,7 @@ def get_queryset(self): return (CertificateOfParticipation.objects .filter(student_profile__user_id=self.kwargs['user_id']) .select_related('student_profile')) - + def filter_enrollments(self, enrollments): # Only courses within study period of regular student profile must be included start_term_pair = date_to_term_pair(datetime(day=1, month=9, year=self.object.student_profile.year_of_admission, diff --git a/lms/filters.py b/lms/filters.py index 088922483f..a297b19656 100644 --- a/lms/filters.py +++ b/lms/filters.py @@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError from django.forms import SlugField, forms from django.http import QueryDict +from django.contrib.sites.models import Site from core.models import Branch from courses.constants import SemesterTypes @@ -110,15 +111,15 @@ def __init__(self, data=None, queryset=None, request=None, **kwargs): branch_code = data.pop("branch", None) if request.user.is_authenticated and request.user.roles.issubset(student_permission_roles): profiles = get_student_profiles(user=request.user, - site=request.site) + site=Site.objects.get(pk=settings.SITE_ID)) user_branch_ids = [profile.branch_id for profile in profiles] self.user_branches = Branch.objects.filter( active=True, id__in=user_branch_ids, - site_id=request.site.pk + site_id=settings.SITE_ID ) main_branch_code = get_student_profile(user=request.user, - site=request.site).branch.code + site=Site.objects.get(pk=settings.SITE_ID)).branch.code if not branch_code and main_branch_code: branch_code = [main_branch_code] else: @@ -138,7 +139,7 @@ def __init__(self, data=None, queryset=None, request=None, **kwargs): if b.established <= current_term.academic_year] def get_branches(self, request): - return Branch.objects.for_site(request.site.pk) + return Branch.objects.for_site(settings.SITE_ID) @property def form(self): diff --git a/lms/settings/base.py b/lms/settings/base.py index c5f9d68447..02355d77b4 100644 --- a/lms/settings/base.py +++ b/lms/settings/base.py @@ -1,4 +1,5 @@ import logging +import os import warnings from pathlib import Path from typing import Any, Dict, List, Optional @@ -8,6 +9,8 @@ import django +import ssl + env = environ.Env() # Try to read .env file, if it's not present, assume that application # is deployed to production and skip reading the file @@ -15,7 +18,6 @@ warnings.simplefilter("ignore") environ.Env.read_env(env_file=env.str("ENV_FILE", default=None)) - ROOT_DIR = Path(__file__).parents[2] SHARED_APPS_DIR = ROOT_DIR / "apps" @@ -137,8 +139,16 @@ } # https://docs.djangoproject.com/en/dev/ref/settings/#databases -DATABASES = {"default": env.db_url(var="DATABASE_URL")} - +DATABASES = { + "default": { + "ENGINE": env.str("DATABASE_ENGINE"), + "NAME": env.str("DATABASE_NAME"), + "USER": env.str("DATABASE_USER"), + "PASSWORD": env.str("DATABASE_PASSWORD"), + "HOST": env.str("DATABASE_HOST"), + "PORT": env.str("DATABASE_PORT"), + } +} MIDDLEWARE = [ # TODO: Return SecurityMiddleware or configure security with nginx-ingress @@ -159,11 +169,13 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} +# SOURCE_SSL_CA_CERT = "/var/www/.redis/YandexInternalRootCA.crt" + REDIS_PASSWORD = env.str("REDIS_PASSWORD", default=None) REDIS_HOST = env.str("REDIS_HOST", default="127.0.0.1") -REDIS_PORT = env.int("REDIS_PORT", default=6379) -REDIS_DB_INDEX = env.int("REDIS_DB_INDEX", default=SITE_ID) -REDIS_SSL = env.bool("REDIS_SSL", default=True) +REDIS_PORT = 6379 # env.int("REDIS_PORT", default=6379) +REDIS_DB_INDEX = 3 # env.int("REDIS_DB_INDEX", default=SITE_ID) +REDIS_SSL = False #env.bool("REDIS_SSL", default=True) RQ_QUEUES = { "default": { "HOST": REDIS_HOST, @@ -171,6 +183,11 @@ "DB": REDIS_DB_INDEX, "PASSWORD": REDIS_PASSWORD, "SSL": REDIS_SSL, + # 'REDIS_CLIENT_KWARGS': { + # 'ssl_ca_certs': SOURCE_SSL_CA_CERT, + # # 'ssl_cert_reqs': None, + # 'ssl_min_version': ssl.TLSVersion.TLSv1_3, + # }, }, "high": { "HOST": REDIS_HOST, @@ -178,6 +195,11 @@ "DB": REDIS_DB_INDEX, "PASSWORD": REDIS_PASSWORD, "SSL": REDIS_SSL, + # 'REDIS_CLIENT_KWARGS': { + # 'ssl_ca_certs': SOURCE_SSL_CA_CERT, + # # 'ssl_cert_reqs': None, + # 'ssl_min_version': ssl.TLSVersion.TLSv1_3, + # }, }, } @@ -242,6 +264,7 @@ "messages": "core.jinja2.globals.messages", "get_menu": "core.jinja2.globals.generate_menu", "crispy": "core.jinja2.globals.crispy", + "site": "core.jinja2.globals.site_context", # FIXME: move from django template tags "can_enroll_in_course": "core.templatetags.core_tags.can_enroll_in_course", }, @@ -294,6 +317,7 @@ "core.context_processors.subdomain", "core.context_processors.common_context", "core.context_processors.js_config", + "core.context_processors.site_context", "django_admin_env_notice.context_processors.from_settings", ), "debug": DEBUG, diff --git a/lms/views.py b/lms/views.py index 206f8e1935..1ec58589f4 100644 --- a/lms/views.py +++ b/lms/views.py @@ -10,6 +10,8 @@ from django.http import HttpResponseRedirect from django.utils.translation import pgettext_lazy from django.views import View +from django.conf import settings +from django.contrib.sites.models import Site from core.exceptions import Redirect from core.urls import reverse @@ -64,7 +66,7 @@ def get_queryset(self): )) courses = Course.objects - student_profile = user.get_student_profile(site=self.request.site) + student_profile = user.get_student_profile(site=Site.objects.get(pk=settings.SITE_ID)) if not user.is_curator and not user.is_teacher: if student_profile is None: courses = courses.none() From ff18e368fcf18fb5e7aa1ddd85b68caf1a2efe69 Mon Sep 17 00:00:00 2001 From: Sofya Anokhovskaya Date: Mon, 26 May 2025 15:07:13 +0300 Subject: [PATCH 2/6] fix: site id --- apps/templates/learning/study/honor_code.html | 2 +- apps/templates/learning/study/internships.html | 2 +- apps/templates/learning/study/programs.html | 3 +-- apps/templates/learning/study/useful.html | 3 +-- lms/jinja2/lms/admission/confirmation_of_acceptance.html | 2 +- lms/jinja2/lms/layouts/_top_menu.html | 2 +- lms/jinja2/lms/layouts/_v2_top_menu.html | 2 +- lms/jinja2/lms/layouts/v1_base.html | 4 ++-- lms/jinja2/lms/layouts/v2_base.html | 2 +- lms/jinja2/lms/study/course_list.html | 4 ++-- lms/jinja2/lms/user_profile/user_detail.html | 2 +- lms/templates/_top_menu.html | 2 +- lms/templates/base.html | 4 ++-- 13 files changed, 16 insertions(+), 18 deletions(-) diff --git a/apps/templates/learning/study/honor_code.html b/apps/templates/learning/study/honor_code.html index 1515cc52b1..a307a24fd2 100644 --- a/apps/templates/learning/study/honor_code.html +++ b/apps/templates/learning/study/honor_code.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% load i18n %} -{% block title %}Кодекс чести студента - {{ request.site.name }}{% endblock title %} +{% block title %}Кодекс чести студента - {{ site.name }}{% endblock title %} {% block content %}
diff --git a/apps/templates/learning/study/internships.html b/apps/templates/learning/study/internships.html index 78ff6c1de4..10f9afb935 100644 --- a/apps/templates/learning/study/internships.html +++ b/apps/templates/learning/study/internships.html @@ -3,7 +3,7 @@ {% load static %} {% block body_attrs %} class="gray"{% endblock body_attrs %} {% load markdown from core_tags %} -{% block title %}Проекты организаторов - {{ request.site.name }}{% endblock title %} +{% block title %}Проекты организаторов - {{ site.name }}{% endblock title %} {% block content %}
diff --git a/apps/templates/learning/study/programs.html b/apps/templates/learning/study/programs.html index 5b4b9761d4..bff8d6a089 100644 --- a/apps/templates/learning/study/programs.html +++ b/apps/templates/learning/study/programs.html @@ -2,7 +2,7 @@ {% load i18n %} {% load static %} {% block body_attrs %} class="gray"{% endblock body_attrs %} -{% block title %}Программы обучения - {{ request.site.name }}{% endblock title %} +{% block title %}Программы обучения - {{ site.name }}{% endblock title %} {% block content %}
@@ -21,4 +21,3 @@
{% endblock content %} - diff --git a/apps/templates/learning/study/useful.html b/apps/templates/learning/study/useful.html index 6519179317..58bdbd18af 100644 --- a/apps/templates/learning/study/useful.html +++ b/apps/templates/learning/study/useful.html @@ -2,7 +2,7 @@ {% load i18n %} {% load static %} {% block body_attrs %} class="gray"{% endblock body_attrs %} -{% block title %}Полезное - {{ request.site.name }}{% endblock title %} +{% block title %}Полезное - {{ site.name }}{% endblock title %} {% block content %}
@@ -21,4 +21,3 @@
{% endblock content %} - diff --git a/lms/jinja2/lms/admission/confirmation_of_acceptance.html b/lms/jinja2/lms/admission/confirmation_of_acceptance.html index 8e2ff2f0c7..23d4a5e441 100644 --- a/lms/jinja2/lms/admission/confirmation_of_acceptance.html +++ b/lms/jinja2/lms/admission/confirmation_of_acceptance.html @@ -35,7 +35,7 @@

Форма авторизации

Создание профиля студента ШАД

Поздравляем вас с поступлением в Школу анализа данных и приглашаем создать профиль на сайте {{ - request.site.name }}.

+ site.name }}.

Подтвердите почту, проверьте информацию из анкеты и добавьте ту, которой не хватает. Обратите внимание, что все данные, которые вы здесь укажете, будут использоваться для зачисления в ШАД и для выдачи справок/диплома.

Если какие-то из неизменяемых полей заполнены неверно, то напишите на почту {{ contact_email }}

diff --git a/lms/jinja2/lms/layouts/_top_menu.html b/lms/jinja2/lms/layouts/_top_menu.html index 37f7b541d8..df4105be37 100644 --- a/lms/jinja2/lms/layouts/_top_menu.html +++ b/lms/jinja2/lms/layouts/_top_menu.html @@ -10,7 +10,7 @@
- {{ request.site.name }} + {{ site().name }}
8 %} class="__narrow"{% endif %}> {% for menu_item in menu %} diff --git a/lms/jinja2/lms/layouts/_v2_top_menu.html b/lms/jinja2/lms/layouts/_v2_top_menu.html index 3ce487de87..0ad2413f56 100644 --- a/lms/jinja2/lms/layouts/_v2_top_menu.html +++ b/lms/jinja2/lms/layouts/_v2_top_menu.html @@ -14,7 +14,7 @@ aria-expanded="false" aria-label="Toggle navigation"> - diff --git a/lms/jinja2/lms/layouts/v1_base.html b/lms/jinja2/lms/layouts/v1_base.html index 85864b135d..cd3e832247 100644 --- a/lms/jinja2/lms/layouts/v1_base.html +++ b/lms/jinja2/lms/layouts/v1_base.html @@ -5,7 +5,7 @@ - {% block title %}{{ request.site.name }}{% endblock title %} + {% block title %}{{ site().name }}{% endblock title %} @@ -63,7 +63,7 @@ diff --git a/lms/jinja2/lms/layouts/v2_base.html b/lms/jinja2/lms/layouts/v2_base.html index 696d3d674b..21ad076aab 100644 --- a/lms/jinja2/lms/layouts/v2_base.html +++ b/lms/jinja2/lms/layouts/v2_base.html @@ -3,7 +3,7 @@ - {% block title %}{{ request.site.name }}{% endblock title %} + {% block title %}{{ site().name }}{% endblock title %} diff --git a/lms/jinja2/lms/study/course_list.html b/lms/jinja2/lms/study/course_list.html index 9e93d7ecdd..6517ab133f 100644 --- a/lms/jinja2/lms/study/course_list.html +++ b/lms/jinja2/lms/study/course_list.html @@ -43,7 +43,7 @@ {% if course.duration == CourseDurations.SECOND_HALF %} {% endif %} - {%- if request.site.pk != course.main_branch.site_id and course.is_club_course -%} + {%- if site.pk != course.main_branch.site_id and course.is_club_course -%} , курс CS клуба{% endif %}    @@ -90,7 +90,7 @@ {{ course.meta_course.name }} - {%- if request.site.pk != course.main_branch.site_id and course.is_club_course -%} + {%- if site.pk != course.main_branch.site_id and course.is_club_course -%} , курс CS клуба{% endif %} {% if course.duration == CourseDurations.FIRST_HALF %} diff --git a/lms/jinja2/lms/user_profile/user_detail.html b/lms/jinja2/lms/user_profile/user_detail.html index cd2a1a4326..79814d3823 100644 --- a/lms/jinja2/lms/user_profile/user_detail.html +++ b/lms/jinja2/lms/user_profile/user_detail.html @@ -3,7 +3,7 @@ {% set request_user = request.user %} {% block title -%} - {{ profile_user.get_short_name() }} - {% trans %}Profile{% endtrans %} {% trans %}on site{% endtrans %} {{ request.site.name }} + {{ profile_user.get_short_name() }} - {% trans %}Profile{% endtrans %} {% trans %}on site{% endtrans %} {{ site().name }} {%- endblock title %} {% block body_attrs %} data-init-sections="profile"{% endblock body_attrs %} diff --git a/lms/templates/_top_menu.html b/lms/templates/_top_menu.html index 8b56d78beb..d40a8a0f4f 100644 --- a/lms/templates/_top_menu.html +++ b/lms/templates/_top_menu.html @@ -13,7 +13,7 @@
- {{ request.site.name }} + {{ site.name }}
8 %} class="__narrow"{% endif %}> {% for menu_item in menu %} diff --git a/lms/templates/base.html b/lms/templates/base.html index ad9c5ee276..a1b39db3d1 100644 --- a/lms/templates/base.html +++ b/lms/templates/base.html @@ -4,7 +4,7 @@ - {% block title %}{{ request.site.name }}{% endblock title %} + {% block title %}{{ site.name }}{% endblock title %} @@ -58,7 +58,7 @@ From 75034fe8e06ff4518aef389e02d7ecd10c31993b Mon Sep 17 00:00:00 2001 From: Sofya Anokhovskaya Date: Mon, 26 May 2025 15:52:10 +0300 Subject: [PATCH 3/6] fix: tests --- apps/core/tests/test_models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/core/tests/test_models.py b/apps/core/tests/test_models.py index 4106072456..c4ecc1771f 100644 --- a/apps/core/tests/test_models.py +++ b/apps/core/tests/test_models.py @@ -47,8 +47,6 @@ def test_manager_site_configuration_get_current(rf, settings): request.path = '/' assert SiteConfiguration.objects.get_current() == site_configuration2 assert SiteConfiguration.objects.get_current(request) == site_configuration2 - settings.SITE_ID = None - assert SiteConfiguration.objects.get_current(request) == site_configuration1 @pytest.mark.django_db From 9a2a544dcfb6a4b6323f7548d21f1a43e68a8868 Mon Sep 17 00:00:00 2001 From: Sofya Anokhovskaya Date: Mon, 26 May 2025 16:05:45 +0300 Subject: [PATCH 4/6] fix: arc --- lms/settings/base.py | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/lms/settings/base.py b/lms/settings/base.py index 02355d77b4..cc1b3011c3 100644 --- a/lms/settings/base.py +++ b/lms/settings/base.py @@ -1,5 +1,4 @@ import logging -import os import warnings from pathlib import Path from typing import Any, Dict, List, Optional @@ -9,8 +8,6 @@ import django -import ssl - env = environ.Env() # Try to read .env file, if it's not present, assume that application # is deployed to production and skip reading the file @@ -18,6 +15,7 @@ warnings.simplefilter("ignore") environ.Env.read_env(env_file=env.str("ENV_FILE", default=None)) + ROOT_DIR = Path(__file__).parents[2] SHARED_APPS_DIR = ROOT_DIR / "apps" @@ -139,16 +137,8 @@ } # https://docs.djangoproject.com/en/dev/ref/settings/#databases -DATABASES = { - "default": { - "ENGINE": env.str("DATABASE_ENGINE"), - "NAME": env.str("DATABASE_NAME"), - "USER": env.str("DATABASE_USER"), - "PASSWORD": env.str("DATABASE_PASSWORD"), - "HOST": env.str("DATABASE_HOST"), - "PORT": env.str("DATABASE_PORT"), - } -} +DATABASES = {"default": env.db_url(var="DATABASE_URL")} + MIDDLEWARE = [ # TODO: Return SecurityMiddleware or configure security with nginx-ingress @@ -169,13 +159,11 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} -# SOURCE_SSL_CA_CERT = "/var/www/.redis/YandexInternalRootCA.crt" - REDIS_PASSWORD = env.str("REDIS_PASSWORD", default=None) REDIS_HOST = env.str("REDIS_HOST", default="127.0.0.1") -REDIS_PORT = 6379 # env.int("REDIS_PORT", default=6379) -REDIS_DB_INDEX = 3 # env.int("REDIS_DB_INDEX", default=SITE_ID) -REDIS_SSL = False #env.bool("REDIS_SSL", default=True) +REDIS_PORT = env.int("REDIS_PORT", default=6379) +REDIS_DB_INDEX = env.int("REDIS_DB_INDEX", default=SITE_ID) +REDIS_SSL = env.bool("REDIS_SSL", default=True) RQ_QUEUES = { "default": { "HOST": REDIS_HOST, @@ -183,11 +171,6 @@ "DB": REDIS_DB_INDEX, "PASSWORD": REDIS_PASSWORD, "SSL": REDIS_SSL, - # 'REDIS_CLIENT_KWARGS': { - # 'ssl_ca_certs': SOURCE_SSL_CA_CERT, - # # 'ssl_cert_reqs': None, - # 'ssl_min_version': ssl.TLSVersion.TLSv1_3, - # }, }, "high": { "HOST": REDIS_HOST, @@ -195,11 +178,6 @@ "DB": REDIS_DB_INDEX, "PASSWORD": REDIS_PASSWORD, "SSL": REDIS_SSL, - # 'REDIS_CLIENT_KWARGS': { - # 'ssl_ca_certs': SOURCE_SSL_CA_CERT, - # # 'ssl_cert_reqs': None, - # 'ssl_min_version': ssl.TLSVersion.TLSv1_3, - # }, }, } From 711c42a7374d691e2d2e5b36ee7f7e46bfe59cb7 Mon Sep 17 00:00:00 2001 From: Sofya Anokhovskaya Date: Mon, 26 May 2025 19:54:18 +0300 Subject: [PATCH 5/6] fix: cr --- apps/staff/api/views.py | 12 ++++++++++-- apps/staff/views/views.py | 8 ++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/staff/api/views.py b/apps/staff/api/views.py index d5402694c5..659b9f52f0 100644 --- a/apps/staff/api/views.py +++ b/apps/staff/api/views.py @@ -12,6 +12,7 @@ from api.views import APIBaseView from core.http import HttpRequest from learning.api.serializers import StudentProfileSerializer +from learning.models import GraduateProfile from users.filters import StudentFilter from users.models import StudentProfile from users.services import create_graduate_profiles @@ -28,16 +29,23 @@ class StudentSearchJSONView(ListAPIView): filterset_class = StudentFilter class OutputSerializer(StudentProfileSerializer): + graduation_year = serializers.SerializerMethodField() + class Meta(StudentProfileSerializer.Meta): - fields = ('pk', 'short_name', 'user_id') + fields = ('pk', 'short_name', 'user_id', 'graduation_year') + + def get_graduation_year(self, obj): + if hasattr(obj, 'graduate_profile'): + return obj.graduate_profile.graduation_year + return None def get_serializer_class(self): return self.OutputSerializer def get_queryset(self): return (StudentProfile.objects + .select_related('user', 'graduate_profile') .filter(site=Site.objects.get(id=settings.SITE_ID)) - .select_related('user') .only('user__username', 'user__first_name', 'user__last_name', 'user_id') .order_by('user__last_name', diff --git a/apps/staff/views/views.py b/apps/staff/views/views.py index 29eed4a762..8ace5006b6 100644 --- a/apps/staff/views/views.py +++ b/apps/staff/views/views.py @@ -138,6 +138,10 @@ def get_context_data(self, **kwargs): .values_list("graduation_year", flat=True) .order_by("graduation_year") .distinct() + ), + "graduation_years": ( + GraduateProfile.objects.filter( + student_profile__site=settings.SITE_ID).values_list("graduation_year", flat=True).order_by("graduation_year").distinct() ) } return context @@ -875,7 +879,7 @@ def get(self, request, year, month, day, *args, **kwargs): diploma_issued_on = datetime.date(int(year), int(month), int(day)) report = OfficialDiplomasReport(diploma_issued_on) site_aware_queryset = report.get_queryset().filter( - branch__site=Site.objects.get(pk=settings.SITE_ID) + branch__site_id = settings.SITE_ID ) if not site_aware_queryset.count(): raise Http404 @@ -891,7 +895,7 @@ class OfficialDiplomasTeXView(CuratorOnlyMixin, generic.TemplateView): def get_context_data(self, year, month, day, **kwargs): diploma_issued_on = datetime.date(int(year), int(month), int(day)) report = OfficialDiplomasReport(diploma_issued_on) - student_profiles = report.get_queryset().filter(branch__site=Site.objects.get(pk=settings.SITE_ID)) + student_profiles = report.get_queryset().filter(branch__site_id = settings.SITE_ID) students = (sp.user for sp in student_profiles) courses_qs = report.get_courses_queryset(students).annotate( classes_total=Count("courseclass") From 396dc637ebcc321058e5a858fec5722e84e909ae Mon Sep 17 00:00:00 2001 From: Sofya Anokhovskaya Date: Mon, 26 May 2025 20:08:34 +0300 Subject: [PATCH 6/6] fix: cr --- apps/staff/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/staff/api/views.py b/apps/staff/api/views.py index 659b9f52f0..f665971e6e 100644 --- a/apps/staff/api/views.py +++ b/apps/staff/api/views.py @@ -44,8 +44,8 @@ def get_serializer_class(self): def get_queryset(self): return (StudentProfile.objects - .select_related('user', 'graduate_profile') .filter(site=Site.objects.get(id=settings.SITE_ID)) + .select_related('user', 'graduate_profile') .only('user__username', 'user__first_name', 'user__last_name', 'user_id') .order_by('user__last_name',