diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fde8bb7..8897e31 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ stages: - build - deploy + - utils variables: DOCKER_TLS_CERTDIR: /certs @@ -113,3 +114,37 @@ deploy: EOF - ssh $SSH_ADDRESS "docker system prune -a --force" retry: 2 + + +reset-compose: + image: kroniak/ssh-client:3.19 + stage: utils + when: manual + rules: + - if: '$CI_COMMIT_REF_NAME == "master"' + variables: + SSH_HOST: "158.160.172.23" + SSH_USER: "ubuntu" + SSH_ADDRESS: "$SSH_USER@$SSH_HOST" + SSH_PRIVATE_KEY: SSH_PRIVATE_KEY + script: + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - ssh-keyscan -H "$SSH_HOST" > /dev/null 2>&1 + + - AUTH_COMMAND="echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin"; + - ssh $SSH_ADDRESS "$AUTH_COMMAND" + - scp -C -r infrastructure/ compose.yaml $SSH_ADDRESS:~/deploy/ + - ssh $SSH_ADDRESS "docker -v" + - | + ssh $SSH_ADDRESS <<'EOF' + cd ~/deploy + docker compose down -v > deploy.log 2>&1 + docker compose up -d --remove-orphans --force-recreate >> deploy.log 2>&1 + docker compose ps >> deploy.log 2>&1 + EOF + - ssh $SSH_ADDRESS "docker system prune -a --force" + retry: 2 diff --git a/services/backend/apps/achievement/migrations/0001_initial.py b/services/backend/apps/achievement/migrations/0001_initial.py index b20fb21..b6ad6c1 100644 --- a/services/backend/apps/achievement/migrations/0001_initial.py +++ b/services/backend/apps/achievement/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 12:09 +# Generated by Django 5.1.6 on 2025-03-02 21:24 import apps.achievement.models import uuid @@ -19,7 +19,8 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('name', models.CharField(max_length=30, unique=True, verbose_name='название')), ('description', models.TextField(verbose_name='описание')), - ('icon', models.FileField(upload_to=apps.achievement.models.Achievement.image_url_upload_to, verbose_name='иконка достижения')), + ('icon', models.ImageField(upload_to=apps.achievement.models.Achievement.image_url_upload_to, verbose_name='иконка достижения')), + ('slug', models.SlugField(unique=True, verbose_name='слаг')), ], options={ 'verbose_name': 'ачивка', diff --git a/services/backend/apps/achievement/migrations/0002_achievement_need_count_achievement_type.py b/services/backend/apps/achievement/migrations/0002_achievement_need_count_achievement_type.py deleted file mode 100644 index e16f3b6..0000000 --- a/services/backend/apps/achievement/migrations/0002_achievement_need_count_achievement_type.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 12:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('achievement', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='achievement', - name='need_count', - field=models.IntegerField(default=5, help_text='Здесь нужно указать количество действий, необходимое для получения ачивок. Например, если вы указали в предыдущем пункте "Выполненные задания" а тут 5, то ачивка будет выдаваться за 5 решенных заданий', verbose_name='кол-во того, что нужно для получения ачивки'), - ), - migrations.AddField( - model_name='achievement', - name='type', - field=models.CharField(choices=[('correct_tasks', 'Выполненные задания')], default='correct_tasks', help_text='За какой тип достижений будет выдаваться ачивка', max_length=20, verbose_name='тип'), - ), - ] diff --git a/services/backend/apps/achievement/migrations/0003_remove_achievement_need_count_and_more_squashed_0004_alter_achievement_slug.py b/services/backend/apps/achievement/migrations/0003_remove_achievement_need_count_and_more_squashed_0004_alter_achievement_slug.py deleted file mode 100644 index 682a718..0000000 --- a/services/backend/apps/achievement/migrations/0003_remove_achievement_need_count_and_more_squashed_0004_alter_achievement_slug.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 13:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - replaces = [('achievement', '0003_remove_achievement_need_count_and_more'), ('achievement', '0004_alter_achievement_slug')] - - dependencies = [ - ('achievement', '0002_achievement_need_count_achievement_type'), - ] - - operations = [ - migrations.RemoveField( - model_name='achievement', - name='need_count', - ), - migrations.RemoveField( - model_name='achievement', - name='type', - ), - migrations.AddField( - model_name='achievement', - name='slug', - field=models.SlugField(unique=True, verbose_name='слаг'), - ), - ] diff --git a/services/backend/apps/achievement/migrations/0005_alter_achievement_icon.py b/services/backend/apps/achievement/migrations/0005_alter_achievement_icon.py deleted file mode 100644 index 7ed0851..0000000 --- a/services/backend/apps/achievement/migrations/0005_alter_achievement_icon.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 14:03 - -import apps.achievement.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('achievement', '0003_remove_achievement_need_count_and_more_squashed_0004_alter_achievement_slug'), - ] - - operations = [ - migrations.AlterField( - model_name='achievement', - name='icon', - field=models.ImageField(upload_to=apps.achievement.models.Achievement.image_url_upload_to, verbose_name='иконка достижения'), - ), - ] diff --git a/services/backend/apps/competition/migrations/0001_initial.py b/services/backend/apps/competition/migrations/0001_initial.py index 1ada8da..c13f62b 100644 --- a/services/backend/apps/competition/migrations/0001_initial.py +++ b/services/backend/apps/competition/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 10:28 +# Generated by Django 5.1.6 on 2025-03-02 21:24 import apps.competition.models import datetime diff --git a/services/backend/apps/review/migrations/0001_initial.py b/services/backend/apps/review/migrations/0001_initial.py index 1d0ac7b..0ec3a21 100644 --- a/services/backend/apps/review/migrations/0001_initial.py +++ b/services/backend/apps/review/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 10:28 +# Generated by Django 5.1.6 on 2025-03-02 21:24 import uuid from django.db import migrations, models diff --git a/services/backend/apps/review/migrations/0002_initial.py b/services/backend/apps/review/migrations/0002_initial.py index 2d6ee5c..426c065 100644 --- a/services/backend/apps/review/migrations/0002_initial.py +++ b/services/backend/apps/review/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 10:28 +# Generated by Django 5.1.6 on 2025-03-02 21:24 import django.db.models.deletion from django.db import migrations, models diff --git a/services/backend/apps/task/admin.py b/services/backend/apps/task/admin.py index 4a8ff15..6feaced 100644 --- a/services/backend/apps/task/admin.py +++ b/services/backend/apps/task/admin.py @@ -16,6 +16,8 @@ class CompletionAttachmentInline(admin.StackedInline): class CompetitionTaskAdmin(admin.ModelAdmin): list_display = ("title", "type", "points") filter_horizontal = ("reviewers",) + list_filter = ("type",) + inlines = (CompletionAttachmentInline,) @admin.register(CompetitionTaskSubmission) diff --git a/services/backend/apps/task/migrations/0001_initial.py b/services/backend/apps/task/migrations/0001_initial.py index c2cbaa8..8a2fa34 100644 --- a/services/backend/apps/task/migrations/0001_initial.py +++ b/services/backend/apps/task/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 5.1.6 on 2025-03-02 10:28 +# Generated by Django 5.1.6 on 2025-03-02 21:24 import apps.task.models import django.db.models.deletion -import tinymce.models +import martor.models import uuid from django.db import migrations, models @@ -22,16 +22,17 @@ class Migration(migrations.Migration): name='CompetitionTask', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('in_competition_position', models.PositiveSmallIntegerField(blank=True, null=True)), + ('in_competition_position', models.PositiveSmallIntegerField()), ('title', models.CharField(max_length=50, verbose_name='заголовок')), - ('description', tinymce.models.HTMLField(max_length=300, verbose_name='описание')), + ('description', martor.models.MartorField(verbose_name='описание')), ('max_attempts', models.PositiveSmallIntegerField(blank=True, null=True)), ('type', models.CharField(choices=[('input', 'Ввод правильного ответа'), ('checker', 'Ввод кода'), ('review', 'Ручная')], max_length=8, verbose_name='тип проверки')), ('correct_answer_file', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTask.answer_file_upload_to, verbose_name='файл с правильным ответом')), ('points', models.IntegerField(blank=True, null=True, verbose_name='баллы за задание')), ('answer_file_path', models.TextField(blank=True, default='stdout', help_text='Путь до файла в котором ожидается результат. Пример: stdout или ./output.txt', null=True, verbose_name='куда сделать вывод программы участнику')), + ('submission_reviewers_count', models.PositiveSmallIntegerField(blank=True, default=1, null=True)), ('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')), - ('reviewers', models.ManyToManyField(blank=True, to='review.reviewer')), + ('reviewers', models.ManyToManyField(blank=True, help_text='Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему', to='review.reviewer', verbose_name='ревьюверы')), ], options={ 'verbose_name': 'задание', @@ -43,12 +44,13 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('file', models.FileField(upload_to=apps.task.models.CompetitionTaskAttachment.file_upload_at, verbose_name='файл')), - ('bind_at', models.FilePathField(verbose_name='путь сохранения')), + ('bind_at', models.CharField(max_length=255, verbose_name='путь сохранения')), ('public', models.BooleanField(default=False, verbose_name='публичный')), ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask', verbose_name='задание')), ], options={ - 'abstract': False, + 'verbose_name': 'вложение', + 'verbose_name_plural': 'вложения', }, ), migrations.CreateModel( @@ -69,19 +71,20 @@ class Migration(migrations.Migration): name='CompetitionTaskSubmission', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8, verbose_name='статус')), - ('content', models.FileField(upload_to=apps.task.models.CompetitionTaskSubmission.submission_content_upload_to)), - ('stdout', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_upload_to)), - ('result', models.JSONField(blank=True, default=None, null=True)), - ('earned_points', models.IntegerField(blank=True, null=True)), - ('checked_at', models.DateTimeField(blank=True, null=True)), - ('plagiarism_checked', models.BooleanField(default=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')), + ('status', models.CharField(choices=[('sent', 'Отправлено на проверку'), ('checking', 'Проверка'), ('checked', 'Проверено')], default='sent', max_length=8, verbose_name='статус')), + ('content', models.FileField(upload_to=apps.task.models.CompetitionTaskSubmission.submission_content_upload_to, verbose_name='содержание посылки')), + ('stdout', models.FileField(blank=True, help_text='Используется только при проверке чекером', null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_upload_to, verbose_name='вывод программы')), + ('result', models.JSONField(blank=True, default=None, null=True, verbose_name='результат проверки')), + ('earned_points', models.IntegerField(blank=True, null=True, verbose_name='баллы за задание')), + ('checked_at', models.DateTimeField(blank=True, null=True, verbose_name='дата проверки')), + ('plagiarism_checked', models.BooleanField(default=False, verbose_name='проверено на плагиат')), + ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='дата отправки')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask', verbose_name='задание')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user', verbose_name='пользователь')), ], options={ - 'abstract': False, + 'verbose_name': 'посылка', + 'verbose_name_plural': 'посылки', }, ), ] diff --git a/services/backend/apps/task/migrations/0002_alter_competitiontasksubmission_options_and_more.py b/services/backend/apps/task/migrations/0002_alter_competitiontasksubmission_options_and_more.py deleted file mode 100644 index 9cc1672..0000000 --- a/services/backend/apps/task/migrations/0002_alter_competitiontasksubmission_options_and_more.py +++ /dev/null @@ -1,71 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 12:09 - -import apps.task.models -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('review', '0002_initial'), - ('task', '0001_initial'), - ('user', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='competitiontasksubmission', - options={'verbose_name': 'посылка', 'verbose_name_plural': 'посылки'}, - ), - migrations.AlterField( - model_name='competitiontask', - name='reviewers', - field=models.ManyToManyField(blank=True, help_text='Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему', to='review.reviewer', verbose_name='ревьюверы'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='checked_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='дата проверки'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='content', - field=models.FileField(upload_to=apps.task.models.CompetitionTaskSubmission.submission_content_upload_to, verbose_name='содержание посылки'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='earned_points', - field=models.IntegerField(blank=True, null=True, verbose_name='баллы за задание'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='plagiarism_checked', - field=models.BooleanField(default=False, verbose_name='проверено на плагиат'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='result', - field=models.JSONField(blank=True, default=None, null=True, verbose_name='результат проверки'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='stdout', - field=models.FileField(blank=True, help_text='Используется только при проверке чекером', null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_upload_to, verbose_name='вывод программы'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='task', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask', verbose_name='задание'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='timestamp', - field=models.DateTimeField(auto_now_add=True, verbose_name='дата отправки'), - ), - migrations.AlterField( - model_name='competitiontasksubmission', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user', verbose_name='пользователь'), - ), - ] diff --git a/services/backend/apps/task/migrations/0003_alter_competitiontask_description.py b/services/backend/apps/task/migrations/0003_alter_competitiontask_description.py deleted file mode 100644 index 2dfa914..0000000 --- a/services/backend/apps/task/migrations/0003_alter_competitiontask_description.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 12:23 - -import tinymce.models -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('task', '0002_alter_competitiontasksubmission_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='competitiontask', - name='description', - field=tinymce.models.HTMLField(verbose_name='описание'), - ), - ] diff --git a/services/backend/apps/task/migrations/0004_competitiontask_submission_reviewers_count_and_more.py b/services/backend/apps/task/migrations/0004_competitiontask_submission_reviewers_count_and_more.py deleted file mode 100644 index 400255c..0000000 --- a/services/backend/apps/task/migrations/0004_competitiontask_submission_reviewers_count_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 12:49 - -import martor.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('task', '0003_alter_competitiontask_description'), - ] - - operations = [ - migrations.AddField( - model_name='competitiontask', - name='submission_reviewers_count', - field=models.PositiveSmallIntegerField(blank=True, default=1, null=True), - ), - migrations.AlterField( - model_name='competitiontask', - name='description', - field=martor.models.MartorField(verbose_name='описание'), - ), - ] diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index c0a4ea5..0c77556 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -79,16 +79,20 @@ class CompetitionTaskCriteria(BaseModel): class CompetitionTaskAttachment(BaseModel): - def file_upload_at(instance, filename): - return f"attachment/{instance.id}/file/{filename}" + def file_upload_at(instance, filename) -> str: + return f"attachments/{instance.id}/file/{filename}" task = models.ForeignKey( CompetitionTask, on_delete=models.CASCADE, verbose_name="задание" ) file = models.FileField(upload_to=file_upload_at, verbose_name="файл") - bind_at = models.FilePathField(verbose_name="путь сохранения") + bind_at = models.CharField(verbose_name="путь сохранения", max_length=255) public = models.BooleanField(default=False, verbose_name="публичный") + class Meta: + verbose_name = "вложение" + verbose_name_plural = "вложения" + class CompetitionTaskSubmission(BaseModel): class StatusChoices(models.TextChoices): diff --git a/services/backend/apps/team/migrations/0001_initial.py b/services/backend/apps/team/migrations/0001_initial.py index 7e4fca4..a2eca7f 100644 --- a/services/backend/apps/team/migrations/0001_initial.py +++ b/services/backend/apps/team/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 10:28 +# Generated by Django 5.1.6 on 2025-03-02 21:24 import django.db.models.deletion import uuid diff --git a/services/backend/apps/user/migrations/0001_initial.py b/services/backend/apps/user/migrations/0001_initial.py index fe09ceb..c1fa8be 100644 --- a/services/backend/apps/user/migrations/0001_initial.py +++ b/services/backend/apps/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 10:28 +# Generated by Django 5.1.6 on 2025-03-02 21:24 import uuid from django.db import migrations, models @@ -9,6 +9,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('achievement', '0001_initial'), ] operations = [ @@ -21,6 +22,7 @@ class Migration(migrations.Migration): ('password', models.TextField(verbose_name='пароль')), ('created_at', models.DateTimeField(auto_now=True)), ('status', models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10)), + ('achievements', models.ManyToManyField(blank=True, to='achievement.achievement', verbose_name='ачивки пользователя')), ], options={ 'verbose_name': 'пользователь', diff --git a/services/backend/apps/user/migrations/0002_user_achievements.py b/services/backend/apps/user/migrations/0002_user_achievements.py deleted file mode 100644 index 33adefa..0000000 --- a/services/backend/apps/user/migrations/0002_user_achievements.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 12:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('achievement', '0001_initial'), - ('user', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='achievements', - field=models.ManyToManyField(blank=True, to='achievement.achievement', verbose_name='ачивки пользователя'), - ), - ]