Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Тексты интерфейса PROCOLLAB

Во всех новых и изменяемых текстах интерфейса используйте Е/е вместо U+0401/U+0451.
Правило относится к подписям, подсказкам, статусам, ошибкам, уведомлениям,
письмам, заголовкам выгрузок, Django admin и макетам Figma.

Не переписывайте пользовательские данные, имена и собственные названия из
справочников. Не меняйте валидаторы допустимых букв, поисковую нормализацию,
идентификаторы и исторические миграции ради этого правила. Проверяйте именно
текст продукта; глобальная подмена символов в данных во время отображения запрещена.
4 changes: 2 additions & 2 deletions courses/services/export_course_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def _format_stage(
published_lessons: list[CourseLesson],
) -> str:
if course_progress.status == ProgressStatus.COMPLETED:
return "Курс завершён"
return "Курс завершен"

lesson_progress_by_lesson_id = {
progress.lesson_id: progress for progress in lesson_progresses
Expand Down Expand Up @@ -212,7 +212,7 @@ def _format_stage(
return _task_header(published_tasks[0])
return _lesson_stage_header(lesson)

return "Этап не определён"
return "Этап не определен"


def _format_answer_cell(answer: UserTaskAnswer | None) -> str:
Expand Down
6 changes: 3 additions & 3 deletions courses/tests/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def test_course_admin_export_marks_completed_course_stage(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(rows), 2)
self.assertEqual(rows[1][2], 100)
self.assertEqual(rows[1][5], "Курс завершён")
self.assertEqual(rows[1][5], "Курс завершен")
self.assertEqual(rows[1][6], "Retention отражает возврат пользователей.")

def test_course_admin_export_resolves_next_stage_for_not_started_next_module(self):
Expand Down Expand Up @@ -375,7 +375,7 @@ def test_course_admin_export_is_undefined_when_all_published_lessons_are_complet
percent=100,
)

self.assertEqual(self._stage_from_export(course), "Этап не определён")
self.assertEqual(self._stage_from_export(course), "Этап не определен")

def test_course_admin_export_is_undefined_when_course_has_no_published_lessons(self):
course = create_course(title="Экспорт без опубликованных уроков")
Expand All @@ -395,7 +395,7 @@ def test_course_admin_export_is_undefined_when_course_has_no_published_lessons(s

self._mark_course_started(student, course)

self.assertEqual(self._stage_from_export(course), "Этап не определён")
self.assertEqual(self._stage_from_export(course), "Этап не определен")

def test_course_admin_export_skips_draft_modules_in_stage_fallback(self):
course = create_course(title="Экспорт пропуска draft модуля")
Expand Down
6 changes: 3 additions & 3 deletions partner_programs/management_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def patch(self, request, pk, section):
):
return conflict(
"reopen_confirmation_required",
"Изменение срока снова откроет приём или оценивание. Подтвердите сохранение.",
"Изменение срока снова откроет прием или оценивание. Подтвердите сохранение.",
)
program.save(update_fields=[*values, "datetime_updated"])
return Response(management_data(program))
Expand Down Expand Up @@ -243,7 +243,7 @@ def patch(self, request, pk, material_id):
if values.pop("expected_updated_at") != material.datetime_updated:
return conflict(
"stale_material",
"Материал изменён другим организатором. Обновите список.",
"Материал изменен другим организатором. Обновите список.",
)
for key, value in values.items():
setattr(material, key, value)
Expand All @@ -258,7 +258,7 @@ def delete(self, request, pk, material_id):
if expected != material.datetime_updated:
return conflict(
"stale_material",
"Материал изменён другим организатором. Обновите список.",
"Материал изменен другим организатором. Обновите список.",
)
# Удаляется только материал программы; исходный UserFile остаётся доступен владельцу.
material.delete()
Expand Down
20 changes: 20 additions & 0 deletions partner_programs/migrations/0026_interface_copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.11 on 2026-10-20 09:27

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('files', '0007_auto_20230929_1727'),
('partner_programs', '0025_registration_connection_details'),
]

operations = [
migrations.AlterField(
model_name='partnerprogrammaterial',
name='file',
field=models.ForeignKey(blank=True, help_text='Если указан файл, ссылка берется из него', null=True, on_delete=django.db.models.deletion.SET_NULL, to='files.userfile', verbose_name='Файл'),
),
]
4 changes: 2 additions & 2 deletions partner_programs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ class PartnerProgramMaterial(models.Model):
blank=True,
null=True,
verbose_name="Файл",
help_text="Если указан файл, ссылка берётся из него",
help_text="Если указан файл, ссылка берется из него",
)

