포스트

[Recipe: Original] 레시피 수정, 삭제

레시피 수정

모델

이름설명
Ingredient재료
Recipe레시피
RecipeStep레시피 과정(단계)
RecipeIngredient레시피에 들어가는 재료와 양

이름설명
RecipeUpdateForm레시피 수정 폼
RecipeIngredientUpdateFormSet레시피 재료 수정용 formset
RecipeIngredientFormSet레시피 재료 추가용 formset
RecipeStepUpdateFormSet레시피 단계 수정용 formset
RecipeStepFormSet레시피 단계 추가용 formset

RecipeIngredientUpdateFormSet

1
2
3
4
5
6
7
8
9
10
11
RecipeIngredientUpdateFormSet = forms.inlineformset_factory(
    Recipe,
    RecipeIngredient,
    fields=(
        "ingredient",
        "quantity",
    ),
    extra=0,
    can_delete=True,
    labels={"ingredient": "", "quantity": ""},
)

RecipeIngredientFormSet

1
2
3
4
5
6
7
8
9
10
11
RecipeIngredientFormSet = forms.inlineformset_factory(
    Recipe,
    RecipeIngredient,
    fields=(
        "ingredient",
        "quantity",
    ),
    extra=1,
    can_delete=False,
    labels={"ingredient": "", "quantity": ""},
)

RecipeStepUpdateFormSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RecipeStepUpdateFormSet = forms.inlineformset_factory(
    Recipe,
    RecipeStep,
    fields=("detail",),
    extra=0,
    can_delete=True,
    labels={"detail": ""},
    widgets={
        "detail": forms.Textarea(
            attrs={
                "rows": 1,
            }
        )
    }
)

RecipeStepFormSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RecipeStepFormSet = forms.inlineformset_factory(
    Recipe,
    RecipeStep,
    fields=("detail",),
    extra=1,
    can_delete=False,
    labels={"detail": ""},
    widgets={
        "detail": forms.Textarea(
            attrs={
                "rows": 1,
            }
        )
    }
)

UserPassesTestMixinUpdateView를 상속받는 RecipeUptateView를 생성했다.
속성 template_name은 레시피 수정 페이지 템플릿으로 설정한다.
속성 pk_url_kwargrecipe_pk로 설정했다.

test_func

작성자 또는 관리자만이 레시피를 수정할 수 있게 하기 위해 test_func을 재정의한다.

1
2
3
4
5
6
def test_func(self):
    keys = self.request.path.split("/")
    recipe = Recipe.objects.get(pk=int(keys[2]))
    isAuthor = self.request.user == recipeuser
    isAdmin = self.request.user.is_superuser or self.request.user.is_staff
    return isAuthor or isAdmin

GET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def get(self, *args, **kwargs):
    recipe = Recipe.objects.get(pk=kwargs["recipe_pk"])

    # 재료를 추가할 때 이미 있는 재료를 선택지에서 제외하기 위해
    # 이미 있는 재료를 `temp`로 저장하고
    # `ingredient`를 `temp`를 제외한 `Ingredient`로 지정했다.
    temp = RecipeIngredient.objects.filter(recipe=recipe)
    ingredients = Ingredient.objects.exclude(recipeingredient__in=temp)
    options = [ingredient for ingredient in ingredients]

    # 재료 수정용 폼
    # `prefix`를 따로 설정하지 않으면
    # 재료 추가용 폼의 데이터와 수정 데이터를 구분해서 받기 어렵다.
    ingredientupdateformset = RecipeIngredientUpdateFormSet(
        instance=recipe, prefix="ingredient-update"
    )
    ingredientformset = RecipeIngredientFormSet()

    # 단계 수정용 폼
    # 재료 수정용 폼의 경우와 같은 이유로 `prefix`를 설정했다.
    stepupdateformset = RecipeStepUpdateFormSet(
        instance=recipe, prefix="step-update"
    )
    stepformset = RecipeStepFormSet()

    # 레시피 수정용 폼
    form = RecipeUpdateForm(instance=recipe)

    return self.render_to_response({
        "form": form,
        "ingredientupdateformset": ingredientupdateformset,
        "ingredientformset": ingredientformset,
        "stepupdateformset": stepupdateformset,
        "stepformset": stepformset,
        "options": options,
        "recipe": recipe,
    })

