Написать валидацию в js для проверки существования объектов в контейнере
Вопрос в следующем.
Есть 2 модели в питоне - Recipe и Ingredient. У них есть связная модель RecipeIngredient.
Есть форма добавления рецепта и добавления ингредиентов из бд. Все прекрасно работает и сохраняет. НО...
Форма сохраняется не зависимо от того есть добавленные элементы ингредиентов или нет. Тк поле явно не указано в forms.py - то и поставить валидацию на его заполнение нет возможности(если же форму объявить, то required просто не даст сохранить форму, тк элементы добавляются в список, а само поле остается пустым).
Я новичок в python(а с js только начинаю знакомиться), потому, если я правильно понимаю задачу, нужно написать проверку контейнера в файле formRecipe.js, в который добавляются ингредиенты на существование объектов или вывести ошибку о необходимости заполнить поле. Помогите решить проблему, к сожалению, не нашел ответа на этот вопрос.

models.py
class Recipe(models.Model):
title = models.CharField(
max_length=254, verbose_name='Название рецепта'
)
author = models.ForeignKey(
User, on_delete=models.CASCADE,
verbose_name='Автор',
related_name='recipes'
)
image = models.ImageField(
upload_to='recipes/images/',
verbose_name='Фото рецепта',
)
text = models.TextField(
verbose_name='Описание'
)
time_cooking = models.PositiveIntegerField(
verbose_name='Время приготовления'
)
slug = models.SlugField(
verbose_name='Ссылка на рецепт',
unique=True, null=True, blank=True
)
tags = models.ManyToManyField(
Tag, through='TagRecipe',
related_name='recipes',
verbose_name='Тэг'
)
ingredient = models.ManyToManyField(
Ingredient,
through='RecipeIngredient',
through_fields=('recipe', 'ingredient'),
verbose_name='Ингридиент',
)
pub_date = models.DateTimeField(
verbose_name='Дата публикации',
auto_now_add=True,
)
objects = RecipeQuerySet.as_manager()
class Meta:
ordering = ('-pub_date', )
verbose_name = 'Рецепт'
verbose_name_plural = 'Рецепты'
def __str__(self):
return self.title
class Ingredient(models.Model):
title = models.CharField(
max_length=254,
verbose_name='Название ингредиента',
)
unit = models.CharField(
max_length=128,
verbose_name='Единица измерения'
)
class RecipeIngredient(models.Model):
recipe = models.ForeignKey(
Recipe,
on_delete=models.CASCADE,
verbose_name='Рецепт',
related_name='recipe_ingredients',
)
ingredient = models.ForeignKey(
Ingredient,
on_delete=models.CASCADE,
verbose_name='Ингредиент',
related_name='ingredient_recipes',
)
count = models.PositiveIntegerField(
verbose_name='Количество',
)
view.py
def get_ingredients(request):
ingredients = {}
for key in request.POST:
if key.startswith('nameIngredient'):
value_ingredient = key[15:]
ingredients[request.POST[key]] = request.POST[
'valueIngredient_' + value_ingredient
]
return ingredients
@login_required
def recipe_create(request):
form = RecipeForm(
request.POST or None,
files=request.FILES or None
)
ingredients = get_ingredients(request)
if not form.is_valid():
context = {
'form': form,
'is_new': True,
}
return render(
request,
'recipes/recipe_create.html',
context
)
recipe = form.save(commit=False)
recipe.author = request.user
recipe.save()
RecipeIngredient.objects.filter(recipe=recipe).delete()
objs = []
if ingredients is None:
raise forms.ValidationError(
'А кто поле будет заполнять, Пушкин?',
params={'ingredients': ingredients},
)
for title, count in ingredients.items():
ingredient = get_object_or_404(Ingredient, title=title)
objs.append(RecipeIngredient(
recipe=recipe,
ingredient=ingredient,
count=count
))
RecipeIngredient.objects.bulk_create(objs)
form.save_m2m()
return redirect('index')
serializer.py + views.py
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = ('title', 'unit',)
extra_kwargs = {'title': {'required': True}}
class GetIngredient(viewsets.GenericViewSet, mixins.ListModelMixin):
queryset = Ingredient.objects.all()
permission_classes = (permissions.IsAuthenticated,)
serializer_class = IngredientSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('title',)
html
{% extends "recipes/base.html" %}
{% load static %}
{% block title %}
{% if recipe_edit %}
Добавить рецепт
{% else %}
Редактировать рецепт
{% endif %}
{% endblock %}
{% block static_css %}
<link rel="stylesheet" href="{% static 'pages/form.css' %}">
{% endblock %}
{% block heading %}
{% if user.is_authenticated %}
{% if recipe_edit %}
<h1 class="main__title">Редактировать рецепт</h1>
{% else %}
<h1 class="main__title">Добавить рецепт</h1>
{% endif %}
{% endif %}
{% endblock %}
{% block content %}
{% load recipe_filters %}
<div class="form-container">
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form__group">
<label for="id_name" class="form__label">{{ form.title.label }}</label>
<div class="form__field-group">
{{ form.title|addclass:'form__input' }}
<span class="form__error"></span>
</div>
</div>
<div class="form__group">
<p class="form__label">{{ form.tags.label }}</p>
<div class="form__field-group">
<div class="tags">
{% for tag in form.fields.tags.choices.queryset %}
<div class="tags__item">
<input type="checkbox" name="tags" value="{{ tag.id }}" id="id_{{ tag.display_name }}"
class="tags__checkbox tags__checkbox_{{ tag.color }}" checked>
<label for="id_tags_{{ tag.id }}" class="tags__label">{{ tag.title }}</label>
</div>
{% endfor %}
</div>
<span class="form__error"></span>
</div>
</div>
<div class="form__group">
<label for="nameIngredient" class="form__label">Ингредиенты</label>
<div class="form__field-group">
<div class="form__field-group-ingredientes">
<div class="form__dropdown">
<input class="form__input" id="nameIngredient" type="text">
<div class="form__dropdown-items"></div>
</div>
<input type="number" id="cantidad" class="form__input" min="0">
<label for="cantidad" class="form__label" id="cantidadVal">шт</label>
</div>
<div class="form__field-group-ingredientes-container" ></div>
<span class="form__ingredient-link" id="addIng">Добавить ингредиент</span>
<span class="form__error"></span>
</div>
</div>
<div class="form__group">
<label for="id_time" class="form__label">{{ form.time_cooking.label }}</label>
<div class="form__field-group form__field-group_time">
{{ form.time_cooking|addclass:'form__input' }}
<label for="id_time" class="form__label">минут</label>
<span class="form__error"></span>
</div>
</div>
<div class="form__group">
<label for="id_description" class="form__label">{{ form.text.label }}</label>
<div class="form__field-group">
{{ form.text|addclass:'form__textarea' }}
<span class="form__error"></span>
</div>
</div>
<div class="form__group">
<span class="form__label">{{ form.image.label }}</span>
<div class="form__field-group">
{{ form.image }}
<span class="form__error"></span>
</div>
</div>
<div class="form__footer">
{% if recipe_edit %}
<button type="submit" class="button button_style_blue">Редактировать рецепт</button>
{% else %}
<button type="submit" class="button button_style_blue">Создать рецепт</button>
{% endif %}
</div>
</form>
</div>
{% endblock %}
{% block static_js %}
<script src="{% static 'js/config/config.js' %}"></script>
<script src="{% static 'js/components/Header.js' %}"></script>
<script src="{% static 'js/utils/debouncing.js' %}"></script>
<script src="{% static 'js/api/Api.js' %}"></script>
<script src="{% static 'formRecipe.js' %}"></script>
{% endblock %}
formRecipe.js
const counterId = document.querySelector('#counter');
const ingredientsContainer = document.querySelector('.form__field-group-ingredientes-container');
const nameIngredient = document.querySelector('#nameIngredient');
const formDropdownItems = document.querySelector('.form__dropdown-items');
const cantidadVal = document.querySelector('#cantidadVal');
const cantidad = document.querySelector('#cantidad')
const addIng = document.querySelector('#addIng');
const api = new Api(apiUrl);
const header = new Header(counterId);
const defineInitialIndex = function () {
const ingredients = ingredientsContainer.querySelectorAll('.form__field-item-ingredient')
if (ingredients.length === 0) { return 1 }
const data = Array.from(ingredients).map(item => {
if (!item.getAttribute('id')) { return 0 }
if (!item.getAttribute('id').split('_')[1]) { return 0 }
return Number(item.getAttribute('id').split('_')[1])
})
data.sort((a, b) => a-b)
return data[data.length - 1] + 1
}
function Ingredients() {
let cur = defineInitialIndex();
// клик по элементам с сервера
const dropdown = (e) => {
if (e.target.classList.contains('form__item-list')) {
nameIngredient.value = e.target.textContent;
formDropdownItems.style.display = ''
cantidadVal.textContent = e.target.getAttribute('data-val');
}
};
// Добавление элемента из инпута
const addIngredient = (e) => {
if(nameIngredient.value && cantidad.value) {
const data = getValue();
const elem = document.createElement('div');
elem.classList.add('form__field-item-ingredient');
elem.id = `ing_${cur}`;
elem.innerHTML = `<span> ${data.name} ${data.value}${data.units}</span> <span class="form__field-item-delete"></span>
<input id="nameIngredient_${cur}" name="nameIngredient_${cur}" type="hidden" value="${data.name}">
<input id="valueIngredient_${cur}" name="valueIngredient_${cur}" type="hidden" value="${data.value}">
<input id="unitsIngredient_${cur}" name="unitsIngredient_${cur}" type="hidden" value="${data.units}">`;
cur++;
ingredientsContainer.appendChild(elem);
}
};
// удаление элемента
const eventDelete = (e) => {
if(e.target.classList.contains('form__field-item-delete')) {
const item = e.target.closest('.form__field-item-ingredient');
item.removeEventListener('click',eventDelete);
item.remove()
};
};
ingredientsContainer.addEventListener('click', eventDelete);
// получение данных из инпутов для добавления
const getValue = (e) => {
const data = {
name: nameIngredient.value,
value: cantidad.value,
units: cantidadVal.textContent
};
clearValue(nameIngredient);
clearValue(cantidad);
return data;
};
// очистка инпута
const clearValue = (input) => {
input.value = '';
};
return {
clearValue,
getValue,
addIngredient,
dropdown
}
}
const cbEventInput = (elem) => {
return api.getIngredients(elem.target.value).then( e => {
if(e.length !== 0 ) {
const items = e.map( elem => {
return `<a class="form__item-list" data-val="${elem.unit}"">${elem.title}</a>`
}).join(' ')
formDropdownItems.style.display = 'flex';
formDropdownItems.innerHTML = items;
}
})
.catch( e => {
console.log(e)
})
};
const eventInput = debouncing(cbEventInput, 1000);
// вешаем апи
nameIngredient.addEventListener('input', eventInput);
const ingredients = Ingredients();
// вешаем слушатель на элементы с апи
formDropdownItems.addEventListener('click', ingredients.dropdown);
// вешаем слушатель на кнопку
addIng.addEventListener('click', ingredients.addIngredient);
Ответы (1 шт):
Думаю проблема с этим. Укажите в форме необходимость заполнить 'ingridient` поле и смотрите п. 1 и 2 в коде ниже
class Recipe(models.Model):
...
ingredient = models.ManyToManyField( # <!- 1. Валидируйте это поле
Ingredient,
through='RecipeIngredient',
through_fields=('recipe', 'ingredient'), # <!- Обычно это не нужно
verbose_name='Ингридиент',
# Я бы добавил тут related_name='recipes', чтобы получить доступ из Ингридентов сразу к Рецептам
)
...
class Ingredient(models.Model):
...
class RecipeIngredient(models.Model):
recipe = models.ForeignKey(
Recipe,
on_delete=models.CASCADE,
verbose_name='Рецепт',
related_name='recipe_ingredients', # <- 2. Оно вам не нужно, См. 1
)
ingredient = models.ForeignKey(
Ingredient,
on_delete=models.CASCADE,
verbose_name='Ингредиент',
related_name='ingredient_recipes',
)
count = models.PositiveIntegerField(
verbose_name='Количество',
)