datetime_created = models.DateTimeField(auto_now_add=True)
Expand Down Expand Up @@ -445,7 +445,7 @@ def clean(self):
"invalid_submission", "Программа не является конкурсной."
)
if not self.partner_program.is_project_submission_open():
raise TeamError("submission_closed", "Срок подачи проектов завершён.")
raise TeamError("submission_closed", "Срок подачи проектов завершен.")
team_policy.validate_submission_team(self.project, self)
from django.db import connection
from partner_programs.services.case_fields import (
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/registration_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ProgramRegistration(models.Model):
def clean(self):
super().clean()
if self.external_status == "connected" and not self.external_url:
raise ValidationError({"external_url": "Укажите адрес подключённой формы."})
raise ValidationError({"external_url": "Укажите адрес подключенной формы."})

def __str__(self):
return f"Регистрация программы {self.program_id}"
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/serializers/attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class ProgramNotSubmittedMetadataSerializer(serializers.Serializer):

WAITING_REASONS = {
"no_assignments": "Эксперты не назначены",
"no_completed_evaluations": "Нет завершённых оценок",
"no_completed_evaluations": "Нет завершенных оценок",
"partially_evaluated": "Частично оценено",
"awaiting_first_evaluation": "Ожидает первой оценки",
}
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/serializers/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def get_fields(self):
def validate(self, attrs):
editable = BASIC_FIELDS if self.context["section"] == "basic" else DATE_FIELDS
if not any(key in attrs for key in editable):
raise serializers.ValidationError("Нет изменённых полей.")
raise serializers.ValidationError("Нет измененных полей.")
return attrs

class Meta:
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/serializers/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ class ProgramProjectFilterRequestSerializer(serializers.Serializer):
def validate_filters(self, value):
if not isinstance(value, dict):
raise serializers.ValidationError(
"Поле filters должно быть объектом (словарём ключ-значение)"
"Поле filters должно быть объектом (словарем ключ-значение)"
)

if len(value) > self.MAX_FILTERS:
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/services/case_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def validate_program_case_configuration(field):
elif len({value.casefold() for value in options}) != len(options):
errors[
"options"
] = "Варианты кейсов не должны повторяться (без учёта регистра и внешних пробелов)."
] = "Варианты кейсов не должны повторяться (без учета регистра и внешних пробелов)."
if field.pk:
used = set(field.values.values_list("value_text", flat=True).distinct())
if used - set(options):
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/services/management_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def save_form(program_id, user, payload):
if pk not in ids:
if field.values.filter(value_text__regex=r"\S").exists():
raise ValidationError(
{"fields": "Нельзя удалить поле с сохранёнными ответами."}
{"fields": "Нельзя удалить поле с сохраненными ответами."}
)
field.delete()
for item in data["fields"]:
Expand Down
8 changes: 4 additions & 4 deletions partner_programs/services/native_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def open_program(pk, *, lock=False):
def published_config(program):
config = config_for(program)
if not config or not config.published or config.published["mode"] != "native":
raise ValidationError("Встроенная регистрация ещё не опубликована.")
raise ValidationError("Встроенная регистрация еще не опубликована.")
return config


Expand Down Expand Up @@ -315,7 +315,7 @@ def current_answers(program, data):
config = published_config(program)
if config.published_version != data["version"]:
raise WorkspaceConflict(
"form_changed", "Анкета обновлена. Проверьте ответы и отправьте её ещё раз."
"form_changed", "Анкета обновлена. Проверьте ответы и отправьте ее еще раз."
)
return config, validate_answers(config.published, data["answers"])