POST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def post(self, *args, **kwargs):
    recipe = Recipe.objects.get(pk=kwargs["recipe_pk"])
    ingredients = RecipeIngredient.objects.filter(recipe=recipe).order_by("pk")
    form = RecipeForm(self.request.POST, self.request.FILES, instance=recipe)

    # 레시피 단계 폼
    stepforms = RecipeStepFormSet(self.request.POST)
    stepupdateforms = RecipeStepUpdateFormSet(
        self.request.POST, prefix="step-update"
    )

    # 레시피 재료 폼
    ingredientforms = RecipeIngredientFormSet(self.request.POST)
    ingredientupdateforms = RecipeIngredientUpdateFormSet(
        self.request.POST, prefix="ingredient-update"
    )

    # 재료 수정 폼의 개수
    update_ingredient_num = int(
        self.request.POST.get("ingredient-update-TOTAL_FORMS")
    )
    # 재료 추가 폼의 개수
    ingredient_num = int(self.request.POST.get("recipeingredient_set-TOTAL_FORMS"))
    # 단계 수정 폼의 개수
    step_num = int(self.request.POST.get("step-update-TOTAL_FORMS"))

    if form.is_valid():
        recipe = form.save(commit=False)
        recipe.user = self.request.user
        recipe.save()

        # 단계 수정 폼
        for subform in stepupdateforms:
            if subform.is_valid():
                step = subform.save(commit=False)
                # 폼에 내용이 있으면 수정된 내용을 저장한다.
                # 폼에 내용이 없으면 해당 단계를 제거한다.
                if step.detail != "":
                    step.save()
                else:
                    step.remove()

        # 단계 추가 폼
        for subform in stepforms:
            if subform.is_valid():
                step = subform.save(commit=False)
                # 폼에 내용이 있는 경우에만 저장한다.
                if step.detail != "":
                    step.recipe = recipe
                    step.save()

        # 삭제할 재료를 위한 리스트
        targets = list()

        # 재료 수정 폼
        for i in range(update_ingredient_num):
            ingredient_pk = self.request.POST.get(
                f"ingredient-update-{i}-ingredient"
            )
            # 재료를 삭제하지 않는 경우
            if ingredient_pk:
                ingredient = Ingredient.objects.get(pk=ingredient_pk)
                quantity = self.request.POST.get(f"ingredient-update-{i}-quantity")
                RecipeIngredient(
                    pk=ingredients[i].pk,
                    recipe=recipe,
                    ingredient=ingredient,
                    quantity=quantity,
                ).save()
            # 재료를 삭제하는 경우
            else:
                targets.append(ingredients[i].pk)

        # 재료 삭제
        for target in targets:
            RecipeIngredient.objects.get(pk=target).delete()

        # 재료, 수량 정보 저장용 리스트
        raw_ingredient = list()

        for i in range(1, ingredient_num):
            raw_ingredient.append((
                self.request.POST.get(f"recipeingredient_set-{i}-ingredient"),
                self.request.POST.get(f"recipeingredient_set-{i}-quantity"),
            ))

        # 재료 추가
        for ingredient, quantity in raw_ingredient:
            # 기존에 있던 재료인 경우
            if ingredient.isdigit():
                RecipeIngredient.objects.create(
                    recipe=recipe,
                    ingredient=Ingredient.objects.get(pk=int(ingredient)),
                    quantity=quantity,
                )
            # 새로운 재료인 경우
            else:
                temp = Ingredient.objects.create(name=ingredient)
                RecipeIngredient.objects.create(
                    recipe=recipe, ingredient=temp, quantity=quantity
                )

        # 단계를 삭제하는 경우
        for i in range(step_num):
            if self.request.POST.get(f"step-update-{i}-DELETE") == "on":
                target = RecipeStep.objects.get(
                    pk=self.request.POST.get(f"step-update-{i}-id")
                )
                target.delete()

        return redirect("recipes:recipe_detail", recipe.pk)

    # 레시피 폼이 유효하지 않은 경우 다시 폼을 렌더링해준다.
    return self.render_to_response({
        "form": form,
        "ingredientupdateforms": ingredientupdateforms,
        "stepupdateforms": stepupdateforms,
        "ingredientforms": ingredientforms,
        "stepforms": stepforms,
    })

템플릿

단계 수정 및 추가

