Skip to content

Commit 9fd489d

Browse files
authored
Merge pull request #533 from PROCOLLAB-github/feature/project-mospolytech-data
Доработана валидация данных дополнительных полей программ
2 parents c701dd1 + d12cd44 commit 9fd489d

2 files changed

Lines changed: 316 additions & 21 deletions

File tree

partner_programs/tests.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
from django.test import TestCase
2+
from django.utils import timezone
3+
4+
from partner_programs.models import PartnerProgram, PartnerProgramField
5+
from projects.serializers import PartnerProgramFieldValueUpdateSerializer
6+
7+
8+
class PartnerProgramFieldValueUpdateSerializerInvalidTests(TestCase):
9+
def setUp(self):
10+
now = timezone.now()
11+
self.partner_program = PartnerProgram.objects.create(
12+
name="Тестовая программа",
13+
tag="test_tag",
14+
description="Описание тестовой программы",
15+
city="Москва",
16+
image_address="https://example.com/image.png",
17+
cover_image_address="https://example.com/cover.png",
18+
advertisement_image_address="https://example.com/advertisement.png",
19+
presentation_address="https://example.com/presentation.pdf",
20+
data_schema={},
21+
draft=True,
22+
projects_availability="all_users",
23+
datetime_registration_ends=now + timezone.timedelta(days=30),
24+
datetime_started=now,
25+
datetime_finished=now + timezone.timedelta(days=60),
26+
)
27+
28+
def make_field(self, field_type, is_required, options=None):
29+
return PartnerProgramField.objects.create(
30+
partner_program=self.partner_program,
31+
name="test_field",
32+
label="Test Field",
33+
field_type=field_type,
34+
is_required=is_required,
35+
options="|".join(options) if options else "",
36+
)
37+
38+
def test_required_text_field_empty(self):
39+
field = self.make_field("text", is_required=True)
40+
data = {"field_id": field.id, "value_text": ""}
41+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
42+
self.assertFalse(serializer.is_valid())
43+
self.assertIn(
44+
"Поле должно содержать текстовое значение.", str(serializer.errors)
45+
)
46+
47+
def test_required_textarea_field_null(self):
48+
field = self.make_field("textarea", is_required=True)
49+
data = {"field_id": field.id, "value_text": None}
50+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
51+
self.assertFalse(serializer.is_valid())
52+
self.assertIn(
53+
"Поле должно содержать текстовое значение.", str(serializer.errors)
54+
)
55+
56+
def test_checkbox_invalid_string(self):
57+
field = self.make_field("checkbox", is_required=True)
58+
data = {"field_id": field.id, "value_text": "maybe"}
59+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
60+
self.assertFalse(serializer.is_valid())
61+
self.assertIn("ожидается 'true' или 'false'", str(serializer.errors).lower())
62+
63+
def test_checkbox_invalid_type(self):
64+
field = self.make_field("checkbox", is_required=True)
65+
data = {"field_id": field.id, "value_text": 1}
66+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
67+
self.assertFalse(serializer.is_valid())
68+
self.assertIn("ожидается 'true' или 'false'", str(serializer.errors).lower())
69+
70+
def test_select_invalid_choice(self):
71+
field = self.make_field("select", is_required=True, options=["арбуз", "ананас"])
72+
data = {"field_id": field.id, "value_text": "яблоко"}
73+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
74+
self.assertFalse(serializer.is_valid())
75+
self.assertIn(
76+
"Недопустимое значение для поля типа 'select'", str(serializer.errors)
77+
)
78+
79+
def test_select_required_empty(self):
80+
field = self.make_field("select", is_required=True, options=["арбуз", "ананас"])
81+
data = {"field_id": field.id, "value_text": ""}
82+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
83+
self.assertFalse(serializer.is_valid())
84+
self.assertIn(
85+
"Значение обязательно для поля типа 'select'", str(serializer.errors)
86+
)
87+
88+
def test_radio_invalid_type(self):
89+
field = self.make_field("radio", is_required=True, options=["арбуз", "ананас"])
90+
data = {"field_id": field.id, "value_text": ["арбуз"]}
91+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
92+
self.assertFalse(serializer.is_valid())
93+
self.assertIn("Not a valid string.", str(serializer.errors))
94+
95+
def test_radio_invalid_value(self):
96+
field = self.make_field("radio", is_required=True, options=["арбуз", "ананас"])
97+
data = {"field_id": field.id, "value_text": "груша"}
98+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
99+
self.assertFalse(serializer.is_valid())
100+
self.assertIn(
101+
"Недопустимое значение для поля типа 'radio'", str(serializer.errors)
102+
)
103+
104+
def test_file_invalid_type(self):
105+
field = self.make_field("file", is_required=True)
106+
data = {"field_id": field.id, "value_text": 123}
107+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
108+
self.assertFalse(serializer.is_valid())
109+
self.assertIn(
110+
"Ожидается корректная ссылка (URL) на файл.", str(serializer.errors)
111+
)
112+
113+
def test_file_empty_required(self):
114+
field = self.make_field("file", is_required=True)
115+
data = {"field_id": field.id, "value_text": ""}
116+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
117+
self.assertFalse(serializer.is_valid())
118+
self.assertIn("Файл обязателен для этого поля.", str(serializer.errors))
119+
120+
121+
class PartnerProgramFieldValueUpdateSerializerValidTests(TestCase):
122+
def setUp(self):
123+
now = timezone.now()
124+
self.partner_program = PartnerProgram.objects.create(
125+
name="Тестовая программа",
126+
tag="test_tag",
127+
description="Описание тестовой программы",
128+
city="Москва",
129+
image_address="https://example.com/image.png",
130+
cover_image_address="https://example.com/cover.png",
131+
advertisement_image_address="https://example.com/advertisement.png",
132+
presentation_address="https://example.com/presentation.pdf",
133+
data_schema={},
134+
draft=True,
135+
projects_availability="all_users",
136+
datetime_registration_ends=now + timezone.timedelta(days=30),
137+
datetime_started=now,
138+
datetime_finished=now + timezone.timedelta(days=60),
139+
)
140+
141+
def make_field(self, field_type, is_required, options=None):
142+
return PartnerProgramField.objects.create(
143+
partner_program=self.partner_program,
144+
name="test_field",
145+
label="Test Field",
146+
field_type=field_type,
147+
is_required=is_required,
148+
options="|".join(options) if options else "",
149+
)
150+
151+
def test_optional_text_field_valid(self):
152+
field = self.make_field("text", is_required=False)
153+
data = {"field_id": field.id, "value_text": "some value"}
154+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
155+
self.assertTrue(serializer.is_valid())
156+
157+
def test_required_text_field_valid(self):
158+
field = self.make_field("text", is_required=True)
159+
data = {"field_id": field.id, "value_text": "not empty"}
160+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
161+
self.assertTrue(serializer.is_valid())
162+
163+
def test_optional_textarea_valid(self):
164+
field = self.make_field("textarea", is_required=False)
165+
data = {"field_id": field.id, "value_text": "optional long text"}
166+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
167+
self.assertTrue(serializer.is_valid())
168+
169+
def test_required_textarea_valid(self):
170+
field = self.make_field("textarea", is_required=True)
171+
data = {"field_id": field.id, "value_text": "required long text"}
172+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
173+
self.assertTrue(serializer.is_valid())
174+
175+
def test_checkbox_true_valid(self):
176+
field = self.make_field("checkbox", is_required=True)
177+
data = {"field_id": field.id, "value_text": "true"}
178+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
179+
self.assertTrue(serializer.is_valid())
180+
181+
def test_checkbox_false_valid(self):
182+
field = self.make_field("checkbox", is_required=False)
183+
data = {"field_id": field.id, "value_text": "false"}
184+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
185+
self.assertTrue(serializer.is_valid())
186+
187+
def test_select_valid(self):
188+
field = self.make_field("select", is_required=True, options=["арбуз", "ананас"])
189+
data = {"field_id": field.id, "value_text": "ананас"}
190+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
191+
self.assertTrue(serializer.is_valid())
192+
193+
def test_radio_valid(self):
194+
field = self.make_field(
195+
"radio", is_required=True, options=["арбуз", "апельсин"]
196+
)
197+
data = {"field_id": field.id, "value_text": "апельсин"}
198+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
199+
self.assertTrue(serializer.is_valid())
200+
201+
def test_optional_select_empty_valid(self):
202+
field = self.make_field(
203+
"select", is_required=False, options=["арбуз", "апельсин"]
204+
)
205+
data = {"field_id": field.id, "value_text": ""}
206+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
207+
self.assertTrue(serializer.is_valid())
208+
209+
def test_file_valid_url(self):
210+
field = self.make_field("file", is_required=True)
211+
data = {"field_id": field.id, "value_text": "https://example.com/file.pdf"}
212+
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
213+
self.assertTrue(serializer.is_valid())