Expand Down Expand Up @@ -346,7 +346,7 @@ def create_enrollment(program, user, answers, schema):
@transaction.atomic
def enroll(pk, user, data):
if not user.is_authenticated or not user.is_active:
raise PermissionDenied("Войдите в подтверждённый аккаунт.")
raise PermissionDenied("Войдите в подтвержденный аккаунт.")
program = open_program(pk, lock=True)
if PartnerProgramUserProfile.objects.filter(
partner_program=program, user=user
Expand Down Expand Up @@ -422,7 +422,7 @@ def confirm(pk, token):
except signing.BadSignature:
raise WorkspaceConflict(
"confirmation_expired",
"Ссылка недействительна или истекла. Отправьте письмо ещё раз.",
"Ссылка недействительна или истекла. Отправьте письмо еще раз.",
)
program = open_program(pk, lock=True)
pending = get_object_or_404(
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/services/project_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def require_can_apply_project_to_program(
user: User,
) -> None:
if not program.is_project_submission_open():
raise ValidationError("Срок подачи проектов в программу завершён.")
raise ValidationError("Срок подачи проектов в программу завершен.")

if program.is_manager(user):
return
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/services/project_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def submit_program_project(link_id, user):
if link.submitted:
raise ValidationError({"detail": "Проект уже был сдан на проверку."})
if not link.partner_program.is_project_submission_open():
raise ValidationError({"detail": "Срок подачи проектов в программу завершён."})
raise ValidationError({"detail": "Срок подачи проектов в программу завершен."})
try:
validate_case_before_submission(link)
except DjangoValidationError as error:
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/tests/test_attention_analytics_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def test_assignments_without_scores_have_no_completed_evaluations_reason(self):
self.assignment(self.work())
item = self.get(PROJECTS)["results"][0]
self.assertEqual(item["reason"], "no_completed_evaluations")
self.assertEqual(item["reason_label"], "Нет завершённых оценок")
self.assertEqual(item["reason_label"], "Нет завершенных оценок")
self.assertEqual(item["assignments_total"], 1)
self.assertEqual(item["assignments_completed"], 0)

Expand Down
2 changes: 1 addition & 1 deletion partner_programs/tests/test_program_project_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_submit_blocked_after_deadline(self):
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data.get("detail"),
"Срок подачи проектов в программу завершён.",
"Срок подачи проектов в программу завершен.",
)
link.refresh_from_db()
self.assertFalse(link.submitted)
Expand Down
2 changes: 1 addition & 1 deletion partner_programs/tests/test_project_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,5 @@ def test_apply_project_is_blocked_after_submission_deadline(self):
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data[0],
"Срок подачи проектов в программу завершён.",
"Срок подачи проектов в программу завершен.",
)
2 changes: 1 addition & 1 deletion project_rates/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def get(self, request, notification_id=None):
{
"id": row.pk,
"text": (
f"Эксперт {row.expert_name} ({row.expert_email}) вошёл на платформу "
f"Эксперт {row.expert_name} ({row.expert_email}) вошел на платформу "
f"и принял приглашение в программу «{row.invitation.partner_program.name}»."
),
"program_id": row.invitation.partner_program_id,
Expand Down
4 changes: 2 additions & 2 deletions project_rates/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def submit_project_scores(*, user, project_id: int, data) -> None:
) and expected_revision != revision(program):
raise CriteriaConflict(
"criteria_changed",
"Критерии изменились. Обновите форму оценки; введённые ответы сохранены в форме.",
"Критерии изменились. Обновите форму оценки; введенные ответы сохранены в форме.",
)
rating_data, criteria_ids, program = _prepare_project_score_data(
user=user,
Expand Down Expand Up @@ -189,7 +189,7 @@ def _prepare_project_score_data(
program.datetime_evaluation_ends
and timezone.now() > program.datetime_evaluation_ends
):
raise ValidationError("Срок оценивания завершён.")
raise ValidationError("Срок оценивания завершен.")

required = set(program.criterias.exclude(type="str").values_list("pk", flat=True))
if not required.issubset(criteria_ids):
Expand Down
4 changes: 2 additions & 2 deletions project_rates/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ def _validate_data_type(criteria_type: str, value: str):
):
raise ValueError("Требуется конечное число подходящего типа.")
except ValueError:
raise ValueError("Введённое значение не соответствует формату!")
raise ValueError("Введенное значение не соответствует формату!")
except TypeError:
raise TypeError("Вы не ввели никакие данные!")

elif (criteria_type == "bool") and (value not in ["True", "False"]):
raise TypeError("Введённое значение не соответствует формату!")
raise TypeError("Введенное значение не соответствует формату!")

@staticmethod
def _validate_numeric_limits(
Expand Down
4 changes: 2 additions & 2 deletions project_rates/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def save_project_experts(program_id, user, data):
if assignment.has_scores():
raise WorkspaceConflict(
"scores_protected",
"Нельзя снять эксперта с сохранённой оценкой.",
"Нельзя снять эксперта с сохраненной оценкой.",
)
removals.append(assignment.pk)
check_assignment_limit(
Expand Down Expand Up @@ -700,7 +700,7 @@ def save_assignments(program_id, user, expert_id, data):
)
if protected - wanted:
raise WorkspaceConflict(
"scores_protected", "Нельзя снять назначение с сохранёнными оценками."
"scores_protected", "Нельзя снять назначение с сохраненными оценками."
)
check_assignment_limit(program, [(pid, expert.pk) for pid in wanted - existing])
program.project_expert_assignments.filter(
Expand Down
4 changes: 2 additions & 2 deletions projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class ProjectCompanyInline(admin.TabularInline):
extra = 1
autocomplete_fields = ("company", "decision_maker")
fields = ("company", "contribution", "decision_maker")
verbose_name = "Партнёр проекта"
verbose_name_plural = "Партнёры проекта"
verbose_name = "Партнер проекта"
verbose_name_plural = "Партнеры проекта"


class ResourceInline(admin.StackedInline):
Expand Down
19 changes: 19 additions & 0 deletions projects/migrations/0035_interface_copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.11 on 2026-10-20 09:27

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('projects', '0034_default_project_name'),
]

operations = [
migrations.AlterField(
model_name='resource',
name='partner_company',
field=models.ForeignKey(blank=True, help_text='Если не указано — ресурс в поиске партнера.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='resources', to='projects.company'),
),
]
Loading
Loading