diff --git a/apps/admission/locale/ru/LC_MESSAGES/django.po b/apps/admission/locale/ru/LC_MESSAGES/django.po index d07b880070..c314966c3a 100644 --- a/apps/admission/locale/ru/LC_MESSAGES/django.po +++ b/apps/admission/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 14:53+0000\n" +"POT-Creation-Date: 2024-08-21 13:32+0000\n" "PO-Revision-Date: 2024-07-23 17:04+0000\n" "Last-Translator: Дмитрий Чернушевич \n" "Language-Team: LANGUAGE \n" diff --git a/apps/courses/tests/test_views.py b/apps/courses/tests/test_views.py index d57b4a3e92..354c9f7b34 100644 --- a/apps/courses/tests/test_views.py +++ b/apps/courses/tests/test_views.py @@ -25,11 +25,10 @@ from files.response import XAccelRedirectFileResponse from files.views import ProtectedFileDownloadView from learning.models import Enrollment -from learning.settings import Branches, GradeTypes, StudentStatuses +from learning.settings import Branches, GradeTypes, StudentStatuses, EnrollmentTypes from learning.invitation.views import complete_student_profile from learning.tests.factories import EnrollmentFactory, CourseInvitationFactory from users.constants import Roles -from users.models import StudentProfile from users.services import update_student_status from users.tests.factories import ( CuratorFactory, StudentFactory, StudentProfileFactory, TeacherFactory, UserFactory @@ -325,8 +324,14 @@ def test_view_course_detail_enroll_by_invitation(client): assert course_invitation.enrolled_students.count() == 0 url = course_invitation.get_absolute_url() - response = client.post(url, follow=True) - assert response.redirect_chain[-1][0] == course.get_absolute_url() + response = client.get(url) + assert response.status_code == 200 + html = response.content.decode('utf-8') + assert 'Как вы хотите записаться на курс?' in html + response = client.post(url, data={ + "type": EnrollmentTypes.REGULAR + }) + assert response.status_code == 302 assert Enrollment.objects.filter(invitation=course_invitation.invitation).exists() assert course_invitation.enrolled_students.count() == 1 assert current_profile == course_invitation.enrolled_students.all().first() diff --git a/apps/htmlpages/locale/ru/LC_MESSAGES/django.po b/apps/htmlpages/locale/ru/LC_MESSAGES/django.po index e724371647..db2a54222d 100644 --- a/apps/htmlpages/locale/ru/LC_MESSAGES/django.po +++ b/apps/htmlpages/locale/ru/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 14:53+0000\n" +"POT-Creation-Date: 2024-08-21 13:32+0000\n" "PO-Revision-Date: 2015-03-18 08:34+0000\n" "Last-Translator: Jannis Leidel \n" "Language-Team: Russian (http://www.transifex.com/projects/p/django/language/" diff --git a/apps/learning/forms.py b/apps/learning/forms.py index 102075151a..4c9f1a70ec 100644 --- a/apps/learning/forms.py +++ b/apps/learning/forms.py @@ -24,6 +24,7 @@ ) from .models import AssignmentComment +from .settings import EnrollmentTypes, InvitationEnrollmentTypes class SubmitLink(BaseInput): @@ -284,13 +285,41 @@ def clean_testimonial(self): class CourseEnrollmentForm(forms.Form): + type = forms.ChoiceField( + label=_("Как вы хотите записаться на курс?"), + choices=EnrollmentTypes.choices, + initial=EnrollmentTypes.REGULAR, + required=True + ) reason = forms.CharField( label=_("Почему вы выбрали этот курс?"), widget=forms.Textarea(), required=False) def __init__(self, **kwargs): + ask_enrollment_reason = kwargs.pop('ask_enrollment_reason', None) + super().__init__(**kwargs) + if not ask_enrollment_reason: + self.fields.pop('reason') + + self.helper = FormHelper(self) + self.helper.layout.append(Submit('enroll', 'Записаться на курс')) + +class CourseInvitationEnrollmentForm(forms.Form): + type = forms.ChoiceField( + label=_("Как вы хотите записаться на курс?"), + choices=EnrollmentTypes.choices, + initial=EnrollmentTypes.REGULAR, + required=True + ) + + def __init__(self, **kwargs): + enrollment_type = kwargs.pop('enrollment_type', None) super().__init__(**kwargs) + if enrollment_type != InvitationEnrollmentTypes.ANY: + assert enrollment_type in EnrollmentTypes.values + self.fields["type"].initial = enrollment_type + self.fields["type"].disabled = True self.helper = FormHelper(self) self.helper.layout.append(Submit('enroll', 'Записаться на курс')) diff --git a/apps/learning/gradebook/data.py b/apps/learning/gradebook/data.py index 9374d3f539..816b99fee8 100644 --- a/apps/learning/gradebook/data.py +++ b/apps/learning/gradebook/data.py @@ -13,7 +13,7 @@ from courses.constants import AssignmentStatus from courses.models import Assignment, Course from learning.models import Enrollment, StudentAssignment, StudentGroup -from learning.settings import GradeTypes +from learning.settings import GradeTypes, EnrollmentTypes __all__ = ('GradebookStudent', 'GradeBookData', 'gradebook_data', 'get_student_assignment_state') @@ -164,7 +164,8 @@ def gradebook_data(course: Course, student_group: Optional[int] = None) -> Grade # Collect active enrollments enrolled_students = OrderedDict() course_enrollments = (Enrollment.active - .filter(course=course)) + .filter(course=course) + .exclude(type=EnrollmentTypes.LECTIONS_ONLY)) if student_group is not None: course_enrollments = course_enrollments.filter(student_group=student_group) enrollments = (course_enrollments diff --git a/apps/learning/gradebook/tests.py b/apps/learning/gradebook/tests.py index e665cdb29e..c4d4d72672 100644 --- a/apps/learning/gradebook/tests.py +++ b/apps/learning/gradebook/tests.py @@ -38,7 +38,7 @@ get_personal_assignments_by_stepik_id ) from learning.settings import ( - AssignmentScoreUpdateSource, Branches, GradeTypes, StudentStatuses, EnrollmentGradeUpdateSource + AssignmentScoreUpdateSource, Branches, GradeTypes, StudentStatuses, EnrollmentGradeUpdateSource, EnrollmentTypes ) from learning.tests.factories import ( AssignmentCommentFactory, EnrollmentFactory, StudentAssignmentFactory, @@ -219,11 +219,16 @@ def test_save_gradebook(client, assert_redirect): @pytest.mark.django_db def test_gradebook_data(settings): co = CourseFactory() - e1, e2, e3, e4, e5 = EnrollmentFactory.create_batch(5, course=co) + e1, e2, e3, e4, e5, e6 = EnrollmentFactory.create_batch(6, course=co) a1, a2, a3 = AssignmentFactory.create_batch(3, course=co, passing_score=1, maximum_score=10) data = gradebook_data(co) assert len(data.assignments) == 3 + assert len(data.students) == 6 + e6.type = EnrollmentTypes.LECTIONS_ONLY + e6.save() + data = gradebook_data(co) + assert len(data.assignments) == 3 assert len(data.students) == 5 e1.is_deleted = True e1.save() diff --git a/apps/learning/migrations/0052_auto_20240821_1331.py b/apps/learning/migrations/0052_auto_20240821_1331.py new file mode 100644 index 0000000000..d4f7ffdb00 --- /dev/null +++ b/apps/learning/migrations/0052_auto_20240821_1331.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.18 on 2024-08-21 13:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('learning', '0051_auto_20240802_2000'), + ] + + operations = [ + migrations.AddField( + model_name='courseinvitation', + name='enrollment_type', + field=models.CharField(choices=[('Regular', 'InvitationEnrollmentTypes|Regular'), ('lections', 'InvitationEnrollmentTypes|Lections'), ('Any', 'InvitationEnrollmentTypes|Any')], default='Any', max_length=100, verbose_name='Enrollment|type'), + ), + migrations.AddField( + model_name='enrollment', + name='type', + field=models.CharField(choices=[('Regular', 'EnrollmentTypes|Regular'), ('lections', 'EnrollmentTypes|Lections')], default='Regular', max_length=100, verbose_name='Enrollment|type'), + ), + ] diff --git a/apps/learning/models.py b/apps/learning/models.py index 45d05a3d51..b16d5100ed 100644 --- a/apps/learning/models.py +++ b/apps/learning/models.py @@ -43,7 +43,7 @@ ) from learning.settings import ( ENROLLMENT_DURATION, AssignmentScoreUpdateSource, GradeTypes, GradingSystems, EnrollmentGradeUpdateSource, - InvitationCategories + InvitationCategories, EnrollmentTypes, InvitationEnrollmentTypes ) from learning.utils import humanize_duration, grade_to_base_system from users.constants import ThumbnailSizes @@ -358,6 +358,11 @@ class Enrollment(TimezoneAwareMixin, TimeStampedModel): Course, verbose_name=_("Course offering"), on_delete=models.CASCADE) + type = models.CharField( + verbose_name=_("Enrollment|type"), + max_length=100, + choices=EnrollmentTypes.choices, + default=EnrollmentTypes.REGULAR) grade = models.CharField( verbose_name=_("Enrollment|grade"), max_length=100, @@ -480,6 +485,11 @@ class CourseInvitation(models.Model): verbose_name=_("CourseOffering|capacity"), default=0, help_text=_("0 - unlimited")) + enrollment_type = models.CharField( + verbose_name=_("Enrollment|type"), + max_length=100, + choices=InvitationEnrollmentTypes.choices, + default=InvitationEnrollmentTypes.ANY) enrolled_students = models.ManyToManyField( StudentProfile, verbose_name=_("Enrolled students"), diff --git a/apps/learning/services/enrollment_service.py b/apps/learning/services/enrollment_service.py index abfc272a9d..d36323067c 100644 --- a/apps/learning/services/enrollment_service.py +++ b/apps/learning/services/enrollment_service.py @@ -16,7 +16,7 @@ from learning.services.notification_service import ( remove_course_notifications_for_student ) -from learning.settings import GradeTypes, EnrollmentGradeUpdateSource +from learning.settings import GradeTypes, EnrollmentGradeUpdateSource, EnrollmentTypes from users.models import StudentProfile, User @@ -42,7 +42,7 @@ def _format_reason_record(reason_text: str, course: Course) -> str: @classmethod def enroll(cls, student_profile: StudentProfile, course: Course, - reason_entry: str = '', + reason_entry: str = '', type: EnrollmentTypes = EnrollmentTypes.REGULAR, student_group: Optional[StudentGroup] = None, **attrs: Any) -> Enrollment: if course.group_mode != CourseGroupModes.NO_GROUPS and not student_group: raise ValidationError("Provide student group value") @@ -81,7 +81,8 @@ def enroll(cls, student_profile: StudentProfile, course: Course, "is_deleted": False, "student_profile": student_profile, "student_group": student_group, - "reason_entry": reason_entry + "reason_entry": reason_entry, + "type": type }) updated = (Enrollment.objects .filter(*filters) diff --git a/apps/learning/settings.py b/apps/learning/settings.py index ddc37bfec6..3bd494eb91 100644 --- a/apps/learning/settings.py +++ b/apps/learning/settings.py @@ -34,6 +34,15 @@ class AcademicDegreeLevels(DjangoChoices): ACADEMIC_LEAVE = C("10", _('academic_leave')) OTHER = C("other", _('Other')) +class EnrollmentTypes(DjangoChoices): + REGULAR = C("Regular", _("EnrollmentTypes|Regular")) + LECTIONS_ONLY = C("lections", _("EnrollmentTypes|Lections")) + +class InvitationEnrollmentTypes(DjangoChoices): + REGULAR = C("Regular", _("InvitationEnrollmentTypes|Regular")) + LECTIONS_ONLY = C("lections", _("InvitationEnrollmentTypes|Lections")) + ANY = C("Any", _("InvitationEnrollmentTypes|Any")) + class InvitationCategories(DjangoChoices): UNIVERSITY = C("university", _("Invitation|University")) STAFF = C("staff", _("Invitation|Staff")) diff --git a/apps/learning/study/tests/test_views.py b/apps/learning/study/tests/test_views.py index 6a4e47d739..448a08c2fe 100644 --- a/apps/learning/study/tests/test_views.py +++ b/apps/learning/study/tests/test_views.py @@ -29,7 +29,7 @@ StudentAssignment ) from learning.permissions import ViewCourses, ViewOwnStudentAssignment -from learning.settings import Branches, StudentStatuses, GradeTypes +from learning.settings import Branches, StudentStatuses, GradeTypes, EnrollmentTypes from learning.tests.factories import ( AssignmentCommentFactory, EnrollmentFactory, StudentAssignmentFactory, CourseInvitationFactory ) @@ -551,7 +551,9 @@ def test_view_student_courses_list_as_invited(client): assert len(response.context_data['ongoing_rest']) == 1 assert len(response.context_data['archive']) == 1 - client.post(course_invitation.get_absolute_url(), follow=True) + client.post(course_invitation.get_absolute_url(), data={ + "type": EnrollmentTypes.REGULAR + }) response = client.get(url) assert len(response.context_data['ongoing_enrolled']) == 2 assert len(response.context_data['ongoing_rest']) == 0 diff --git a/apps/learning/tests/test_admin.py b/apps/learning/tests/test_admin.py index 6c6a5c1a6d..6a30c9ae35 100644 --- a/apps/learning/tests/test_admin.py +++ b/apps/learning/tests/test_admin.py @@ -10,7 +10,7 @@ from core.urls import reverse from courses.tests.factories import AssignmentFactory, CourseFactory from learning.models import AssignmentScoreAuditLog, AssignmentStatus, StudentAssignment, EnrollmentGradeLog -from learning.settings import AssignmentScoreUpdateSource, GradeTypes +from learning.settings import AssignmentScoreUpdateSource, GradeTypes, EnrollmentTypes from learning.tests.factories import EnrollmentFactory from users.tests.factories import CuratorFactory @@ -87,10 +87,13 @@ def test_admin_enrollment_grade_log(client): assert not EnrollmentGradeLog.objects.exists() enrollment_url = reverse('admin:learning_enrollment_change', args=[enrollment.pk]) - form_data = {'student_profile': enrollment.student_profile.pk, - 'grade': GradeTypes.GOOD, - 'grade_history-TOTAL_FORMS': 0, - 'grade_history-INITIAL_FORMS': 0} + form_data = { + 'type': EnrollmentTypes.REGULAR, + 'student_profile': enrollment.student_profile.pk, + 'grade': GradeTypes.GOOD, + 'grade_history-TOTAL_FORMS': 0, + 'grade_history-INITIAL_FORMS': 0 + } client.login(curator) response = client.post(enrollment_url, form_data) assert response.status_code == 302 diff --git a/apps/learning/tests/test_enrollment.py b/apps/learning/tests/test_enrollment.py index 71ba4cd377..c47eef7df1 100644 --- a/apps/learning/tests/test_enrollment.py +++ b/apps/learning/tests/test_enrollment.py @@ -20,7 +20,7 @@ ) from learning.services import EnrollmentService, StudentGroupService from learning.services.enrollment_service import CourseCapacityFull -from learning.settings import Branches, StudentStatuses +from learning.settings import Branches, StudentStatuses, EnrollmentTypes, InvitationEnrollmentTypes from learning.tests.factories import ( CourseInvitationFactory, EnrollmentFactory, StudentGroupFactory ) @@ -38,10 +38,11 @@ def test_service_enroll(settings): semester=current_semester, group_mode=CourseGroupModes.MANUAL) student_group = StudentGroupFactory(course=course) - enrollment = EnrollmentService.enroll(student_profile, course, + enrollment = EnrollmentService.enroll(student_profile, course, type=EnrollmentTypes.LECTIONS_ONLY, student_group=student_group, reason_entry='test enrollment') reason_entry = EnrollmentService._format_reason_record('test enrollment', course) assert enrollment.reason_entry == reason_entry + assert enrollment.type == EnrollmentTypes.LECTIONS_ONLY assert not enrollment.is_deleted assert enrollment.student_group_id == student_group.pk student_group = StudentGroupService.resolve(course, student_profile=student_profile2) @@ -49,6 +50,7 @@ def test_service_enroll(settings): reason_entry='test enrollment', student_group=student_group) assert enrollment.student_group == student_group + assert enrollment.type == EnrollmentTypes.REGULAR @pytest.mark.django_db @@ -87,9 +89,10 @@ def test_enrollment_capacity_view(client): course.save() response = client.get(course.get_absolute_url()) assert smart_bytes(_("Places available")) in response.content - form = {'course_pk': course.pk} - client.post(course.get_enroll_url(), form) - assert 1 == Enrollment.active.filter(student=student, course=course).count() + client.post(course.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) + assert Enrollment.active.filter(student=student, course=course).count() == 1 # Capacity is reached course.refresh_from_db() assert course.learners_count == 1 @@ -99,7 +102,9 @@ def test_enrollment_capacity_view(client): response = client.get(course.get_absolute_url()) assert smart_bytes(_("Enroll in")) not in response.content # POST request should be rejected - response = client.post(course.get_enroll_url(), form) + response = client.post(course.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 302 # Increase capacity course.capacity += 1 @@ -109,7 +114,7 @@ def test_enrollment_capacity_view(client): assert (smart_bytes(_("Places available")) + b": 1") in response.content # Unenroll first student, capacity should increase client.login(student) - response = client.post(course.get_unenroll_url(), form) + client.post(course.get_unenroll_url()) assert Enrollment.active.filter(course=course).count() == 0 course.refresh_from_db() assert course.learners_count == 0 @@ -147,8 +152,9 @@ def test_enrollment(client): course = CourseFactory(main_branch=student1.get_student_profile().branch, semester=current_semester) url = course.get_enroll_url() - form = {'course_pk': course.pk} - response = client.post(url, form) + response = client.post(url, data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 302 assert course.enrollment_set.count() == 1 as_ = AssignmentFactory.create_batch(3, course=course) @@ -156,18 +162,22 @@ def test_enrollment(client): .filter(student=student1) .values_list('student', 'assignment')) co_other = CourseFactory(semester=current_semester) - form.update({'back': 'study:course_list'}) url = co_other.get_enroll_url() - response = client.post(url, form) + response = client.post(url, data={ + "type": EnrollmentTypes.REGULAR, + "back": "study:course_list" + }) assert response.status_code == 302 assert co_other.enrollment_set.count() == 1 assert course.enrollment_set.count() == 1 # Try to enroll to old CO old_semester = SemesterFactory.create(year=2010) old_co = CourseFactory.create(semester=old_semester) - form = {'course_pk': old_co.pk} url = old_co.get_enroll_url() - assert client.post(url, form).status_code == 403 + response = client.post(url, data={ + "type": EnrollmentTypes.REGULAR + }) + assert response.status_code == 403 @pytest.mark.django_db @@ -177,18 +187,22 @@ def test_enrollment_reason_entry(client): today = now_local(student_profile.user.time_zone) current_term = SemesterFactory.create_current( enrollment_period__ends_on=today.date()) - course = CourseFactory(main_branch=student_profile.branch, semester=current_term) - form = {'course_pk': course.pk, 'reason': 'foo'} - client.post(course.get_enroll_url(), form) + course = CourseFactory(main_branch=student_profile.branch, semester=current_term, ask_enrollment_reason=True) + client.post(course.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR, + "reason": "foo" + }) assert Enrollment.active.count() == 1 date = today.strftime(DATE_FORMAT_RU) assert Enrollment.objects.first().reason_entry == f'{date}\nfoo\n\n' - client.post(course.get_unenroll_url(), form) + client.post(course.get_unenroll_url()) assert Enrollment.active.count() == 0 assert Enrollment.objects.first().reason_entry == f'{date}\nfoo\n\n' # Enroll for the second time, first entry reason should be saved - form['reason'] = 'bar' - client.post(course.get_enroll_url(), form) + client.post(course.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR, + "reason": "bar" + }) assert Enrollment.active.count() == 1 assert Enrollment.objects.first().reason_entry == f'{date}\nbar\n\n{date}\nfoo\n\n' @@ -200,28 +214,37 @@ def test_enrollment_leave_reason(client): today = now_local(student_profile.user.time_zone) current_semester = SemesterFactory.create_current( enrollment_period__ends_on=today.date()) - co = CourseFactory(main_branch=student_profile.branch, semester=current_semester) - form = {'course_pk': co.pk} - client.post(co.get_enroll_url(), form) + co = CourseFactory(main_branch=student_profile.branch, semester=current_semester, ask_enrollment_reason=True) + client.post(co.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert Enrollment.active.count() == 1 assert Enrollment.objects.first().reason_entry == '' - form['reason'] = 'foo' - client.post(co.get_unenroll_url(), form) + client.post(co.get_unenroll_url(), data={ + "type": EnrollmentTypes.REGULAR, + "reason": "foo" + }) assert Enrollment.active.count() == 0 e = Enrollment.objects.first() assert today.strftime(DATE_FORMAT_RU) in e.reason_leave assert 'foo' in e.reason_leave # Enroll for the second time and leave with another reason - client.post(co.get_enroll_url(), form) + client.post(co.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert Enrollment.active.count() == 1 - form['reason'] = 'bar' - client.post(co.get_unenroll_url(), form) + client.post(co.get_unenroll_url(), data={ + "type": EnrollmentTypes.REGULAR, + "reason": "bar" + }) assert Enrollment.active.count() == 0 e = Enrollment.objects.first() assert 'foo' in e.reason_leave assert 'bar' in e.reason_leave co_other = CourseFactory.create(semester=current_semester) - client.post(co_other.get_enroll_url(), {}) + client.post(co_other.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) e_other = Enrollment.active.filter(course=co_other).first() assert not e_other.reason_entry assert not e_other.reason_leave @@ -234,19 +257,22 @@ def test_unenrollment(client, settings, assert_redirect): current_semester = SemesterFactory.create_current() course = CourseFactory(main_branch=student.get_student_profile().branch, semester=current_semester) as_ = AssignmentFactory.create_batch(3, course=course) - form = {'course_pk': course.pk} # Enrollment already closed ep = EnrollmentPeriod.objects.get(semester=current_semester, site_id=settings.SITE_ID) today = timezone.now() ep.ends_on = (today - datetime.timedelta(days=1)).date() ep.save() - response = client.post(course.get_enroll_url(), form) + response = client.post(course.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 403 ep.ends_on = (today + datetime.timedelta(days=1)).date() ep.save() - response = client.post(course.get_enroll_url(), form, follow=True) - assert response.status_code == 200 + response = client.post(course.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) + assert response.status_code == 302 assert Enrollment.objects.count() == 1 response = client.get(course.get_absolute_url()) assert smart_bytes("Unenroll") in response.content @@ -254,7 +280,7 @@ def test_unenrollment(client, settings, assert_redirect): assert Enrollment.objects.count() == 1 enrollment = Enrollment.objects.first() assert not enrollment.is_deleted - client.post(course.get_unenroll_url(), form) + client.post(course.get_unenroll_url()) assert Enrollment.active.filter(student=student, course=course).count() == 0 assert Enrollment.objects.count() == 1 enrollment = Enrollment.objects.first() @@ -265,7 +291,9 @@ def test_unenrollment(client, settings, assert_redirect): assignment__course=course)) assert len(a_ss) == 3 # On re-enroll use old record - client.post(course.get_enroll_url(), form) + client.post(course.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert Enrollment.objects.count() == 1 enrollment = Enrollment.objects.first() assert enrollment.pk == enrollment_id @@ -277,8 +305,10 @@ def test_unenrollment(client, settings, assert_redirect): assert len(response.context_data['archive']) == 0 # Check `back` url on unenroll action url = course.get_unenroll_url() + "?back=study:course_list" - assert_redirect(client.post(url, form), - reverse('study:course_list')) + response = client.post(url, data={ + "type": EnrollmentTypes.REGULAR + }) + assert_redirect(response, reverse('study:course_list')) assert set(a_ss) == set(StudentAssignment.objects .filter(student=student, assignment__course=course)) @@ -314,9 +344,10 @@ def test_reenrollment(client): assignment3 = AssignmentFactory(course=course, restricted_to=[sg]) assert StudentAssignment.objects.filter(student_id=student.pk).count() == 1 client.login(student) - form = {'course_pk': course.pk} - response = client.post(course.get_enroll_url(), form, follow=True) - assert response.status_code == 200 + response = client.post(course.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) + assert response.status_code == 302 e.refresh_from_db() assert StudentAssignment.objects.filter(student_id=student.pk).count() == 2 @@ -333,12 +364,15 @@ def test_enrollment_in_other_branch(client): student_profile_spb = StudentProfileFactory(branch__code=Branches.SPB) student_profile_nsk = StudentProfileFactory(branch__code=Branches.NSK) client.login(student_profile_spb.user) - form = {'course_pk': course_spb.pk} - response = client.post(course_spb.get_enroll_url(), form) + response = client.post(course_spb.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 302 assert Enrollment.objects.count() == 1 client.login(student_profile_nsk.user) - response = client.post(course_spb.get_enroll_url(), form) + response = client.post(course_spb.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 403 assert Enrollment.objects.count() == 1 student_profile = StudentProfileFactory(branch__code='xxx') @@ -372,16 +406,21 @@ def test_view_course_additional_branches(client): student_spb = StudentFactory(branch=course_spb.main_branch) branch_nsk = BranchFactory(code=Branches.NSK) student_nsk = StudentFactory(branch=branch_nsk) - form = {'course_pk': course_spb.pk} client.login(student_spb) - response = client.post(course_spb.get_enroll_url(), form) + response = client.post(course_spb.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 302 assert Enrollment.objects.count() == 1 client.login(student_nsk) - response = client.post(course_spb.get_enroll_url(), form) + response = client.post(course_spb.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 403 CourseBranch(course=course_spb, branch=branch_nsk).save() - response = client.post(course_spb.get_enroll_url(), form) + response = client.post(course_spb.get_enroll_url(), data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 302 assert Enrollment.objects.count() == 2 @@ -442,12 +481,28 @@ def test_enrollment_by_invitation(settings, client): "course_enroll_by_invitation", kwargs={"course_token": "WRONG_TOKEN", **course.url_kwargs}, subdomain=settings.LMS_SUBDOMAIN) - response = client.post(wrong_url, {}) + response = client.post(wrong_url) assert response.status_code == 404 - response = client.post(enroll_url, {}) + response = client.get(enroll_url) + html = response.content.decode() + assert response.status_code == 200 + assert "Как вы хотите записаться на курс?" in html + response = client.post(enroll_url, data={ + "type": EnrollmentTypes.REGULAR + }) + assert response.status_code == 302 + enrollments = Enrollment.active.filter(student=invited, course=course).all() + assert len(enrollments) == 1 + unenroll_url = course.get_unenroll_url() + client.post(unenroll_url) + assert not Enrollment.active.filter(student=invited, course=course).exists() + course_invitation.enrollment_type = InvitationEnrollmentTypes.LECTIONS_ONLY + course_invitation.save() + response = client.get(enroll_url) assert response.status_code == 302 enrollments = Enrollment.active.filter(student=invited, course=course).all() assert len(enrollments) == 1 + assert enrollments.first().type == EnrollmentTypes.LECTIONS_ONLY @pytest.mark.django_db @@ -469,10 +524,11 @@ def test_enrollment_populate_assignments(client): assignment_spb = AssignmentFactory(course=course, restricted_to=[student_group_spb]) assignment_nsk = AssignmentFactory(course=course, restricted_to=[student_group_nsk]) assert StudentAssignment.objects.count() == 0 - form = {'course_pk': course.pk} client.login(student) enroll_url = course.get_enroll_url() - response = client.post(enroll_url, form) + response = client.post(enroll_url, data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 302 assert course.enrollment_set.count() == 1 student_assignments = StudentAssignment.objects.filter(student=student) diff --git a/apps/learning/tests/test_student_groups.py b/apps/learning/tests/test_student_groups.py index 4c9eb13c7c..7ad542701f 100644 --- a/apps/learning/tests/test_student_groups.py +++ b/apps/learning/tests/test_student_groups.py @@ -18,7 +18,7 @@ from learning.models import Enrollment, StudentAssignment, StudentGroup from learning.permissions import DeleteStudentGroup, ViewStudentGroup from learning.services import EnrollmentService, StudentGroupService -from learning.settings import Branches, GradeTypes +from learning.settings import Branches, GradeTypes, EnrollmentTypes from learning.teaching.forms import StudentGroupForm from learning.teaching.utils import get_student_groups_url from learning.tests.factories import ( @@ -116,9 +116,10 @@ def test_student_group_resolving_on_enrollment(client): assert len(student_groups) == 1 student_group = student_groups[0] enroll_url = course.get_enroll_url() - form = {'course_pk': course.pk} client.login(student_profile1.user) - response = client.post(enroll_url, form) + response = client.post(enroll_url, data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 302 enrollments = Enrollment.active.filter(student_profile=student_profile1, course=course).all() @@ -127,7 +128,9 @@ def test_student_group_resolving_on_enrollment(client): assert enrollment.student_group == student_group # No permission through public interface client.login(student_profile2.user) - response = client.post(enroll_url, form) + response = client.post(enroll_url, data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 403 @@ -146,6 +149,7 @@ def test_student_group_resolving_on_enrollment_admin(client, settings): semester=current_semester) post_data = { 'course': course.pk, + 'type': EnrollmentTypes.REGULAR, 'student': student.pk, 'student_profile': student.get_student_profile(settings.SITE_ID).pk, 'grade': GradeTypes.NOT_GRADED, @@ -183,7 +187,9 @@ def test_student_group_resolving_enrollment_by_invitation(settings, client): course_invitation = CourseInvitationFactory(course=course) enroll_url = course_invitation.get_absolute_url() client.login(invited) - response = client.post(enroll_url, {}) + response = client.post(enroll_url, data={ + "type": EnrollmentTypes.REGULAR + }) assert response.status_code == 302 enrollments = Enrollment.active.filter(student=invited, course=course).all() assert len(enrollments) == 1 diff --git a/apps/learning/tests/test_views.py b/apps/learning/tests/test_views.py index 05a57b81c6..6792179dc4 100644 --- a/apps/learning/tests/test_views.py +++ b/apps/learning/tests/test_views.py @@ -18,7 +18,7 @@ from courses.tests.factories import * from learning.invitation.views import complete_student_profile, is_student_profile_valid from learning.permissions import ViewEnrollment -from learning.settings import GradeTypes, StudentStatuses +from learning.settings import GradeTypes, StudentStatuses, EnrollmentTypes from learning.tests.factories import * from users.models import StudentTypes, StudentProfile from users.services import update_student_status @@ -133,8 +133,9 @@ def test_course_detail_view_enrolled_invited_capabilities(client): response = client.get(course_invitation.invitation.get_absolute_url()) assert response.status_code == 200 assert 'Записаться' in response.content.decode('utf-8') - response = client.post(url, follow=True) - assert response.redirect_chain[-1][0] == course.get_absolute_url() + client.post(url, data={ + "type": EnrollmentTypes.REGULAR + }) enrollment = Enrollment.objects.get(invitation=course_invitation.invitation) enrollment.invitation = None diff --git a/apps/learning/views/enrollment.py b/apps/learning/views/enrollment.py index 0acbdba2f8..b953b5e9c2 100644 --- a/apps/learning/views/enrollment.py +++ b/apps/learning/views/enrollment.py @@ -1,19 +1,18 @@ from typing import Any -from vanilla import FormView, GenericView - from django.contrib import messages from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from django.views import generic +from django.views.generic import FormView from auth.mixins import PermissionRequiredMixin from core.exceptions import Redirect from core.http import HttpRequest from core.urls import reverse from courses.views.mixins import CourseURLParamsMixin -from learning.forms import CourseEnrollmentForm +from learning.forms import CourseEnrollmentForm, CourseInvitationEnrollmentForm from learning.models import CourseInvitation, Enrollment from learning.permissions import ( EnrollInCourse, EnrollInCourseByInvitation, EnrollPermissionObject, @@ -24,6 +23,7 @@ from learning.services.student_group_service import ( StudentGroupError, StudentGroupService ) +from learning.settings import EnrollmentTypes class CourseEnrollView(CourseURLParamsMixin, PermissionRequiredMixin, FormView): @@ -45,8 +45,14 @@ def has_permission(self): raise Redirect(to=self.course.get_absolute_url()) return has_perm + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['ask_enrollment_reason'] = self.course.ask_enrollment_reason + return kwargs + def form_valid(self, form): - reason_entry = form.cleaned_data["reason"].strip() + 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) try: @@ -58,6 +64,7 @@ def form_valid(self, form): try: EnrollmentService.enroll(student_profile, self.course, reason_entry=reason_entry, + type=type, student_group=student_group) msg = _("You are successfully enrolled in the course") messages.success(self.request, msg, extra_tags='timeout') @@ -108,7 +115,9 @@ def get_object(self, queryset=None): class CourseInvitationEnrollView(PermissionRequiredMixin, - CourseURLParamsMixin, GenericView): + CourseURLParamsMixin, FormView): + form_class = CourseInvitationEnrollmentForm + template_name = "learning/enrollment/enrollment_enter.html" course_invitation: CourseInvitation permission_required = EnrollInCourseByInvitation.name @@ -126,6 +135,11 @@ def setup(self, request: HttpRequest, **kwargs: Any) -> None: course=self.course)) self.course_invitation = get_object_or_404(qs) + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['enrollment_type'] = self.course_invitation.enrollment_type + return kwargs + def has_permission(self) -> bool: if super().has_permission(): return True @@ -136,24 +150,26 @@ def has_permission(self) -> bool: raise Redirect(to=invitation.get_absolute_url()) return False - def post(self, request, *args, **kwargs): + def enroll(self, request, type): invitation = self.course_invitation.invitation - student_profile = request.user.get_student_profile(self.request.site) + user = request.user + student_profile = user.get_student_profile(request.site) try: resolved_group = StudentGroupService.resolve(self.course, student_profile=student_profile, invitation=invitation) except StudentGroupError as e: - messages.error(self.request, str(e), extra_tags='timeout') + messages.error(request, str(e), extra_tags='timeout') raise Redirect(to=self.course.get_absolute_url()) try: EnrollmentService.enroll(student_profile, self.course, reason_entry='', + type=type, invitation=invitation, student_group=resolved_group) self.course_invitation.enrolled_students.add(student_profile) msg = _("You are successfully enrolled in the course") - messages.success(self.request, msg, extra_tags='timeout') + messages.success(request, msg, extra_tags='timeout') redirect_to = self.course.get_absolute_url() except AlreadyEnrolled: msg = _("You are already enrolled in the course") @@ -163,4 +179,19 @@ def post(self, request, *args, **kwargs): msg = _("No places available, sorry") messages.error(request, msg, extra_tags='timeout') redirect_to = invitation.get_absolute_url() - raise Redirect(to=redirect_to) + return HttpResponseRedirect(redirect_to) + + def form_valid(self, form): + type = form.cleaned_data["type"].strip() + return self.enroll(self.request, type) + + def get(self, request, *args, **kwargs): + if self.course_invitation.enrollment_type in EnrollmentTypes.values: + return self.enroll(request, self.course_invitation.enrollment_type) + else: + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["course"] = self.course + return context diff --git a/apps/projects/locale/ru/LC_MESSAGES/django.po b/apps/projects/locale/ru/LC_MESSAGES/django.po index fcd5a0217a..36f2afe02a 100644 --- a/apps/projects/locale/ru/LC_MESSAGES/django.po +++ b/apps/projects/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 14:53+0000\n" +"POT-Creation-Date: 2024-08-21 13:32+0000\n" "PO-Revision-Date: 2022-02-21 15:24+0000\n" "Last-Translator: Сергей Жеревчук \n" "Language-Team: LANGUAGE \n" diff --git a/apps/staff/views.py b/apps/staff/views.py index 8a340a17f0..b6dfd084b4 100644 --- a/apps/staff/views.py +++ b/apps/staff/views.py @@ -854,7 +854,7 @@ def download_csv(self, request): response['Content-Disposition'] = f'attachment; filename="{filename}"' writer = csv.writer(response) - writer.writerow(['ФИО', "Ссылка на ЛК", _("Type"), _("Telegram"), _('email address'), + writer.writerow(['ФИО', "Ссылка на ЛК", _("Branch"), _("Type"), _("Telegram"), _('email address'), _('Former field of study'), _('Field of study')]) for log in filtered_qs: @@ -863,6 +863,7 @@ def download_csv(self, request): writer.writerow([ student_profile.get_full_name(), request.build_absolute_uri(student_profile.get_absolute_url()), + student_profile.branch.name, student_profile.get_type_display(), user.telegram_username, user.email, @@ -916,8 +917,8 @@ def download_csv(self, request): response['Content-Disposition'] = f'attachment; filename="{filename}"' writer = csv.writer(response) - writer.writerow(['ФИО', "Ссылка на ЛК", _("Type"), _("Telegram"), _('email address'), _('Former status'), - _('Status')]) + writer.writerow(['ФИО', "Ссылка на ЛК", _("Branch"), _("Type"), _("Telegram"), _('email address'), + _('Former status'), _('Status')]) for log in filtered_qs: student_profile = log.student_profile @@ -925,6 +926,7 @@ def download_csv(self, request): writer.writerow([ student_profile.get_full_name(), request.build_absolute_uri(student_profile.get_absolute_url()), + student_profile.branch.name, student_profile.get_type_display(), user.telegram_username, user.email, diff --git a/apps/surveys/locale/ru/LC_MESSAGES/django.po b/apps/surveys/locale/ru/LC_MESSAGES/django.po index 8edcc3cfc4..08bb5640f4 100644 --- a/apps/surveys/locale/ru/LC_MESSAGES/django.po +++ b/apps/surveys/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 14:53+0000\n" +"POT-Creation-Date: 2024-08-21 13:32+0000\n" "PO-Revision-Date: 2019-10-31 16:30+0000\n" "Last-Translator: b' '\n" "Language-Team: LANGUAGE \n" diff --git a/apps/users/views.py b/apps/users/views.py index 92e9537faf..78b834f659 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -34,7 +34,7 @@ from learning.forms import TestimonialForm from learning.icalendar import get_icalendar_links from learning.models import Enrollment, StudentAssignment -from learning.settings import GradeTypes, StudentStatuses +from learning.settings import GradeTypes, StudentStatuses, EnrollmentTypes from users.compat import get_graduate_profile as get_graduate_profile_compat from users.models import SHADCourseRecord, YandexUserData, StudentTypes from users.thumbnails import CropboxData, get_user_thumbnail, photo_thumbnail_cropbox @@ -127,6 +127,7 @@ def get_context_data(self, **kwargs): (enrollment.grade == GradeTypes.NOT_GRADED and enrollment.course.semester == current_semester) enrollment.view_invited = enrollment.student_profile.type == StudentTypes.INVITED enrollment.view_partner = enrollment.student_profile.type == StudentTypes.PARTNER + enrollment.view_lections_only = enrollment.type == EnrollmentTypes.LECTIONS_ONLY context["enrollments"] = enrollments if is_certificates_of_participation_enabled: certificates = (CertificateOfParticipation.objects diff --git a/compscicenter_ru/locale/ru/LC_MESSAGES/django.po b/compscicenter_ru/locale/ru/LC_MESSAGES/django.po index 16632e38e3..133d33cb4b 100644 --- a/compscicenter_ru/locale/ru/LC_MESSAGES/django.po +++ b/compscicenter_ru/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 14:53+0000\n" +"POT-Creation-Date: 2024-08-21 13:32+0000\n" "PO-Revision-Date: 2020-02-03 16:52+0000\n" "Last-Translator: b' '\n" "Language-Team: LANGUAGE \n" @@ -365,7 +365,7 @@ msgctxt "menu" msgid "Библиотека" msgstr "" -#: compscicenter_ru/lms_menu.py:27 compscicenter_ru/lms_menu.py:114 +#: compscicenter_ru/lms_menu.py:27 compscicenter_ru/lms_menu.py:113 msgctxt "menu" msgid "Полезное" msgstr "" @@ -465,12 +465,12 @@ msgctxt "menu" msgid "Файлы" msgstr "" -#: compscicenter_ru/lms_menu.py:115 +#: compscicenter_ru/lms_menu.py:114 msgctxt "menu" msgid "Фейсбук" msgstr "" -#: compscicenter_ru/lms_menu.py:116 +#: compscicenter_ru/lms_menu.py:115 msgctxt "menu" msgid "Пересечения" msgstr "" diff --git a/compsciclub_ru/locale/ru/LC_MESSAGES/django.po b/compsciclub_ru/locale/ru/LC_MESSAGES/django.po index b7f1268aab..602457e405 100644 --- a/compsciclub_ru/locale/ru/LC_MESSAGES/django.po +++ b/compsciclub_ru/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 14:53+0000\n" +"POT-Creation-Date: 2024-08-21 13:32+0000\n" "PO-Revision-Date: 2020-09-09 04:43+0000\n" "Last-Translator: b' '\n" "Language-Team: LANGUAGE \n" diff --git a/lms/jinja2/lms/courses/course_detail.html b/lms/jinja2/lms/courses/course_detail.html index 6c4c7027a2..84876f10b4 100644 --- a/lms/jinja2/lms/courses/course_detail.html +++ b/lms/jinja2/lms/courses/course_detail.html @@ -233,29 +233,14 @@

