feat: mixin for update forms

This commit is contained in:
2025-01-20 11:40:07 +01:00
parent e540d4cba0
commit df67125599
13 changed files with 227 additions and 120 deletions
+66
View File
@@ -0,0 +1,66 @@
from typing import Type
from django import forms
from django.db import models
from django.shortcuts import render
from django.views.generic import TemplateView
class HTMXFormComponent(TemplateView):
form_class: Type[forms.ModelForm] = None
template_name: str = "components/default/form.html"
hx_trigger = ""
view_name = ""
def get_view_name(self):
return self.view_name
def get_form_class(self) -> Type[forms.ModelForm]:
return self.form_class
def get_form(self, initial=True):
instance = self.get_object()
if initial:
return self.form_class(initial=self.get_initial_values(instance), instance=instance)
return self.form_class(self.request.POST, instance=instance)
def get_object(self) -> models.Model:
raise NotImplementedError
def get_initial_values(self, instance):
return {}
def get_context_data(self, **kwargs):
form = self.get_form()
return {
"form": form,
"url": self.get_view_name(),
}
def post(self, request, *args, **kwargs):
form = self.get_form(initial=False)
if form.is_valid():
instance = form.save()
response = render(
request,
self.template_name,
{
"form": form,
"instance": instance,
},
)
response["HX-Trigger"] = self.hx_trigger
return response
return render(
request,
self.template_name,
{
"form": form,
},
)
+16
View File
@@ -0,0 +1,16 @@
from django.contrib.auth import get_user_model
from django.utils.text import gettext_lazy as _
from django.contrib.auth.forms import PasswordChangeForm
from web.mixins import StylingMixin
User = get_user_model()
class ChangePasswordForm(StylingMixin, PasswordChangeForm):
styled_fields = ("old_password", "new_password1", "new_password2")
placeholder_for_field = {
"old_password": _("********"),
"new_password1": _("********"),
"new_password2": _("********"),
}
+6 -1
View File
@@ -15,7 +15,12 @@ class UserInfoForm(StylingMixin, forms.ModelForm):
"last_name": _("Moreno"),
}
classes = StylingMixin.classes.difference({'w-full'})
def save(self, commit=True):
self.instance.username = self.cleaned_data.get("email")
return super().save(commit)
def is_valid(self):
return super().is_valid()
class Meta:
model = User
+1 -50
View File
@@ -1,9 +1,5 @@
from django.conf import settings
from django.core.paginator import Paginator
from django import forms
from django.db import models
from django.views.generic import TemplateView
from django.shortcuts import get_object_or_404, render
class FilteredQuerysetMixin:
@@ -62,11 +58,7 @@ class StylingMixin:
for field_name in self.styled_fields:
field = self.fields[field_name]
field.required = True
field.widget.attrs.update(
{
"class": ' '.join(self.classes)
}
)
field.widget.attrs.update({"class": " ".join(self.classes)})
if self.placeholder_for_field.get(field_name):
field.widget.attrs.update(
@@ -74,44 +66,3 @@ class StylingMixin:
"placeholder": self.placeholder_for_field.get(field_name),
}
)
class ReadUpdateView(TemplateView):
model: models.Model = None
form_class: forms.ModelForm = None
template_name: str = ""
ok_template_name: str = ""
error_template_name: str = ""
def get_model(self):
return self.model
def get_object(self):
pk = self.kwargs.get('pk')
cls = self.get_model()
return get_object_or_404(cls, pk=pk)
def get_initial_values(self, instance):
return {}
def get_context_data(self, **kwargs):
instance = self.get_object()
form = self.form_class(initial=self.get_initial_values(instance))
return {
"form": form,
}
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
instance = form.save()
return render(request, self.ok_template_name, {
"form": form,
"instance": instance,
})
return render(request, self.error_template_name, {
"form": form,
})
@@ -0,0 +1,25 @@
{% load i18n %}
{% load static %}
<form hx-post="{{ url }}" hx-swap="none">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}"
class="inline-block text-sm font-medium text-gray-900 dark:text-white ml-2">
{{ field.label }}
</label>
{{ field }}
</div>
{% endfor %}
{% for k, error in form.errors.items %}
<span class="flex justify-center mb-2 text-sm font-medium text-red-900 dark:text-red-400">{{ error }}</span>
{% endfor %}
<button class="text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none
focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600
dark:hover:bg-primary-700 dark:focus:ring-primary-800"
>
{% translate 'Guardar' %}
</button>
</form>
+15 -18
View File
@@ -1,28 +1,25 @@
{% load i18n %}
{% load static %}
<form hx-post="{% url 'web:user_info_component' %}" hx-swap="none">
{% csrf_token %}
{% for field in form %}
<form hx-post="{{ url }}" hx-swap="none">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}"
class="inline-block text-sm font-medium text-gray-900 dark:text-white ml-2">
{{ field.label }}
</label>
{{ field }}
<label for="{{ field.id_for_label }}"
class="inline-block text-sm font-medium text-gray-900 dark:text-white ml-2">
{{ field.label }}
</label>
{{ field }}
</div>
{% endfor %}
{% endfor %}
{% for k, error in form.errors.items %}
{% for k, error in form.errors.items %}
<span class="flex justify-center mb-2 text-sm font-medium text-red-900 dark:text-red-400">{{ error }}</span>
{% endfor %}
{% endfor %}
<button class="text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none
<button class="text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none
focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600
dark:hover:bg-primary-700 dark:focus:ring-primary-800"
>
{% translate 'Guardar' %}
</button>
>
{% translate 'Guardar' %}
</button>
</form>
@@ -2,11 +2,11 @@
{% load static %}
{% for address in customer_addresses %}
<form
id="edit_address_{{ address.pk }}"
class="hidden py-2 md:py-4"
hx-post="{% url 'web:update_customer_address' pk=address.pk %}"
hx-on:htmx:after-request="htmx.toggleClass(htmx.find('#edit_address_{{ address.pk }}'), 'hidden'); htmx.toggleClass(htmx.find('#details_address_{{ address.pk }}'), 'hidden')"
hx-swap="none"
id="edit_address_{{ address.pk }}"
class="hidden py-2 md:py-4"
hx-post="{% url 'web:update_customer_address' pk=address.pk %}"
hx-on:htmx:after-request="htmx.toggleClass(htmx.find('#edit_address_{{ address.pk }}'), 'hidden'); htmx.toggleClass(htmx.find('#details_address_{{ address.pk }}'), 'hidden')"
hx-swap="none"
>
{% csrf_token %}
<div class="mb-4 grid gap-4 sm:grid-cols-2 sm:gap-8 lg:gap-16">
@@ -159,14 +159,8 @@
{% translate 'Editar datos' %}
</button>
</div>
</div>
{% if not forloop.last %}
<hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-700">
{% endif %}
{% endfor %}
+9 -2
View File
@@ -10,8 +10,15 @@
{% translate 'Mi cuenta' %}
</h2>
<div
class="mt-4 p-6 border border-gray-100 rounded-md dark:border-gray-600"
hx-get="{% url 'web:user_info_component' %}" hx-trigger="load"
class="mt-4 p-6 border border-gray-100 rounded-md dark:border-gray-600"
hx-get="{% url 'web:user_info_component' %}" hx-trigger="load, updated_user_info"
>
</div>
<div
class="mt-4 p-6 border border-gray-100 rounded-md dark:border-gray-600"
hx-get="{% url 'web:change_password_component' %}" hx-trigger="load, changed_password"
>
</div>
+30
View File
@@ -0,0 +1,30 @@
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from shop.tests.mixins import CreateProductsMixin
class TestChangeUserPassword(TestCase, CreateProductsMixin):
def setUp(self):
self.password = "ihatesand"
self.user = User.objects.create_user("vader", "darth@vader.com", self.password)
def test_update_user_info(self):
self.client.force_login(self.user)
response = self.client.get(reverse("web:change_password_component"))
assert response.status_code == 200
new_password = "ifnhatesandbro"
data = {
"old_password": self.password,
"new_password1": new_password,
"new_password2": new_password,
}
response = self.client.post(reverse("web:change_password_component"), data)
assert response.status_code == 200
self.user.refresh_from_db()
assert self.user.check_password(new_password)
+12 -7
View File
@@ -11,7 +11,7 @@ class TestCustomerAddresses(TestCase, CreateProductsMixin):
self.user = User.objects.create_user("vader", "darth@vader.com", "ihatesand")
self.customer_address = CustomerAddress.objects.create(
user=self.user,
full_name='Darth Vader',
full_name="Darth Vader",
address_type=CustomerAddress.Types.SHIPPING,
address="Vader Fortress",
address_town="Mustafar",
@@ -33,13 +33,18 @@ class TestCustomerAddresses(TestCase, CreateProductsMixin):
"address_zip": "00001",
}
response = self.client.post(reverse("web:update_customer_address", kwargs={"pk": self.customer_address.pk}), data)
response = self.client.post(
reverse(
"web:update_customer_address", kwargs={"pk": self.customer_address.pk}
),
data,
)
assert response.status_code == 200
self.customer_address.refresh_from_db()
assert self.customer_address.full_name == data.get('full_name')
assert self.customer_address.address == data.get('address')
assert self.customer_address.address_town == data.get('address_town')
assert self.customer_address.address_state == data.get('address_state')
assert self.customer_address.address_phone == data.get('address_phone')
assert self.customer_address.full_name == data.get("full_name")
assert self.customer_address.address == data.get("address")
assert self.customer_address.address_town == data.get("address_town")
assert self.customer_address.address_state == data.get("address_state")
assert self.customer_address.address_phone == data.get("address_phone")
+5 -5
View File
@@ -2,7 +2,6 @@ from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from shop.models import CustomerAddress
from shop.tests.mixins import CreateProductsMixin
@@ -13,12 +12,13 @@ class TestUserInfo(TestCase, CreateProductsMixin):
def test_update_user_info(self):
self.client.force_login(self.user)
response = self.client.get(reverse("web:user_info_component"))
assert response.status_code == 200
data = {
"first_name": "Anakin",
"last_name": "Skywalker",
"email": "anakin@skywalker.com"
"email": "anakin@skywalker.com",
}
response = self.client.post(reverse("web:user_info_component"), data)
@@ -26,6 +26,6 @@ class TestUserInfo(TestCase, CreateProductsMixin):
assert response.status_code == 200
self.user.refresh_from_db()
assert self.user.first_name == data.get('first_name')
assert self.user.last_name == data.get('last_name')
assert self.user.email == data.get('email')
assert self.user.first_name == data.get("first_name")
assert self.user.last_name == data.get("last_name")
assert self.user.email == data.get("email")
+6 -1
View File
@@ -11,7 +11,7 @@ from web.views.components import (
list_wishlisted_products,
update_customer_address,
user_info_component,
wishlist_button,
wishlist_button, change_password_component,
)
from web.views.users import (
login,
@@ -91,6 +91,11 @@ urlpatterns = [
login_required(user_info_component, login_url=_("/login/")),
name="user_info_component",
),
path(
"web/components/change-password/",
login_required(change_password_component, login_url=_("/login/")),
name="change_password_component",
),
# api
path("web/add-cart-item/", add_cart_item, name="add_cart_item"),
path("web/delete-cart-item/<int:pk>/", delete_cart_item, name="delete_cart_item"),
+31 -25
View File
@@ -1,9 +1,12 @@
from decimal import Decimal
from django.db import models
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views.generic import TemplateView
from config.mixins import HTMXFormComponent
from shop.filters import ProductFilter
from shop.models import (
CartItem,
@@ -13,10 +16,10 @@ from shop.models import (
ShippingMethod,
WishlistedProduct,
)
from users.forms.change_password import ChangePasswordForm
from users.forms.info import UserInfoForm
from web.forms import CustomerAddressForm
from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin
from web.models import WebSettings
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
from web.utils import get_or_create_cart
@@ -146,37 +149,39 @@ class WishlistButton(TemplateView):
}
class UserInfoComponentView(TemplateView):
template_name = "components/users/info.html"
class UserInfoFormComponentView(HTMXFormComponent):
form_class = UserInfoForm
hx_trigger = "updated_user_info"
def get_context_data(self, **kwargs):
web_settings = WebSettings.load()
user = self.request.user
form = self.form_class(
initial={
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
}
)
def get_object(self) -> models.Model:
return self.request.user
def get_view_name(self):
return reverse("web:user_info_component")
def get_initial_values(self, instance):
return {
"form": form,
"email": instance.email,
"first_name": instance.first_name,
"last_name": instance.last_name,
}
def post(self, request, *args, **kwargs):
user = self.request.user
form = self.form_class(request.POST)
if form.is_valid():
user.email = form.cleaned_data.get("email")
user.first_name = form.cleaned_data.get("first_name")
user.last_name = form.cleaned_data.get("last_name")
user.save()
return HttpResponse(status=200)
class ChangePasswordFormComponentView(HTMXFormComponent):
form_class = ChangePasswordForm
hx_trigger = "changed_password"
return render(request, self.template_name, {"form": form})
def get_form(self, initial=True):
if initial:
return self.form_class(user=self.request.user)
return self.form_class(self.request.user, self.request.POST)
def get_object(self) -> models.Model:
return self.request.user
def get_view_name(self):
return reverse("web:change_password_component")
class ListCustomerAddressComponent(TemplateView):
@@ -203,5 +208,6 @@ list_products = ListProducts.as_view()
list_wishlisted_products = ListWishlistedProducts.as_view()
wishlist_button = WishlistButton.as_view()
user_info_component = UserInfoComponentView.as_view()
user_info_component = UserInfoFormComponentView.as_view()
change_password_component = ChangePasswordFormComponentView.as_view()
list_customer_addresses_component = ListCustomerAddressComponent.as_view()