projects/serializers.py

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from urllib.parse import urlparse
2+
13
from django.contrib.auth import get_user_model
24
from django.core.cache import cache
35
from rest_framework import serializers
@@ -410,40 +412,120 @@ class PartnerProgramFieldValueUpdateSerializer(serializers.Serializer):
410412
required=False,
411413
allow_blank=True,
412414
allow_null=True,
413-
help_text="Укажите значение для текстового поля.",
415+
help_text="Укажите значение для поля.",
414416
)
415417

416418
def validate(self, attrs):
417419
field = attrs.get("field")
418420
value_text = attrs.get("value_text")
419421

420-
is_required = field.is_required
422+
validator = self._get_validator(field)
423+
validator(field, value_text, attrs)
424+
425+
return attrs
426+
427+
def _get_validator(self, field):
428+
validators = {
429+
"text": self._validate_text,
430+
"textarea": self._validate_text,
431+
"checkbox": self._validate_checkbox,
432+
"select": self._validate_select,
433+
"radio": self._validate_radio,
434+
"file": self._validate_file,
435+
}
436+
try:
437+
return validators[field.field_type]
438+
except KeyError:
439+
raise serializers.ValidationError(
440+
f"Тип поля '{field.field_type}' не поддерживается."
441+
)
421442

422-
if field.field_type == "text" or field.field_type == "text":
423-
if is_required and (value_text is None or value_text == "textarea"):
443+
def _validate_text(self, field, value, attrs):
444+
if field.is_required:
445+
if value is None or str(value).strip() == "":
424446
raise serializers.ValidationError(
425447
"Поле должно содержать текстовое значение."
426448
)
427-
elif field.field_type == "checkbox":
428-
if is_required and value_text in (None, ""):
449+
else:
450+
if value is not None and not isinstance(value, str):
429451
raise serializers.ValidationError(
430-
"Значение обязательно для поля типа 'checkbox'."
452+
"Ожидается строка для текстового поля."
431453
)
432-
if value_text is not None:
433-
if isinstance(value_text, bool):
434-
attrs["value_text"] = "true" if value_text else "false"
435-
elif isinstance(value_text, str):
436-
if value_text.lower() not in ("true", "false"):
437-
raise serializers.ValidationError(
438-
"Для поля типа 'checkbox' ожидается 'true' или 'false'."
439-
)
440-
attrs["value_text"] = value_text.lower()
441-
else:
454+
455+
def _validate_checkbox(self, field, value, attrs):
456+
if field.is_required and value in (None, ""):
457+
raise serializers.ValidationError(
458+
"Значение обязательно для поля типа 'checkbox'."
459+
)
460+
461+
if value is not None:
462+
if isinstance(value, bool):
463+
attrs["value_text"] = "true" if value else "false"
464+
elif isinstance(value, str):
465+
normalized = value.strip().lower()
466+
if normalized not in ("true", "false"):
442467
raise serializers.ValidationError(
443-
"Неверный тип значения для поля 'checkbox'."
468+
"Для поля типа 'checkbox' ожидается 'true' или 'false'."
444469
)
445-
else:
470+
attrs["value_text"] = normalized
471+
else:
472+
raise serializers.ValidationError(
473+
"Неверный тип значения для поля 'checkbox'."
474+
)
475+
476+
def _validate_select(self, field, value, attrs):
477+
self._validate_choice_field(field, value, "select")
478+
479+
def _validate_radio(self, field, value, attrs):
480+
self._validate_choice_field(field, value, "radio")
481+
482+
def _validate_choice_field(self, field, value, field_type):
483+
options = field.get_options_list()
484+
485+
if not options:
446486
raise serializers.ValidationError(
447-
f"Тип поля '{field.field_type}' не поддерживается."
487+
f"Для поля типа '{field_type}' не заданы допустимые значения."
448488
)
449-
return attrs
489+
490+
if field.is_required:
491+
if value is None or value == "":
492+
raise serializers.ValidationError(
493+
f"Значение обязательно для поля типа '{field_type}'."
494+
)
495+
else:
496+
if value is None or value == "":
497+
return # Пустое значение для необязательного поля допустимо
498+
499+
if value is not None:
500+
if not isinstance(value, str):
501+
raise serializers.ValidationError(
502+
f"Ожидается строковое значение для поля типа '{field_type}'."
503+
)
504+
if value not in options:
505+
raise serializers.ValidationError(
506+
f"Недопустимое значение для поля типа '{field_type}'. "
507+
f"Ожидается одно из: {options}."
508+
)
509+
510+
def _validate_file(self, field, value, attrs):
511+
if field.is_required:
512+
if value is None or value == "":
513+
raise serializers.ValidationError("Файл обязателен для этого поля.")
514+
515+
if value is not None:
516+
if not isinstance(value, str):
517+
raise serializers.ValidationError(
518+
"Ожидается строковое значение для поля 'file'."
519+
)
520+
521+
if not self._is_valid_url(value):
522+
raise serializers.ValidationError(
523+
"Ожидается корректная ссылка (URL) на файл."
524+
)
525+
526+
def _is_valid_url(self, url: str) -> bool:
527+
try:
528+
parsed = urlparse(url)
529+
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
530+
except Exception:
531+
return False

0 commit comments

Comments
 (0)