추가 버튼을 누르면 단계 추가 폼이 늘어난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="steps-section">
    {# 레시피 단계 수정 #}
    {{ stepupdateformset.management_form}}

    {% for stepform in stepupdateformset %}
    <div class="step-update-form">
        {{ stepform }}
    </div>
    {% endfor %}

    {# 레시피 단계 추가 #}
    {{ stepformset.management_form }}

    {% for stepform in stepformset %}
    <div class="step-form hidden">
        {{ stepform }}
    </div>
    {% endfor %}
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let addStep = document.querySelector('#add-step')
let stepForm = document.querySelectorAll('.step-form')

addStep.addEventListener('click', addDetail)

function addDetail(e) {
    e.preventDefault()

    let newForm = stepForm[0].cloneNode(true)
    let formRegex = RegExp(`recipestep_set-(\\d){1}-`, 'g')

    stepNum++
    newForm.innerHTML = newForm.innerHTML.replace(formregex, `recipestep_set-${stepNum}-`)
    newForm.innerHTML = newForm.innerHTML.replace()
    stepSection.insertBefore(newForm, line)
    totalSteps.setAttribute('value', `${stepNum + 1}`)
}

재료 수정 및 추가

단계 추가와 마찬가지로 추가 버튼을 누르면 재료 추가 폼이 늘어난다.
재료 입력 시 자동완성(검색)을 지원하기 위해 Select2 플러그인을 사용하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="ingredients-section">
    {# 레시피 재료 수정 #}
    {{ ingredientupdateformset.management_form }}

    {% for ingredientform in ingredientupdateformset %}
    <div class="ingredient-update-form">
        {{ ingredientform.ingredient }}
        {{ ingredientform.quantity }}
    </div>
    {% endfor %}

    {# 레시피 재료 추가 #}
    {{ ingredientformset.management_form }}

    {% for ingredientform in ingredientformset %}
    <div class="ingredient-form hidden">
        {{ ingredientform.ingredient }}
        {{ ingredientform.quantity }}
    </div>
    {% endfor %}
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 재료 수정
let updateForms = document.querySelectorAll('.ingredient-update-form')

updateForms.forEach(form => {
    $(function() {
        $(form.querySelector('select')).select2()
    })
})

let ingredientForm = document.querySelectorAll('.ingredient-form')
let addIngredient = document.querySelector('#add-ingredient')
let totalForms = document.querySelector('#id_recipeingredient_set-TOTAL_FORMS')
let formNum = ingredientForm.length - 1
let line = document.querySelector('#bottom-line')
let stepForm = document.querySelectorAll('.step-form')
let addStep = document.querySelector('#add-step')
let totalSteps = document.querySelector('#id_recipestep_set-TOTAL_FORMS')
const ingredientSection = document.getElementById('ingredients-section')
const stepSection = document.getElementById('steps-section')
let stepNum = stepForm.length - 1

// 단계 추가
addStep.addEventListener('click', addDetail)

function addDetail(e) {
  e.preventDefault()

  let newForm = stepForm[0].cloneNode(true)
  let formRegex = RegExp(`recipestep_set-(\\d){1}-`, 'g')

  stepNum++
  newForm.innerHTML = newForm.innerHTML.replace(formRegex, `recipestep_set-${stepNum}-`)
  newForm.innerHTML = newForm.innerHTML.replace()
  newForm.classList.remove('hidden')
  stepSection.insertBefore(newForm, line)
  totalSteps.setAttribute('value', `${stepNum + 1}`)
}

// 재료 추가
addIngredient.addEventListener('click', addForm)

function addForm(e) {
  e.preventDefault()

  let newForm = ingredientForm[0].cloneNode(true)
  let formRegex = RegExp(`recipeingredient_set-(\\d){1}-`,'g')

  formNum++
  newForm.innerHTML = newForm.innerHTML.replace(formRegex, `recipeingredient_set-${formNum}-`)
  newForm.innerHTML = newForm.innerHTML.replace()
  quantityInput = newForm.querySelector('input')
  quantityInput.setAttribute('placeholder', '수량을 입력해주세요')
  ingredientSection.insertBefore(newForm, line)
  totalForms.setAttribute('value', `${formNum + 1}`)

  $(function() {
    $(newForm.querySelector('select')).select2({
      tags: true,
    })
  })

  newForm.classList.remove('hidden')
}

레시피 삭제

UserPassesTestMixinDeleteView를 상속받는 RecipeDeleteView를 생성했다.
속성 modelRecipe이다.
속성 success_urlreverse_lazy("recipes:recipe_list")로 설정했다.
속성 pk_url_kwargrecipe_pk로 설정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class RecipeDeleteView(UsePassesTextMixin, DeleteView):
    model = Recipe
    success_url = reverse_lazy("recipes:recipe_list")
    pk_url_kwarg = "recipe_pk"


    def test_func(self):
        recipe = self.get_object()
        isAuthor = self.request.user == recipeuser
        isAdmin = self.request.user.is_superuser or self.request.user.is_staff
        return isAuthor or isAdmin


    def get_object(self, queryset=None):
        obj = super().get_object(queryset)
        return obj


    def get(self, request, *args, **kwargs):
        return self.post(request, *args, **kwargs)

이미지 관련

레시피 수정, 또는 삭제 시 이미지 변경 또는 삭제가 발생한 경우 signal을 사용해 처리해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@receiver(post_delete, sender=Recipe)
def delete_recipe_image(sender, instance, *args, **kwargs):
    try:
        instance.image.delete(save=False)
    except:
        pass


@receiver(pre_save, sender=Recipe)
def pre_save_image(sender, instance, *args, **kwargs):
    try:
        old_image = instance.__class__.objects.get(pk=instance.pk).image.path
        try:
            new_image = instance.image.path
        except:
            new_image = None
        if new_image != old_image:
            if os.path.exists(old_image):
                os.remove(old_image)
    except:
        pass
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.