{{ news.title }}{% if user.is_curator or user.is_teacher and is_actual_teach {% else %} {% set can_enroll = can_enroll_in_course(request.user, course, request.user.get_student_profile()) %} {% if can_enroll %} - {% if course.ask_enrollment_reason %} {% trans %}Enroll in the course{% endtrans %} {% if course.is_capacity_limited %}
{% trans %}Places available{% endtrans %}: {{ course.places_left }} {% endif %}
- {% else %} -
- {% csrf_token %} - -
- {% endif %} {% elif can_enroll_by_invitation %} -
- {% csrf_token %} - -
+ Записаться по приглашению {% endif %} {% endif %} {% if can_add_assignment %} diff --git a/lms/jinja2/lms/enrollment/invitation_courses.html b/lms/jinja2/lms/enrollment/invitation_courses.html index 0cee319480..18f7305cdc 100644 --- a/lms/jinja2/lms/enrollment/invitation_courses.html +++ b/lms/jinja2/lms/enrollment/invitation_courses.html @@ -31,10 +31,7 @@ {% set perm_obj = InvitationEnrollPermissionObject(course_invitation, request.user.get_student_profile()) %} {% set can_enroll_by_invitation = request.user.has_perm(EnrollInCourseByInvitation.name, perm_obj) %} {% if can_enroll_by_invitation %} -
- {% csrf_token %} - -
+ Записаться {% endif %} {% endif %} diff --git a/lms/jinja2/lms/gradebook/gradebook_form.html b/lms/jinja2/lms/gradebook/gradebook_form.html index b8559cba5b..8fc4412b32 100644 --- a/lms/jinja2/lms/gradebook/gradebook_form.html +++ b/lms/jinja2/lms/gradebook/gradebook_form.html @@ -88,6 +88,10 @@

{% endfor %} +
+

+
Всего студентов и слушателей: {{ gradebook.course.learners_count }}
+


diff --git a/lms/jinja2/lms/user_profile/_tab_account.html b/lms/jinja2/lms/user_profile/_tab_account.html index bf07520373..4151af256e 100644 --- a/lms/jinja2/lms/user_profile/_tab_account.html +++ b/lms/jinja2/lms/user_profile/_tab_account.html @@ -18,6 +18,9 @@

{{ profile_user.first_name }} прослушал{{ suffix }} и сдал{{ s {% endif %} {% if can_view_course_icons and enrollment.view_partner%} + {% endif %} + {% if can_view_course_icons and enrollment.view_lections_only%} + {% endif %} {{ enrollment.course }} diff --git a/locale/ru/LC_MESSAGES/django.po b/locale/ru/LC_MESSAGES/django.po index be1a02b47c..7fb0743c4b 100644 --- a/locale/ru/LC_MESSAGES/django.po +++ b/locale/ru/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 14:53+0000\n" -"PO-Revision-Date: 2024-08-12 14:54+0000\n" +"POT-Creation-Date: 2024-08-21 13:32+0000\n" +"PO-Revision-Date: 2024-08-21 13:32+0000\n" "Last-Translator: Имя Чернушевич \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -26,7 +26,7 @@ msgid "" "here for the last time." msgstr "" -#: apps/api/apps.py:7 apps/learning/settings.py:159 +#: apps/api/apps.py:7 apps/learning/settings.py:167 msgid "REST API" msgstr "" @@ -61,7 +61,7 @@ msgid "First %s symbols of the secret key" msgstr "" #: apps/api/models.py:23 apps/learning/gallery/models.py:43 -#: apps/learning/models.py:1004 apps/learning/models.py:1048 +#: apps/learning/models.py:1014 apps/learning/models.py:1058 #: apps/users/models.py:167 apps/users/models.py:257 msgid "User" msgstr "Имя пользователя" @@ -70,8 +70,8 @@ msgstr "Имя пользователя" msgid "Expire at" msgstr "Истекает" -#: apps/api/models.py:32 apps/learning/models.py:478 -#: apps/learning/models.py:538 +#: apps/api/models.py:32 apps/learning/models.py:483 +#: apps/learning/models.py:548 msgid "Token" msgstr "Токен" @@ -130,7 +130,7 @@ msgstr "" msgid "Update in Gerrit" msgstr "Обновление в Gerrit" -#: apps/code_reviews/models.py:11 apps/learning/models.py:808 +#: apps/code_reviews/models.py:11 apps/learning/models.py:818 msgid "Student Assignment" msgstr "Персональное задание" @@ -168,7 +168,7 @@ msgstr "" msgid "Enabled" msgstr "" -#: apps/core/db/models.py:17 apps/learning/models.py:813 +#: apps/core/db/models.py:17 apps/learning/models.py:823 #: apps/users/models.py:273 msgid "Changed by" msgstr "Кем изменено" @@ -260,7 +260,7 @@ msgstr "Сайт" #: apps/core/models.py:279 apps/core/models.py:334 apps/courses/forms.py:171 #: apps/courses/models.py:70 apps/courses/models.py:306 -#: apps/courses/models.py:881 apps/learning/models.py:1086 +#: apps/courses/models.py:881 apps/learning/models.py:1096 #: apps/templates/learning/event_detail.html:29 #: lms/jinja2/lms/courses/course_class_detail.html:53 msgid "Description" @@ -273,17 +273,19 @@ msgstr "Общее описание, правила обучения" #: apps/core/models.py:291 apps/courses/models.py:61 #: apps/courses/models.py:235 apps/courses/models.py:649 #: apps/learning/admin.py:68 apps/learning/gradebook/views.py:231 -#: apps/learning/models.py:78 apps/learning/models.py:1076 -#: apps/library/models.py:66 apps/staff/filters.py:129 -#: apps/study_programs/models.py:61 apps/users/models.py:177 -#: apps/users/models.py:359 apps/users/models.py:814 +#: apps/learning/models.py:78 apps/learning/models.py:1086 +#: apps/library/models.py:66 apps/staff/filters.py:130 +#: apps/staff/filters.py:205 apps/study_programs/models.py:61 +#: apps/users/models.py:177 apps/users/models.py:359 apps/users/models.py:814 #: lms/jinja2/lms/courses/meta_detail.html:27 +#: lms/jinja2/lms/staff/academic_discipline_log.html:19 +#: lms/jinja2/lms/staff/status_log.html:19 #: lms/jinja2/lms/user_profile/_tab_student_profiles.html:44 msgid "Branch" msgstr "Отделение" #: apps/core/models.py:292 apps/courses/models.py:244 -#: apps/learning/admin.py:339 apps/learning/models.py:545 +#: apps/learning/admin.py:339 apps/learning/models.py:555 msgid "Branches" msgstr "Отделения" @@ -326,7 +328,7 @@ msgstr "Адреса" #: apps/core/models.py:362 apps/courses/forms.py:508 apps/courses/models.py:65 #: apps/faq/models.py:9 apps/learning/gallery/models.py:65 -#: apps/learning/models.py:64 apps/learning/models.py:537 +#: apps/learning/models.py:64 apps/learning/models.py:547 #: apps/universities/models.py:6 apps/universities/models.py:21 #: apps/universities/models.py:43 apps/universities/models.py:67 msgid "Name" @@ -415,7 +417,7 @@ msgid "Assignment|deadline" msgstr "Срок сдачи" #: apps/courses/apps.py:7 apps/courses/models.py:211 -#: apps/learning/models.py:557 +#: apps/learning/models.py:567 msgid "Courses" msgstr "Курсы" @@ -508,7 +510,7 @@ msgid "Online Submission" msgstr "Через сайт" #: apps/courses/constants.py:59 apps/grading/constants.py:162 -#: apps/learning/settings.py:160 +#: apps/learning/settings.py:168 msgid "Yandex.Contest" msgstr "Яндекс.Контест" @@ -520,7 +522,7 @@ msgstr "Внешний сервис" msgid "Code Review Submission" msgstr "Задача с код-ревью" -#: apps/courses/constants.py:62 apps/learning/models.py:619 +#: apps/courses/constants.py:62 apps/learning/models.py:629 msgid "Penalty" msgstr "Штраф" @@ -573,7 +575,7 @@ msgid "To all groups" msgstr "Всем группам" #: apps/courses/forms.py:67 apps/courses/models.py:188 -#: apps/users/models.py:1110 apps/users/models.py:1127 +#: apps/users/models.py:1118 apps/users/models.py:1135 msgid "Course|name" msgstr "Название" @@ -597,13 +599,14 @@ msgid "Venue" msgstr "Место проведения" #: apps/courses/forms.py:165 apps/courses/models.py:871 -#: apps/notifications/models.py:92 apps/staff/filters.py:134 +#: apps/notifications/models.py:92 apps/staff/filters.py:135 +#: apps/staff/filters.py:210 apps/staff/views.py:857 apps/staff/views.py:919 #: apps/users/models.py:794 msgid "Type" msgstr "Тип" #: apps/courses/forms.py:168 apps/courses/models.py:879 -#: apps/learning/models.py:1084 +#: apps/learning/models.py:1094 msgid "CourseClass|Name" msgstr "Название" @@ -627,7 +630,7 @@ msgid "CourseClass|Other materials" msgstr "Другие материалы" #: apps/courses/forms.py:190 apps/courses/models.py:874 -#: apps/learning/models.py:1089 +#: apps/learning/models.py:1099 msgid "Date" msgstr "Дата" @@ -636,7 +639,7 @@ msgid "Format: dd.mm.yyyy" msgstr "Формат: дд.мм.гггг" #: apps/courses/forms.py:195 apps/courses/models.py:875 -#: apps/learning/models.py:1090 +#: apps/learning/models.py:1100 msgid "Starts at" msgstr "Начало" @@ -645,7 +648,7 @@ msgid "Format: hh:mm" msgstr "Формат: чч:мм" #: apps/courses/forms.py:199 apps/courses/models.py:876 -#: apps/learning/models.py:1091 +#: apps/learning/models.py:1101 msgid "Ends at" msgstr "Конец" @@ -731,7 +734,7 @@ msgstr "" "Сколько, по вашему мнению, студенту потребуется времени, чтобы выполнить " "задание?" -#: apps/courses/forms.py:336 apps/learning/forms.py:150 +#: apps/courses/forms.py:336 apps/learning/forms.py:151 msgid "hours:minutes" msgstr "часы:минуты" @@ -775,7 +778,7 @@ msgid "Teacher" msgstr "Преподаватель" #: apps/courses/forms.py:556 apps/learning/models.py:226 -#: apps/users/models.py:1128 +#: apps/users/models.py:1136 msgid "Teachers" msgstr "Преподаватели" @@ -821,8 +824,8 @@ msgstr "Системное поле. Используется для сорти #: apps/courses/models.py:115 apps/courses/models.py:286 #: apps/learning/gallery/models.py:83 apps/learning/models.py:285 -#: apps/learning/models.py:541 apps/users/admin.py:181 -#: apps/users/models.py:1131 lms/jinja2/lms/courses/meta_detail.html:26 +#: apps/learning/models.py:551 apps/users/models.py:1139 +#: lms/jinja2/lms/courses/meta_detail.html:26 #: lms/jinja2/lms/courses/teacher_detail.html:32 #: lms/jinja2/lms/study/course_list.html:115 #: lms/jinja2/lms/teaching/course_list.html:11 @@ -874,8 +877,8 @@ msgid "Manual" msgstr "Ручной" #: apps/courses/models.py:236 apps/learning/models.py:85 -#: apps/learning/models.py:153 apps/learning/models.py:387 -#: apps/learning/models.py:561 apps/users/models.py:911 +#: apps/learning/models.py:153 apps/learning/models.py:392 +#: apps/learning/models.py:571 apps/users/models.py:911 #: lms/jinja2/lms/user_profile/_tab_student_profiles.html:186 msgid "Invitation" msgstr "Приглашение" @@ -904,11 +907,11 @@ msgstr "Полный семестр" msgid "CourseOffering|grading_type" msgstr "Система оценивания" -#: apps/courses/models.py:266 apps/learning/models.py:480 +#: apps/courses/models.py:266 apps/learning/models.py:485 msgid "CourseOffering|capacity" msgstr "Максимальное кол-во слушателей" -#: apps/courses/models.py:268 apps/learning/models.py:482 +#: apps/courses/models.py:268 apps/learning/models.py:487 msgid "0 - unlimited" msgstr "0 - без ограничений" @@ -984,7 +987,7 @@ msgstr "" "Если оставить пустым, то ссылка будет генерироваться на основе последнего " "активного опроса." -#: apps/courses/models.py:323 apps/users/models.py:1111 +#: apps/courses/models.py:323 apps/users/models.py:1119 msgid "Online Course URL" msgstr "URL онлайн-курса" @@ -1067,7 +1070,7 @@ msgstr "Публичные приложения" #: apps/courses/models.py:400 apps/courses/models.py:1126 #: apps/learning/gallery/models.py:49 apps/learning/models.py:68 -#: apps/learning/models.py:359 apps/learning/models.py:476 +#: apps/learning/models.py:359 apps/learning/models.py:481 #: lms/jinja2/lms/user_profile/_tab_assignments.html:10 msgid "Course offering" msgstr "Прочтение курса" @@ -1107,7 +1110,7 @@ msgid "Course Teachers" msgstr "Преподаватели курса" #: apps/courses/models.py:757 apps/courses/models.py:786 -#: apps/learning/models.py:455 apps/learning/models.py:871 +#: apps/learning/models.py:460 apps/learning/models.py:881 #: apps/users/models.py:1030 msgid "Author" msgstr "Автор" @@ -1140,7 +1143,7 @@ msgstr "Новость" msgid "Course news-plural" msgstr "Новости" -#: apps/courses/models.py:868 apps/learning/models.py:1081 +#: apps/courses/models.py:868 apps/learning/models.py:1091 msgid "CourseClass|Venue" msgstr "Место проведения" @@ -1300,7 +1303,7 @@ msgstr "FAQ" msgid "Sort order" msgstr "Сортировка" -#: apps/faq/models.py:16 apps/learning/models.py:549 +#: apps/faq/models.py:16 apps/learning/models.py:559 msgid "Category" msgstr "Категория" @@ -1323,8 +1326,8 @@ msgstr "Вопросы" #: apps/grading/admin.py:29 apps/learning/admin.py:310 #: apps/learning/models.py:351 apps/learning/roles.py:92 #: apps/users/constants.py:36 apps/users/models.py:809 -#: apps/users/models.py:1025 apps/users/models.py:1108 -#: apps/users/models.py:1125 apps/users/models.py:1157 +#: apps/users/models.py:1025 apps/users/models.py:1116 +#: apps/users/models.py:1133 apps/users/models.py:1165 #: lms/jinja2/lms/projects/project_report.html:261 #: lms/jinja2/lms/study/student_assignment_detail.html:138 #: lms/jinja2/lms/teaching/assignment_detail.html:75 @@ -1408,13 +1411,14 @@ msgstr "Конфигурации проверяющей системы" msgid "Please check that the following settings are provided: {}" msgstr "Убедитесь, что указаны следующие настройки: {}" -#: apps/grading/models.py:89 apps/learning/models.py:968 +#: apps/grading/models.py:89 apps/learning/models.py:978 msgid "Assignment Submission" msgstr "Посылка к заданию" -#: apps/grading/models.py:93 apps/learning/forms.py:76 -#: apps/learning/study/forms.py:69 apps/users/models.py:222 -#: apps/users/models.py:819 apps/users/models.py:1068 +#: apps/grading/models.py:93 apps/learning/forms.py:77 +#: apps/learning/study/forms.py:69 apps/staff/filters.py:189 +#: apps/staff/views.py:920 apps/users/models.py:222 apps/users/models.py:819 +#: apps/users/models.py:1068 lms/jinja2/lms/staff/status_log.html:21 #: lms/jinja2/lms/staff/student_search.html:69 #: lms/jinja2/lms/user_profile/_tab_student_profiles.html:52 #: lms/jinja2/lms/user_profile/_tab_student_profiles.html:75 @@ -1496,7 +1500,7 @@ msgstr "Задано" msgid "Asssignment|name" msgstr "Название" -#: apps/learning/admin.py:145 apps/learning/models.py:367 +#: apps/learning/admin.py:145 apps/learning/models.py:372 msgid "Enrollment|grade changed" msgstr "Оценка изменена" @@ -1526,68 +1530,72 @@ msgstr "Невалидный преподаватель курса %s" msgid "Learning" msgstr "Обучение" -#: apps/learning/forms.py:61 apps/learning/models.py:844 +#: apps/learning/forms.py:62 apps/learning/models.py:854 #: apps/users/models.py:894 #: lms/jinja2/lms/user_profile/_tab_student_profiles.html:195 msgid "Comment" msgstr "Комментарий" -#: apps/learning/forms.py:125 +#: apps/learning/forms.py:126 msgid "Please select a valid status" msgstr "Пожалуйста, выберите разрешенный статус." -#: apps/learning/forms.py:130 +#: apps/learning/forms.py:131 msgid "Form is empty." msgstr "Недостаточно данных." -#: apps/learning/forms.py:146 +#: apps/learning/forms.py:147 #: lms/jinja2/lms/teaching/student_assignment_detail.html:196 msgid "Time Spent on Assignment" msgstr "Время, затраченное на выполнение задания" -#: apps/learning/forms.py:151 +#: apps/learning/forms.py:152 msgid "" "Requires the full format including minutes. Do not include the time of " "previous submissions." msgstr "" "Укажите полный формат HH:MM. Время предыдущих посылок учитывать не нужно." -#: apps/learning/forms.py:179 apps/learning/forms.py:228 +#: apps/learning/forms.py:180 apps/learning/forms.py:229 msgid "Send Solution" msgstr "Отправить решение" -#: apps/learning/forms.py:189 +#: apps/learning/forms.py:190 #: apps/learning/services/personal_assignment_service.py:167 #: apps/learning/study/forms.py:55 msgid "Either text or file should be non-empty" msgstr "Требуется текст или приложенный файл" -#: apps/learning/forms.py:196 +#: apps/learning/forms.py:197 msgid "File name is not provided" msgstr "Не указано имя файла" -#: apps/learning/forms.py:200 +#: apps/learning/forms.py:201 #, python-format msgid "`%(value)s` file name has no extension" msgstr "У имени файла `%(value)s` не найдено расширение" -#: apps/learning/forms.py:207 +#: apps/learning/forms.py:208 msgid "Compiler" msgstr "Компилятор" -#: apps/learning/forms.py:210 +#: apps/learning/forms.py:211 msgid "Solution file" msgstr "Файл решения" -#: apps/learning/forms.py:237 +#: apps/learning/forms.py:238 msgid "File should be non-empty" msgstr "Должен быть прикреплен файл" -#: apps/learning/forms.py:260 +#: apps/learning/forms.py:261 msgid "Text should be non-empty" msgstr "Требуется текст" -#: apps/learning/forms.py:288 +#: apps/learning/forms.py:289 +msgid "Как вы хотите записаться на курс?" +msgstr "" + +#: apps/learning/forms.py:295 msgid "Почему вы выбрали этот курс?" msgstr "" @@ -1771,7 +1779,7 @@ msgid "Enrollment key" msgstr "Код записи на курс" #: apps/learning/models.py:92 apps/learning/models.py:166 -#: apps/learning/models.py:380 +#: apps/learning/models.py:385 msgid "Student Group" msgstr "Студенческая группа" @@ -1792,7 +1800,7 @@ msgstr "" msgid "A student group with the same name already exists in the course" msgstr "" -#: apps/learning/models.py:171 apps/learning/models.py:628 +#: apps/learning/models.py:171 apps/learning/models.py:638 #: lms/jinja2/lms/teaching/student_assignment_detail.html:206 msgid "Assignee" msgstr "Проверяющий" @@ -1860,330 +1868,335 @@ msgstr "" "Последний день записи на курсы должен быть >= предполагаемого начала " "семестра ({})" -#: apps/learning/models.py:355 apps/learning/models.py:1136 +#: apps/learning/models.py:355 apps/learning/models.py:1146 #: apps/users/models.py:921 #: lms/jinja2/lms/staff/academic_discipline_log.html:18 +#: lms/jinja2/lms/staff/status_log.html:18 msgid "Student Profile" msgstr "Профиль студента" -#: apps/learning/models.py:362 apps/learning/models.py:446 -#: apps/users/models.py:1135 +#: apps/learning/models.py:362 apps/learning/models.py:489 +msgid "Enrollment|type" +msgstr "Тип" + +#: apps/learning/models.py:367 apps/learning/models.py:451 +#: apps/users/models.py:1143 msgid "Enrollment|grade" msgstr "Оценка" -#: apps/learning/models.py:370 +#: apps/learning/models.py:375 msgid "The student left the course" msgstr "Студент покинул курс" -#: apps/learning/models.py:373 +#: apps/learning/models.py:378 msgid "Entry reason" msgstr "Причина поступления" -#: apps/learning/models.py:376 +#: apps/learning/models.py:381 msgid "Leave reason" msgstr "Причина ухода" -#: apps/learning/models.py:397 apps/learning/models.py:450 +#: apps/learning/models.py:402 apps/learning/models.py:455 msgid "Enrollment" msgstr "Запись на курс" -#: apps/learning/models.py:398 +#: apps/learning/models.py:403 msgid "Enrollments" msgstr "Записи на курсы" -#: apps/learning/models.py:413 +#: apps/learning/models.py:418 msgid "Student profile does not match selected user" msgstr "" -#: apps/learning/models.py:415 +#: apps/learning/models.py:420 msgid "" "Student group must refer to one of the student groups of the selected course" msgstr "Значение должно указывать на одну из групп выбранного курса." -#: apps/learning/models.py:432 +#: apps/learning/models.py:437 msgid "Satisfactory" msgstr "Удовлетворительно" -#: apps/learning/models.py:442 apps/users/models.py:1021 +#: apps/learning/models.py:447 apps/users/models.py:1021 #: lms/jinja2/lms/user_profile/_tab_student_profiles.html:76 #: lms/jinja2/lms/user_profile/_tab_student_profiles.html:107 msgid "Entry Added" msgstr "Дата изменения" -#: apps/learning/models.py:458 apps/learning/models.py:824 +#: apps/learning/models.py:463 apps/learning/models.py:834 msgid "Source" msgstr "Источник" -#: apps/learning/models.py:472 +#: apps/learning/models.py:477 msgid "Course Invitation" msgstr "Приглашение на курс" -#: apps/learning/models.py:485 +#: apps/learning/models.py:495 #, fuzzy #| msgid "Enrollments" msgid "Enrolled students" msgstr "Записи на курсы" -#: apps/learning/models.py:487 +#: apps/learning/models.py:497 msgid "Students who took advantage of the invitation" msgstr "" -#: apps/learning/models.py:492 +#: apps/learning/models.py:502 msgid "Enrollment Invitation" msgstr "Приглашение на курс" -#: apps/learning/models.py:493 +#: apps/learning/models.py:503 msgid "Enrollment Invitations" msgstr "Приглашения на курсы" -#: apps/learning/models.py:506 +#: apps/learning/models.py:516 msgid "Course semester should match invitation semester" msgstr "Семестр курса не совпадает с семестром приглашения" -#: apps/learning/models.py:562 +#: apps/learning/models.py:572 msgid "Invitations" msgstr "Приглашения" -#: apps/learning/models.py:600 +#: apps/learning/models.py:610 msgid "StudentAssignment|assignment" msgstr "Задание" -#: apps/learning/models.py:604 +#: apps/learning/models.py:614 msgid "StudentAssignment|student" msgstr "Студент" -#: apps/learning/models.py:607 +#: apps/learning/models.py:617 msgid "StudentAssignment|Status" msgstr "Статус" -#: apps/learning/models.py:612 lms/jinja2/lms/study/course_list.html:114 +#: apps/learning/models.py:622 lms/jinja2/lms/study/course_list.html:114 #: lms/jinja2/lms/study/student_assignment_detail.html:122 #: lms/jinja2/lms/teaching/student_assignment_detail.html:181 #: lms/jinja2/lms/user_profile/_tab_assignments.html:12 msgid "Grade" msgstr "Оценка" -#: apps/learning/models.py:624 +#: apps/learning/models.py:634 msgid "Assignment|grade changed" msgstr "Оценка изменена" -#: apps/learning/models.py:640 +#: apps/learning/models.py:650 msgid "Student Assignment Watchers" msgstr "Персональное задание" -#: apps/learning/models.py:645 +#: apps/learning/models.py:655 msgid "Execution Time" msgstr "Время выполнения" -#: apps/learning/models.py:648 +#: apps/learning/models.py:658 msgid "The time spent by the student executing this task" msgstr "Время, потраченное на выполнение задания" -#: apps/learning/models.py:661 +#: apps/learning/models.py:671 msgid "Personal Assignment" msgstr "Персональное задание" -#: apps/learning/models.py:662 +#: apps/learning/models.py:672 msgid "Personal Assignments" msgstr "Персональные задания" -#: apps/learning/models.py:667 +#: apps/learning/models.py:677 #, python-brace-format msgid "Grade can't be larger than maximum one ({0})" msgstr "Оценка не может быть больше максимальной ({0})" -#: apps/learning/models.py:816 +#: apps/learning/models.py:826 #, fuzzy #| msgid "Previous Page" msgid "Previous Score" msgstr "Предыдущая страница" -#: apps/learning/models.py:820 +#: apps/learning/models.py:830 msgid "New Score" msgstr "" -#: apps/learning/models.py:829 +#: apps/learning/models.py:839 msgid "Assignment score audit log" msgstr "Лог изменения оценки" -#: apps/learning/models.py:845 +#: apps/learning/models.py:855 msgid "Solution" msgstr "Решение" -#: apps/learning/models.py:853 +#: apps/learning/models.py:863 msgid "AssignmentComment|student_assignment" msgstr "Выполнение задания" -#: apps/learning/models.py:856 +#: apps/learning/models.py:866 msgid "AssignmentComment|Type" msgstr "Тип" -#: apps/learning/models.py:861 +#: apps/learning/models.py:871 msgid "Solution Execution Time" msgstr "Затраченное время" -#: apps/learning/models.py:863 +#: apps/learning/models.py:873 msgid "The time spent by the student executing this submission" msgstr "Время, затраченное студентом на выполнение посылки" -#: apps/learning/models.py:864 +#: apps/learning/models.py:874 msgid "Published" msgstr "Опубликовано" -#: apps/learning/models.py:866 +#: apps/learning/models.py:876 msgid "AssignmentComment|text" msgstr "Текст" -#: apps/learning/models.py:867 apps/users/models.py:366 +#: apps/learning/models.py:877 apps/users/models.py:366 #: apps/users/models.py:398 apps/users/models.py:405 msgid "LaTeX+Markdown is enabled" msgstr "доступны LaTeX и Markdown" -#: apps/learning/models.py:874 +#: apps/learning/models.py:884 msgid "Attached File" msgstr "Приложенный файл" -#: apps/learning/models.py:890 +#: apps/learning/models.py:900 msgid "Assignment-comment" msgstr "Комментарий к заданию" -#: apps/learning/models.py:891 +#: apps/learning/models.py:901 msgid "Assignment-comments" msgstr "Комментарии к заданиям" -#: apps/learning/models.py:977 +#: apps/learning/models.py:987 msgid "Assignment Submission Attachment" msgstr "Приложенный файл" -#: apps/learning/models.py:978 +#: apps/learning/models.py:988 msgid "Assignment Submission Attachments" msgstr "Приложенные файлы" -#: apps/learning/models.py:1008 +#: apps/learning/models.py:1018 msgid "student_assignment" msgstr "Выполнение задания" -#: apps/learning/models.py:1010 +#: apps/learning/models.py:1020 msgid "About passed assignment" msgstr "О сданном задании" -#: apps/learning/models.py:1012 +#: apps/learning/models.py:1022 msgid "About created assignment" msgstr "О сданном задании" -#: apps/learning/models.py:1014 +#: apps/learning/models.py:1024 msgid "About change of deadline" msgstr "О смене времени сдачи" -#: apps/learning/models.py:1016 apps/learning/models.py:1054 +#: apps/learning/models.py:1026 apps/learning/models.py:1064 msgid "Unread" msgstr "Не прочтено" -#: apps/learning/models.py:1018 apps/learning/models.py:1056 +#: apps/learning/models.py:1028 apps/learning/models.py:1066 msgid "User is notified" msgstr "Пользователь оповещён" -#: apps/learning/models.py:1026 +#: apps/learning/models.py:1036 msgid "Assignment notification" msgstr "Уведомление о заданиях" -#: apps/learning/models.py:1027 +#: apps/learning/models.py:1037 msgid "Assignment notifications" msgstr "Уведомления о заданиях" -#: apps/learning/models.py:1031 +#: apps/learning/models.py:1041 msgid "Only teachers can receive notifications of passed assignments" msgstr "Только преподаватели могут получать оповещения о сданных заданиях" -#: apps/learning/models.py:1052 +#: apps/learning/models.py:1062 msgid "Course offering news" msgstr "Новости прочтений" -#: apps/learning/models.py:1064 +#: apps/learning/models.py:1074 msgid "Course offering news notification" msgstr "Оповещение о новостях прочтений" -#: apps/learning/models.py:1065 +#: apps/learning/models.py:1075 msgid "Course offering news notifications" msgstr "Оповещения о новостях прочтений" -#: apps/learning/models.py:1097 +#: apps/learning/models.py:1107 msgid "Non-course event" msgstr "Событие" -#: apps/learning/models.py:1098 +#: apps/learning/models.py:1108 msgid "Non-course events" msgstr "События" -#: apps/learning/models.py:1107 +#: apps/learning/models.py:1117 msgid "Event should end after it's start" msgstr "Конец события должен быть позже начала" -#: apps/learning/models.py:1132 +#: apps/learning/models.py:1142 msgid "Visibility" msgstr "Видимость" -#: apps/learning/models.py:1140 +#: apps/learning/models.py:1150 msgid "Graduated on" msgstr "Дата выпуска" -#: apps/learning/models.py:1141 +#: apps/learning/models.py:1151 msgid "Graduation ceremony date" msgstr "Дата выпускного считается датой выпуска студента" -#: apps/learning/models.py:1144 apps/users/models.py:891 +#: apps/learning/models.py:1154 apps/users/models.py:891 msgid "Fields of study" msgstr "Направления обучения" -#: apps/learning/models.py:1145 +#: apps/learning/models.py:1155 msgid "Academic disciplines that the student graduated from" msgstr "Направления, с которых выпустился студент." -#: apps/learning/models.py:1148 +#: apps/learning/models.py:1158 msgid "Graduation Year" msgstr "Год выпуска" -#: apps/learning/models.py:1149 +#: apps/learning/models.py:1159 msgid "Helps filtering by year" msgstr "" -#: apps/learning/models.py:1152 +#: apps/learning/models.py:1162 msgid "Photo" msgstr "Фото" -#: apps/learning/models.py:1157 +#: apps/learning/models.py:1167 msgid "Testimonial" msgstr "Отзыв" -#: apps/learning/models.py:1158 +#: apps/learning/models.py:1168 msgid "Testimonial about Computer Science Center" msgstr "Отзыв о CS центре" -#: apps/learning/models.py:1161 +#: apps/learning/models.py:1171 msgid "Details" msgstr "Детали" -#: apps/learning/models.py:1166 apps/users/models.py:875 +#: apps/learning/models.py:1176 apps/users/models.py:875 msgid "Diploma Number" msgstr "Номер диплома" -#: apps/learning/models.py:1171 +#: apps/learning/models.py:1181 msgid "Registration Number" msgstr "Регистрационный номер" -#: apps/learning/models.py:1172 +#: apps/learning/models.py:1182 msgid "Registration number in the registry of education" msgstr "Номер в федеральном реестре (ФРДО)" -#: apps/learning/models.py:1177 apps/users/models.py:881 +#: apps/learning/models.py:1187 apps/users/models.py:881 msgid "Diploma Issued on" msgstr "Дата выдачи диплома" -#: apps/learning/models.py:1185 +#: apps/learning/models.py:1195 msgid "Graduate Profile" msgstr "Профиль выпускника" -#: apps/learning/models.py:1186 +#: apps/learning/models.py:1196 msgid "Graduate Profiles" msgstr "Профили выпускников" @@ -2290,126 +2303,147 @@ msgid "Other" msgstr "Другое" #: apps/learning/settings.py:38 +msgid "EnrollmentTypes|Regular" +msgstr "Сдавать" + +#: apps/learning/settings.py:39 +msgid "EnrollmentTypes|Lections" +msgstr "Только слушать" + +#: apps/learning/settings.py:42 +msgid "InvitationEnrollmentTypes|Regular" +msgstr "Сдавать" + +#: apps/learning/settings.py:43 +msgid "InvitationEnrollmentTypes|Lections" +msgstr "Только слушать" + +#: apps/learning/settings.py:44 +#| msgid "InvitationEnrollmentTypes|Both" +msgid "InvitationEnrollmentTypes|Any" +msgstr "На выбор" + +#: apps/learning/settings.py:47 msgid "Invitation|University" msgstr "ВУЗы" -#: apps/learning/settings.py:39 +#: apps/learning/settings.py:48 msgid "Invitation|Staff" msgstr "Сотрудники" -#: apps/learning/settings.py:40 +#: apps/learning/settings.py:49 msgid "Invitation|Teacher" msgstr "Преподаватели" -#: apps/learning/settings.py:41 +#: apps/learning/settings.py:50 msgid "Invitation|Guest" msgstr "Гости" -#: apps/learning/settings.py:42 +#: apps/learning/settings.py:51 msgid "Invitation|Aacdemic" msgstr "Академ" -#: apps/learning/settings.py:43 +#: apps/learning/settings.py:52 msgid "Invitation|Graduate" msgstr "Выпускники" -#: apps/learning/settings.py:44 +#: apps/learning/settings.py:53 msgid "Invitation|Applicant" msgstr "Абитуриенты" -#: apps/learning/settings.py:47 +#: apps/learning/settings.py:56 msgid "StudentInfo|Expelled" msgstr "Отчислен" -#: apps/learning/settings.py:48 +#: apps/learning/settings.py:57 msgid "StudentStatus|Academic leave" msgstr "Академический отпуск" -#: apps/learning/settings.py:49 +#: apps/learning/settings.py:58 msgid "Second academic leave" msgstr "Второй академический отпуск" -#: apps/learning/settings.py:50 +#: apps/learning/settings.py:59 msgid "StudentInfo|Reinstalled" msgstr "Восстановлен" -#: apps/learning/settings.py:51 +#: apps/learning/settings.py:60 msgid "StudentInfo|Will graduate" msgstr "Будет выпускаться" -#: apps/learning/settings.py:52 +#: apps/learning/settings.py:61 msgid "StudentInfo|Graduate" msgstr "Выпускник" -#: apps/learning/settings.py:66 +#: apps/learning/settings.py:75 msgid "Default" msgstr "По умолчанию" -#: apps/learning/settings.py:67 +#: apps/learning/settings.py:76 msgid "Pass/Fail" msgstr "Зачет/Незачет" -#: apps/learning/settings.py:68 +#: apps/learning/settings.py:77 msgid "Pass/Fail + Excellent" msgstr "Зачет/Незачет + Отлично" -#: apps/learning/settings.py:69 +#: apps/learning/settings.py:78 msgid "10-point scale" msgstr "10-балльная шкала" -#: apps/learning/settings.py:76 apps/users/constants.py:53 +#: apps/learning/settings.py:84 apps/users/constants.py:53 msgid "Not graded" msgstr "Без оценки" -#: apps/learning/settings.py:78 +#: apps/learning/settings.py:86 msgid "Without Grade" msgstr "Не оценивается" -#: apps/learning/settings.py:80 +#: apps/learning/settings.py:88 msgid "Enrollment|Unsatisfactory" msgstr "Незачёт" -#: apps/learning/settings.py:83 +#: apps/learning/settings.py:91 msgid "Enrollment|Pass" msgstr "Зачёт" -#: apps/learning/settings.py:86 apps/users/constants.py:56 +#: apps/learning/settings.py:94 apps/users/constants.py:56 msgid "Good" msgstr "Хорошо" -#: apps/learning/settings.py:87 apps/users/constants.py:57 +#: apps/learning/settings.py:95 apps/users/constants.py:57 msgid "Excellent" msgstr "Отлично" -#: apps/learning/settings.py:90 +#: apps/learning/settings.py:98 msgid "Enrollment|Re-credit" msgstr "Перезачтено" -#: apps/learning/settings.py:151 apps/learning/settings.py:166 +#: apps/learning/settings.py:159 apps/learning/settings.py:174 msgid "Gradebook" msgstr "Ведомость" -#: apps/learning/settings.py:152 apps/learning/settings.py:161 +#: apps/learning/settings.py:160 apps/learning/settings.py:169 msgid "Imported from CSV by Yandex.Login" msgstr "Импорт из CSV-файла по Яндекс.Логину" -#: apps/learning/settings.py:153 apps/learning/settings.py:162 +#: apps/learning/settings.py:161 apps/learning/settings.py:170 msgid "Imported from CSV by stepik.org ID" msgstr "Импорт из CSV-файла по stepik.org ID" -#: apps/learning/settings.py:154 apps/learning/settings.py:163 +#: apps/learning/settings.py:162 apps/learning/settings.py:171 msgid "Imported from CSV by LMS Student ID" msgstr "Импорт из CSV-файла по идентификатору студента" -#: apps/learning/settings.py:155 apps/learning/settings.py:164 +#: apps/learning/settings.py:163 apps/learning/settings.py:172 msgid "Admin Panel" msgstr "Админ-панель" -#: apps/learning/settings.py:165 +#: apps/learning/settings.py:173 msgid "Form on Assignment Detail Page" msgstr "Страница задания студента" -#: apps/learning/settings.py:167 +#: apps/learning/settings.py:175 msgid "Gerrit Webhook" msgstr "Геррит" @@ -2472,16 +2506,16 @@ msgstr "Записаться" msgid "{} hrs {:02d} min" msgstr "{} ч. {:02d} мин." -#: apps/learning/views/enrollment.py:43 apps/learning/views/enrollment.py:68 -#: apps/learning/views/enrollment.py:133 apps/learning/views/enrollment.py:163 +#: apps/learning/views/enrollment.py:43 apps/learning/views/enrollment.py:70 +#: apps/learning/views/enrollment.py:135 apps/learning/views/enrollment.py:165 msgid "No places available, sorry" msgstr "Извините, свободных мест не осталось." -#: apps/learning/views/enrollment.py:62 apps/learning/views/enrollment.py:155 +#: apps/learning/views/enrollment.py:64 apps/learning/views/enrollment.py:157 msgid "You are successfully enrolled in the course" msgstr "Вы успешно записались на курс" -#: apps/learning/views/enrollment.py:65 apps/learning/views/enrollment.py:159 +#: apps/learning/views/enrollment.py:67 apps/learning/views/enrollment.py:161 msgid "You are already enrolled in the course" msgstr "Вы уже записаны на этот курс" @@ -2592,35 +2626,47 @@ msgstr "Типы" msgid "Notification" msgstr "Уведомление" -#: apps/staff/filters.py:59 apps/staff/filters.py:101 -#: apps/staff/filters.py:156 +#: apps/staff/filters.py:60 apps/staff/filters.py:102 +#: apps/staff/filters.py:177 apps/staff/filters.py:260 msgid "Filter" msgstr "" -#: apps/staff/filters.py:115 apps/users/models.py:1093 -#: lms/jinja2/lms/staff/academic_discipline_log.html:20 +#: apps/staff/filters.py:116 apps/staff/views.py:858 apps/users/models.py:1100 +#: lms/jinja2/lms/staff/academic_discipline_log.html:21 #: lms/jinja2/lms/user_profile/_tab_student_profiles.html:106 msgid "Field of study" msgstr "Направление обучения" -#: apps/staff/filters.py:120 apps/users/models.py:1086 -#: lms/jinja2/lms/staff/academic_discipline_log.html:19 +#: apps/staff/filters.py:121 apps/staff/views.py:858 apps/users/models.py:1093 +#: lms/jinja2/lms/staff/academic_discipline_log.html:20 msgid "Former field of study" msgstr "Прошлое направление обучения" -#: apps/staff/filters.py:125 apps/users/models.py:1033 +#: apps/staff/filters.py:126 apps/staff/filters.py:201 +#: apps/users/models.py:1033 +#: lms/jinja2/lms/staff/academic_discipline_log.html:22 +#: lms/jinja2/lms/staff/status_log.html:22 msgid "Is processed" msgstr "Обработан ли" -#: apps/staff/filters.py:163 +#: apps/staff/filters.py:178 apps/staff/filters.py:261 msgid "Download" msgstr "Скачать" -#: apps/staff/filters.py:164 -#| msgid "Is processed" +#: apps/staff/filters.py:179 apps/staff/filters.py:262 msgid "Mark processed" msgstr "Обработать" +#: apps/staff/filters.py:187 apps/users/models.py:982 +#: apps/users/models.py:1082 apps/users/models.py:1088 +msgid "Studying" +msgstr "Учится" + +#: apps/staff/filters.py:195 apps/staff/views.py:919 apps/users/models.py:1063 +#: lms/jinja2/lms/staff/status_log.html:20 +msgid "Former status" +msgstr "Прошлый статус" + #: apps/staff/forms.py:13 msgid "Date of Graduation" msgstr "Дата выпуска" @@ -2639,6 +2685,14 @@ msgstr "Склад" msgid "No courses yet" msgstr "Список курсов пуст" +#: apps/staff/views.py:857 apps/staff/views.py:919 apps/users/models.py:382 +msgid "Telegram" +msgstr "" + +#: apps/staff/views.py:857 apps/staff/views.py:919 apps/users/models.py:310 +msgid "email address" +msgstr "" + #: apps/stats/apps.py:7 msgid "Statistics" msgstr "Статистика" @@ -3042,27 +3096,27 @@ msgstr "Внешние сервисы" msgid "Important dates" msgstr "Важные даты" -#: apps/users/admin.py:215 +#: apps/users/admin.py:208 msgid "" "Regular student profile already exists for this admission campaign year." msgstr "" "Профиль регулярного студента уже существует для связанной кампании по " "набору." -#: apps/users/admin.py:251 +#: apps/users/admin.py:244 msgid "Official Student Info" msgstr "Официальный студент" -#: apps/users/admin.py:255 +#: apps/users/admin.py:248 msgid "Curator's note" msgstr "Заметка куратора" -#: apps/users/admin.py:284 apps/users/forms.py:25 apps/users/models.py:346 +#: apps/users/admin.py:277 apps/users/forms.py:25 apps/users/models.py:346 #: lms/jinja2/lms/user_profile/_tab_account.html:151 msgid "Date of Birth" msgstr "Дата рождения" -#: apps/users/admin.py:303 +#: apps/users/admin.py:296 msgid "Anytask" msgstr "" @@ -3188,10 +3242,6 @@ msgstr "Пользователь с таким именем уже сущест msgid "CSCUser|patronymic" msgstr "Отчество" -#: apps/users/models.py:310 -msgid "email address" -msgstr "" - #: apps/users/models.py:312 msgid "staff status" msgstr "" @@ -3252,10 +3302,6 @@ msgstr "Дополнительно" msgid "Github Login" msgstr "Логин на github.com" -#: apps/users/models.py:382 -msgid "Telegram" -msgstr "" - #: apps/users/models.py:391 msgid "stepik.org ID" msgstr "stepik.org ID" @@ -3395,10 +3441,6 @@ msgstr "Профили студентов" msgid "Value should be >= {} (year of the branch establishment)" msgstr "" -#: apps/users/models.py:982 apps/users/models.py:1081 -msgid "Studying" -msgstr "Учится" - #: apps/users/models.py:1035 msgid "Designates whether this Log was processed in document list" msgstr "Был ли этот переход обработан через список для приказов" @@ -3411,48 +3453,45 @@ msgstr "Дата обработки" msgid "Student Log" msgstr "Лог изменения поля [абстрактный]" -#: apps/users/models.py:1063 -msgid "Former status" -msgstr "Прошлый статус" - -#: apps/users/models.py:1074 lms/jinja2/lms/teaching/assignment_detail.html:15 +#: apps/users/models.py:1074 lms/jinja2/lms/staff/status_log.html:9 +#: lms/jinja2/lms/teaching/assignment_detail.html:15 msgid "Student Status Log" msgstr "Лог изменения статуса" -#: apps/users/models.py:1102 +#: apps/users/models.py:1109 #: lms/jinja2/lms/staff/academic_discipline_log.html:9 msgid "Student Academic Discipline Log" msgstr "Лог изменения направления обучения" -#: apps/users/models.py:1115 +#: apps/users/models.py:1123 msgid "Online course record" msgstr "Запись об онлайн-курсе" -#: apps/users/models.py:1116 +#: apps/users/models.py:1124 msgid "Online course records" msgstr "Записи об онлайн-курсах" -#: apps/users/models.py:1141 +#: apps/users/models.py:1149 msgid "SHAD course record" msgstr "Запись о курсе ШАД" -#: apps/users/models.py:1142 +#: apps/users/models.py:1150 msgid "SHAD course records" msgstr "Записи о курсах ШАД" -#: apps/users/models.py:1153 +#: apps/users/models.py:1161 msgid "Reference|signature" msgstr "Подпись" -#: apps/users/models.py:1154 +#: apps/users/models.py:1162 msgid "Reference|note" msgstr "Текст в свободной форме" -#: apps/users/models.py:1162 +#: apps/users/models.py:1170 msgid "Student Reference" msgstr "Справка студента" -#: apps/users/models.py:1163 +#: apps/users/models.py:1171 msgid "Student References" msgstr "Справки студента" @@ -3505,7 +3544,7 @@ msgid "Мои курсы" msgstr "" #: lk_yandexdataschool_ru/menu.py:26 lk_yandexdataschool_ru/menu.py:69 -#: lk_yandexdataschool_ru/menu.py:102 +#: lk_yandexdataschool_ru/menu.py:101 msgctxt "menu" msgid "Полезное" msgstr "" @@ -3580,42 +3619,42 @@ msgctxt "menu" msgid "Файлы" msgstr "" -#: lk_yandexdataschool_ru/menu.py:103 +#: lk_yandexdataschool_ru/menu.py:102 msgctxt "menu" msgid "Фейсбук" msgstr "" -#: lk_yandexdataschool_ru/menu.py:104 +#: lk_yandexdataschool_ru/menu.py:103 msgctxt "menu" msgid "Пересечения" msgstr "" -#: lk_yandexdataschool_ru/menu.py:109 +#: lk_yandexdataschool_ru/menu.py:108 msgctxt "menu" msgid "Набор" msgstr "" -#: lk_yandexdataschool_ru/menu.py:113 +#: lk_yandexdataschool_ru/menu.py:112 msgctxt "menu" msgid "Собеседования" msgstr "" -#: lk_yandexdataschool_ru/menu.py:114 +#: lk_yandexdataschool_ru/menu.py:113 msgctxt "menu" msgid "Анкеты" msgstr "" -#: lk_yandexdataschool_ru/menu.py:115 +#: lk_yandexdataschool_ru/menu.py:114 msgctxt "menu" msgid "Отправка приглашений" msgstr "" -#: lk_yandexdataschool_ru/menu.py:116 +#: lk_yandexdataschool_ru/menu.py:115 msgctxt "menu" msgid "Приглашения" msgstr "" -#: lk_yandexdataschool_ru/menu.py:117 +#: lk_yandexdataschool_ru/menu.py:116 msgctxt "menu" msgid "Результаты" msgstr ""