diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c76d809c1..354f98a95 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,13 +1,13 @@ # Base Python image -FROM python:3.9.1-buster +FROM python:3.10-bullseye # Update system and install backup utilities COPY back-end/docker/image/install-postgres-client.sh / RUN sh install-postgres-client.sh -# Install node 14 -RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - -RUN apt -y install nodejs +# Install node 16 +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash +RUN . ~/.nvm/nvm.sh && nvm install 16 # Setup fish RUN apt -y install fish; chsh -s /usr/bin/fish; mkdir /root/.config; mkdir /root/.config/fish diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 1a2b05c01..544dbed0e 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -18,7 +18,8 @@ services: - ../back-end/.env volumes: # Forwards the local Docker socket to the container. - - /var/run/docker.sock:/var/run/docker-host.sock + - ~/.ssh:/root/.ssh + - /var/run/docker.sock:/var/run/docker.sock # Update this to wherever you want VS Code to mount the folder of your project - ..:/workspace:cached diff --git a/.github/workflows/back-end.yaml b/.github/workflows/back-end.yaml index 197db8ed1..97482e92b 100644 --- a/.github/workflows/back-end.yaml +++ b/.github/workflows/back-end.yaml @@ -12,15 +12,15 @@ jobs: - run: cp back-end/.env.example back-end/.env - run: cp tgbot/.env.example tgbot/.env - run: cp watchdoc/.env.example watchdoc/.env - - name: "Build back-end container" - run: docker-compose build back-end - name: "Run Postgres in background" - run: docker-compose up -d postgres + run: docker compose up -d postgres + - name: "Build back-end container" + run: docker compose build back-end - name: "Run tests" - run: docker-compose run back-end scripts/test.sh + run: docker compose run back-end scripts/test.sh - name: "Check code style" - run: docker-compose run back-end scripts/format.sh --check + run: docker compose run back-end scripts/format.sh --check # - name: "Lint code" # run: docker-compose run back-end scripts/lint.sh - name: "Stop all containers" - run: docker-compose down + run: docker compose down diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c61669a5f..4b6dee64e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -55,7 +55,7 @@ jobs: sudo chmod 777 vpn.log sudo killall openvpn - name: Upload VPN logs - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: VPN logs diff --git a/.github/workflows/front-end.yaml b/.github/workflows/front-end.yaml index f3a93ed77..7c4d8ba1d 100644 --- a/.github/workflows/front-end.yaml +++ b/.github/workflows/front-end.yaml @@ -20,4 +20,4 @@ jobs: - name: "Check code style" run: cd front-end && npm run lint . - name: "Build front end container" - run: docker-compose build front-end + run: docker compose build front-end diff --git a/back-end/docker/image/dev.dockerfile b/back-end/docker/image/dev.dockerfile index a326ad901..a6628b7f7 100644 --- a/back-end/docker/image/dev.dockerfile +++ b/back-end/docker/image/dev.dockerfile @@ -1,5 +1,5 @@ # Base Python image -FROM python:3.10-buster +FROM python:3.10-bullseye # Update system and install backup utilities COPY back-end/docker/image/install-postgres-client.sh / diff --git a/back-end/docker/image/prod.dockerfile b/back-end/docker/image/prod.dockerfile index d028e9993..1e608eb72 100644 --- a/back-end/docker/image/prod.dockerfile +++ b/back-end/docker/image/prod.dockerfile @@ -1,5 +1,5 @@ # Base Python image -FROM python:3.10-buster +FROM python:3.10-bullseye # Update system and install backup utilities COPY back-end/docker/image/install-postgres-client.sh / diff --git a/back-end/src/ams/utils/common.py b/back-end/src/ams/utils/common.py index ef283c3c5..5ea2a99ca 100644 --- a/back-end/src/ams/utils/common.py +++ b/back-end/src/ams/utils/common.py @@ -3,6 +3,6 @@ def get_current_admission_year(): now = datetime.now() - if (now.month, now.day) > (9, 1): + if (now.month, now.day) >= (9, 1): return now.year + 1 return now.year diff --git a/back-end/src/ams/views/applicants.py b/back-end/src/ams/views/applicants.py index 61af435f6..ef136e659 100644 --- a/back-end/src/ams/views/applicants.py +++ b/back-end/src/ams/views/applicants.py @@ -32,6 +32,8 @@ from ams.models.applicants import Applicant +from common.models.universities import Program + from ams.serializers.applicants import ( ApplicantSerializer, ApplicantMutateSerializer, @@ -147,6 +149,15 @@ def create(self, request, *args, **kwargs): request.data["user"] = self.request.user.id serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + if not self.milspecialty_is_selectable( + request.data["milspecialty"], request.data["university_info"]["program"] + ): + return Response( + { + "detail": "You can't select this milspecialty with your educational program" + }, + status=status.HTTP_400_BAD_REQUEST, + ) self.request.user.campuses = [self.request.data["university_info"]["campus"]] self.request.user.save() if self.is_creation_allowed_by_scope(request.data): @@ -177,6 +188,15 @@ def update(self, request, *args, **kwargs): {"detail": "Bad request"}, status=status.HTTP_400_BAD_REQUEST, ) + if not self.milspecialty_is_selectable( + request.data["milspecialty"], request.data["university_info"]["program"] + ): + return Response( + { + "detail": "You can't select this milspecialty with your educational program" + }, + status=status.HTTP_400_BAD_REQUEST, + ) result = super(ApplicantViewSet, self).update(request, **kwargs) applicant.user.campuses = [request.data["university_info"]["campus"]] applicant.user.save() @@ -187,6 +207,10 @@ def update(self, request, *args, **kwargs): generate_documents_for_applicant(updated_applicant) return result + def milspecialty_is_selectable(self, milspecialty_id: int, program_id: int): + milspecialty = Milspecialty.objects.filter(pk=milspecialty_id).first() + return milspecialty.is_selectable_by_program(program_id) + @transaction.atomic def perform_create(self, serializer): return serializer.save() diff --git a/back-end/src/common/admin.py b/back-end/src/common/admin.py index eceb30e40..cadbd12e3 100644 --- a/back-end/src/common/admin.py +++ b/back-end/src/common/admin.py @@ -30,5 +30,5 @@ admin.site.register(Program) admin.site.register(Faculty) -# Milspeciality +# Milspecialty admin.site.register(Milspecialty) diff --git a/back-end/src/common/management/commands/populate.py b/back-end/src/common/management/commands/populate.py index 3d47435fd..6189853be 100644 --- a/back-end/src/common/management/commands/populate.py +++ b/back-end/src/common/management/commands/populate.py @@ -20,6 +20,9 @@ create_faculties, create_programs, ) +from common.populate.milspecialities_selectable_by_programs import ( + create_milspecialities_selectable_by_programs, +) from common.utils.date import get_date_range @@ -169,6 +172,9 @@ def handle(self, *args, **options): subjects = create_subjects(milspecialties) faculties = create_faculties() programs = create_programs(faculties) + create_milspecialities_selectable_by_programs( + milspecialties=milspecialties, programs=programs + ) print(" OK") diff --git a/back-end/src/common/migrations/0005_add_selectable_by_to_milspecs.py b/back-end/src/common/migrations/0005_add_selectable_by_to_milspecs.py new file mode 100644 index 000000000..fb7936676 --- /dev/null +++ b/back-end/src/common/migrations/0005_add_selectable_by_to_milspecs.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.20 on 2024-10-12 16:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0004_add_personal_documents_info'), + ] + + operations = [ + migrations.AddField( + model_name='milspecialty', + name='selectable_by', + field=models.ManyToManyField(to='common.Program'), + ), + migrations.AddField( + model_name='milspecialty', + name='selectable_by_every_program', + field=models.BooleanField(default=True), + ), + ] diff --git a/back-end/src/common/models/milspecialties.py b/back-end/src/common/models/milspecialties.py index 6c86b39c1..3834d4915 100644 --- a/back-end/src/common/models/milspecialties.py +++ b/back-end/src/common/models/milspecialties.py @@ -1,8 +1,12 @@ +from typing import Union + from django.db import models from django.contrib.postgres.fields import ArrayField - -from common.models.universities import Campus +from common.models.universities import Campus, Program +from rest_framework.exceptions import ValidationError +from django.db.models.signals import m2m_changed +from django.dispatch import receiver class Milspecialty(models.Model): @@ -18,9 +22,41 @@ class Milspecialty(models.Model): ) ) + selectable_by = models.ManyToManyField(to=Program, blank=True) + + selectable_by_every_program = models.BooleanField(default=True) + class Meta: verbose_name = "Military Specialty" verbose_name_plural = "Military Specialties" def __str__(self) -> str: return self.title + + def is_selectable_by_program(self, program_: Union[Program, int]): + if isinstance(program_, Program): + program = program_.pk + else: + program = program_ + return ( + self.selectable_by.filter(pk=program).exists() + or self.selectable_by_every_program + ) + + +@receiver(m2m_changed, sender=Milspecialty.selectable_by.through) +def validate_b_titles( + sender, instance: Milspecialty, action, reverse, model, pk_set, **kwargs +): + if action in ["post_add", "post_remove", "post_clear"]: + mismatched_progs = instance.selectable_by.exclude( + faculty__campus__in=instance.available_for + ) + if mismatched_progs.exists(): + program: Program = mismatched_progs.first() + raise ValidationError( + { + "selectable_by": f"Can't make milspecialty {instance} be selectable by program {program}: " + f"program's campus ({program.faculty.campus}) is not available for this program" + } + ) diff --git a/back-end/src/common/populate/milspecialities_selectable_by_programs.py b/back-end/src/common/populate/milspecialities_selectable_by_programs.py new file mode 100644 index 000000000..0ee0568a7 --- /dev/null +++ b/back-end/src/common/populate/milspecialities_selectable_by_programs.py @@ -0,0 +1,20 @@ +from common.models.milspecialties import Milspecialty + +from common.models.universities import ( + Program, +) + + +def create_milspecialities_selectable_by_programs( + milspecialties: dict[str, Milspecialty], programs: dict[str, Milspecialty] +): + milspecialties["453000"].selectable_by.add( + programs["Информационная безопасность"], + programs["Информатика и вычислительная техника"], + programs["Программная инженерия"], + ) + milspecialties["453100"].selectable_by.add( + programs["Информационная безопасность"], + programs["Информатика и вычислительная техника"], + programs["Программная инженерия"], + ) diff --git a/back-end/src/common/populate/milspecialties.py b/back-end/src/common/populate/milspecialties.py index 09be19b3c..9b3bd3734 100644 --- a/back-end/src/common/populate/milspecialties.py +++ b/back-end/src/common/populate/milspecialties.py @@ -10,11 +10,13 @@ def create_milspecialties() -> dict[str, Milspecialty]: "code": "453000", "title": "Организация эксплуатации и ремонта автоматизированных систем управления и вычислительных комплексов ракетно-космической обороны", "available_for": [Campus.MOSCOW.value], + "selectable_by_every_program": False, }, { "code": "453100", "title": "Математическое и программное обеспечение функционирования вычислительных комплексов ракетно-космической обороны", "available_for": [Campus.MOSCOW.value], + "selectable_by_every_program": False, }, { "code": "461300", diff --git a/back-end/src/common/serializers/milspecialties.py b/back-end/src/common/serializers/milspecialties.py index e8723a19e..d2b2bbeff 100644 --- a/back-end/src/common/serializers/milspecialties.py +++ b/back-end/src/common/serializers/milspecialties.py @@ -6,4 +6,15 @@ class MilspecialtySerializer(serializers.ModelSerializer): class Meta: model = Milspecialty - fields = "__all__" + exclude = ["selectable_by", "selectable_by_every_program"] + + +class WithSelectableByProgramMilspecialtySerializer(MilspecialtySerializer): + selectable_by_program = serializers.SerializerMethodField() + + def get_selectable_by_program(self, milspecialty: Milspecialty): + request = self.context["request"] + program = request.query_params.get("program") + if program is not None: + return milspecialty.is_selectable_by_program(program) + return False diff --git a/back-end/src/common/tests/conftest.py b/back-end/src/common/tests/conftest.py index eb3cc8a9e..36bc4891c 100644 --- a/back-end/src/common/tests/conftest.py +++ b/back-end/src/common/tests/conftest.py @@ -3,19 +3,27 @@ from auth.models import User from common.models.milspecialties import Milspecialty from common.models.subjects import Subject +from common.models.universities import Program, Faculty +from typing import Optional @pytest.fixture -def create_milspeciality(): - def call_me(title: str, code: str, available_for=None): +def create_milspecialty(): + def call_me( + title: str, + code: str, + available_for: Optional[list[str]] = None, + selectable_by_every_program: bool = True, + ): if available_for is None: available_for = ["MO"] - milspeciality, _ = Milspecialty.objects.get_or_create( + milspecialty, _ = Milspecialty.objects.get_or_create( title=title, code=code, available_for=available_for, + selectable_by_every_program=selectable_by_every_program, ) - return milspeciality + return milspecialty return call_me @@ -71,3 +79,35 @@ def call_me( return data return call_me + + +@pytest.fixture +def create_faculty(): + def call_me(title: str, abbreviation: str, campus: str = "MO"): + faculty, _ = Faculty.objects.get_or_create( + title=title, + abbreviation=abbreviation, + campus=campus, + ) + return faculty + + return call_me + + +@pytest.fixture +def create_program(): + def call_me( + title: str, + code: str, + faculty: Faculty, + available_to_choose_for_applicants: bool = True, + ): + program, _ = Program.objects.get_or_create( + title=title, + code=code, + faculty=faculty, + available_to_choose_for_applicants=available_to_choose_for_applicants, + ) + return program + + return call_me diff --git a/back-end/src/common/tests/test_milsepcialties_listing.py b/back-end/src/common/tests/test_milsepcialties_listing.py new file mode 100644 index 000000000..d418c1270 --- /dev/null +++ b/back-end/src/common/tests/test_milsepcialties_listing.py @@ -0,0 +1,81 @@ +import pytest + + +@pytest.mark.django_db +def test_milspecialty_selectable( + su_client, create_faculty, create_milspecialty, create_program +): + cs = create_faculty( + title="Факультет Компьютерных Наук", abbreviation="ФКН", campus="MO" + ) + law = create_faculty(title="Юридический факультет", abbreviation="ЮФ", campus="SP") + + ami = create_program( + title="Прикладная математика и информатика", + code="01.03.02 Прикладная математика и информатика", + faculty=cs, + ) + economy = create_program( + title="Экономика", + code="38.03.01 Экономика", + faculty=cs, + ) + + # SPB campus + jurisprudence = create_program( + title="40.03.01 Юриспруденция", + code="40.03.01 Юриспруденция", + faculty=law, + ) + + officers = create_milspecialty( + title="Математическое и программное обеспечение комплексов ПРО", + code="453100", + selectable_by_every_program=False, + ) + + sergeants = create_milspecialty( + title="Стрелковые, командир стрелкового отделения", + code="100182", + selectable_by_every_program=True, + ) + + # Allow selection of "Математическое и программное обеспечение" by ami + officers.selectable_by.add(ami) + + # Default case (no program specified) – do not show selectable_by_program flag + milspecialties_no_program = su_client.get( + f"/api/lms/milspecialties/?campus={cs.campus}" + ).json() + for milspecialty in milspecialties_no_program: + assert "selectable_by_program" not in milspecialty + + milspecialties_for_economy = su_client.get( + f"/api/lms/milspecialties/?campus={cs.campus}&program={economy.pk}" + ).json() + assert len(milspecialties_for_economy) == 2 + golden_selectable_by_economy = { + "453100": False, # "Математическое и программное обеспечение" is not selectable by every program, and economy can't select it + "100182": True, + } + for milspecialty in milspecialties_for_economy: + assert milspecialty["code"] in golden_selectable_by_economy + assert ( + milspecialty["selectable_by_program"] + == golden_selectable_by_economy[milspecialty["code"]] + ), f"Incorrect selectable_by_program (law) for milspecialty \"{milspecialty['title']}\"" + + milspecialties_for_ami = su_client.get( + f"/api/lms/milspecialties/?campus={cs.campus}&program={ami.pk}" + ).json() + assert len(milspecialties_for_ami) == 2 + golden_selectable_by_ami = { + "453100": True, # "Математическое и программное обеспечение" is not selectable by every program, BUT ami CAN select it + "100182": True, + } + for milspecialty in milspecialties_for_ami: + assert milspecialty["code"] in golden_selectable_by_ami + assert ( + milspecialty["selectable_by_program"] + == golden_selectable_by_ami[milspecialty["code"]] + ), f"Incorrect selectable_by_program (ami) for milspecialty \"{milspecialty['title']}\"" diff --git a/back-end/src/common/tests/test_subjects.py b/back-end/src/common/tests/test_subjects.py index 74d0a91c6..44a85624f 100644 --- a/back-end/src/common/tests/test_subjects.py +++ b/back-end/src/common/tests/test_subjects.py @@ -22,13 +22,13 @@ def test_trailing_slash_redirect(su_client): @pytest.mark.django_db def test_get_subject_by_id( - su_client, create_milspeciality, get_new_subject_data, create_subject + su_client, create_milspecialty, get_new_subject_data, create_subject ): - milspeciality = create_milspeciality( + milspecialty = create_milspecialty( title="Математическое обеспечения комплексов ПРО", code="453100" ) subj_data = get_new_subject_data( - title="Строевая подготовка", milspecialty=milspeciality + title="Строевая подготовка", milspecialty=milspecialty ) subject = create_subject(**subj_data) subj_data = unpack_subject(subject) @@ -41,13 +41,13 @@ def test_get_subject_by_id( @pytest.mark.django_db def test_get_subjects_by_title( - su_client, create_milspeciality, get_new_subject_data, create_subject + su_client, create_milspecialty, get_new_subject_data, create_subject ): - milspeciality = create_milspeciality( + milspecialty = create_milspecialty( title="Математическое обеспечения комплексов ПРО", code="453100" ) subj_data = get_new_subject_data( - title="Тактическая подготовка", milspecialty=milspeciality + title="Тактическая подготовка", milspecialty=milspecialty ) subject = create_subject(**subj_data) subj_data = unpack_subject(subject) @@ -64,13 +64,13 @@ def test_get_subjects_by_title( @pytest.mark.django_db def test_get_subjects_by_search( - su_client, create_milspeciality, get_new_subject_data, create_subject + su_client, create_milspecialty, get_new_subject_data, create_subject ): - milspeciality = create_milspeciality( + milspecialty = create_milspecialty( title="Математическое обеспечения комплексов ПРО", code="453100" ) subj_data = get_new_subject_data( - title="Тактико-специальная подготовка 2", milspecialty=milspeciality + title="Тактико-специальная подготовка 2", milspecialty=milspecialty ) subject = create_subject(**subj_data) word = subject.title.split()[1] diff --git a/back-end/src/dms/tests/test_end_to_end_numbering.py b/back-end/src/dms/tests/test_end_to_end_numbering.py index 5806e8eff..c627e332a 100644 --- a/back-end/src/dms/tests/test_end_to_end_numbering.py +++ b/back-end/src/dms/tests/test_end_to_end_numbering.py @@ -123,7 +123,6 @@ def test_sections_reordering( topic8, topic9, ): - check_order([section1, section2, section3, section4]) check_order([topic1, topic2, topic3, topic4, topic5, topic6, topic7]) check_order([section5, section6]) diff --git a/back-end/src/lms/models/students.py b/back-end/src/lms/models/students.py index b36105afe..7c7642b65 100644 --- a/back-end/src/lms/models/students.py +++ b/back-end/src/lms/models/students.py @@ -208,6 +208,13 @@ def student_post_callback(sender, instance: Student, *args, **kwargs): @receiver(models.signals.post_delete, sender=Student) def post_delete_fields(sender, instance: Student, **kwargs): + # Если у пользователя есть модель абитуриента, удаляем и её + if instance.user: + try: + applicant = Applicant.objects.get(user=instance.user) + applicant.delete() + except Applicant.DoesNotExist: + pass # pylint: disable=unused-argument attributes_to_delete = [ "user", diff --git a/back-end/src/lms/views/reference_book.py b/back-end/src/lms/views/reference_book.py index 3baeb13ad..d1e0be111 100644 --- a/back-end/src/lms/views/reference_book.py +++ b/back-end/src/lms/views/reference_book.py @@ -2,6 +2,9 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework.views import APIView +from rest_framework.exceptions import ValidationError +from rest_framework import status +from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend @@ -12,7 +15,10 @@ from common.models.milspecialties import Milspecialty from common.models.universities import Program -from common.serializers.milspecialties import MilspecialtySerializer +from common.serializers.milspecialties import ( + MilspecialtySerializer, + WithSelectableByProgramMilspecialtySerializer, +) from common.serializers.universities import ProgramSerializer from common.filters.milspecialties import MilspecialtyFilter @@ -106,7 +112,11 @@ class MilfacultyViewSet(ModelViewSet): @extend_schema(tags=["reference-book"]) class MilspecialtyViewSet(ModelViewSet): - serializer_class = MilspecialtySerializer + def get_serializer_class(self): + if "program" in self.request.query_params: + return WithSelectableByProgramMilspecialtySerializer + return MilspecialtySerializer + queryset = Milspecialty.objects.all() permission_classes = [ReadOnly | ReferenceBookPermission] @@ -114,6 +124,30 @@ class MilspecialtyViewSet(ModelViewSet): filter_backends = [DjangoFilterBackend] filterset_class = MilspecialtyFilter + @extend_schema( + parameters=[ + OpenApiParameter( + name="program", + type=int, + location=OpenApiParameter.QUERY, + description="Show if selectable by program", + ), + ] + ) + def list(self, request, *args, **kwargs): + program = request.query_params.get("program") + if program is not None: + try: + # Attempt to convert the program to an integer + int(program) + except ValueError: + # If conversion fails, raise a validation error + raise ValidationError( + {"program": "Program must be a valid integer."}, + code=status.HTTP_400_BAD_REQUEST, + ) + return super().list(request, *args, **kwargs) + @extend_schema(tags=["reference-book"]) class MilgroupViewSet(QuerySetScopingMixin, ModelViewSet): diff --git a/back-end/src/lms/views/students.py b/back-end/src/lms/views/students.py index b5ec2eebf..c45769032 100644 --- a/back-end/src/lms/views/students.py +++ b/back-end/src/lms/views/students.py @@ -22,7 +22,7 @@ from common.views.choices import GenericChoicesList -from auth.models import Permission +from auth.models import Group, Permission from auth.permissions import BasePermission from auth.tokens.registration import generate_regconf_token @@ -50,6 +50,8 @@ from lms.types.personnel import Personnel +from common.models.personal import ContactInfo, PersonalDocumentsInfo + class StudentPermission(BasePermission): permission_class = "students" @@ -107,6 +109,7 @@ def get_serializer_class(self): mutate_actions = MUTATE_ACTIONS + [ "registration", "registration_for_existing_students", + "register_from_applicant", ] if self.action in mutate_actions: return StudentMutateSerializer @@ -157,6 +160,48 @@ def registration(self, request): serializer = self.get_serializer(instance) return Response(serializer.data) + @action( + detail=False, methods=["post"], permission_classes=[permissions.IsAdminUser] + ) + def register_from_applicant(self, request): + request.data[ + "status" + ] = "ST" # Выставляем входной структуре статус обучающегося + + # Если студент регистрировался в 2022 или 2023 году, у него нет ИНН и СНИЛСа в системе, + # нужно заполнить + if request.data.get("personal_documents_info") is None: + request.data["personal_documents_info"] = { + "tax_id": "-", + "insurance_number": "-", + } + + serializer = self.get_serializer_class()( + data=request.data + ) # TODO: write new serializer + + serializer.is_valid(raise_exception=True) + + corporate_email = serializer.validated_data["contact_info"]["corporate_email"] + user = serializer.validated_data["user"] + + contact_info = ContactInfo.objects.get(corporate_email=corporate_email) + + student = serializer.save() + + student.user = user + student.contact_info = contact_info + + student.save() + + students, _ = Group.objects.get_or_create(name="Студент") + students.user_set.add(user) + + applicants, _ = Group.objects.get_or_create(name="Абитуриент") + applicants.user_set.remove(user) + + return Response(self.get_serializer(student).data) + @registration.mapping.post def registration_for_existing_students(self, request): request.data["status"] = "ST" diff --git a/front-end/src/api/reference-book.js b/front-end/src/api/reference-book.js index 871e4272a..a0dca9a0b 100644 --- a/front-end/src/api/reference-book.js +++ b/front-end/src/api/reference-book.js @@ -31,6 +31,14 @@ export function getMilSpecialties(campus) { }); } +export function getMilSpecialtiesSelectableByProgram(campus, program) { + return request({ + url: BASE_API_URL + milspecialties, + method: "get", + params: { campus, program }, + }); +} + export function editMilSpecialty(id, data) { return request({ url: `${BASE_API_URL}${milspecialties}${id}/`, diff --git a/front-end/src/api/user.js b/front-end/src/api/user.js index 85c6b948b..8ff6403be 100644 --- a/front-end/src/api/user.js +++ b/front-end/src/api/user.js @@ -32,6 +32,14 @@ export function registerStudent(data) { }); } +export function registerStudentFromApplicant(data) { + return request({ + url: BASE_API_URL + LMS_URLS.register.studentFromApplicant, + method: "post", + data, + }); +} + export function registerTeacher(data) { return request({ url: BASE_API_URL + LMS_URLS.register.teachers, diff --git a/front-end/src/common/inputs/Select.vue b/front-end/src/common/inputs/Select.vue index 0211ada9c..38cd2dcbe 100644 --- a/front-end/src/common/inputs/Select.vue +++ b/front-end/src/common/inputs/Select.vue @@ -14,10 +14,11 @@ v-bind="$attrs" > @@ -55,6 +56,7 @@ class SelectInput extends InputsMixin { return { label: _isObject(rawLabel) ? JSON.stringify(rawLabel) : rawLabel, optionValue: getValue(option), + class_: option.class, }; }); } diff --git a/front-end/src/components/@ApplicantsDocuments/Table.vue b/front-end/src/components/@ApplicantsDocuments/Table.vue index 5f7f4373c..78fef3668 100644 --- a/front-end/src/components/@ApplicantsDocuments/Table.vue +++ b/front-end/src/components/@ApplicantsDocuments/Table.vue @@ -1,5 +1,6 @@