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"), "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: class Meta:
model = User model = User
+1 -50
View File
@@ -1,9 +1,5 @@
from django.conf import settings from django.conf import settings
from django.core.paginator import Paginator 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: class FilteredQuerysetMixin:
@@ -62,11 +58,7 @@ class StylingMixin:
for field_name in self.styled_fields: for field_name in self.styled_fields:
field = self.fields[field_name] field = self.fields[field_name]
field.required = True field.required = True
field.widget.attrs.update( field.widget.attrs.update({"class": " ".join(self.classes)})
{
"class": ' '.join(self.classes)
}
)
if self.placeholder_for_field.get(field_name): if self.placeholder_for_field.get(field_name):
field.widget.attrs.update( field.widget.attrs.update(
@@ -74,44 +66,3 @@ class StylingMixin:
"placeholder": self.placeholder_for_field.get(field_name), "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 i18n %}
{% load static %} {% load static %}
<form hx-post="{% url 'web:user_info_component' %}" hx-swap="none"> <form hx-post="{{ url }}" hx-swap="none">
{% csrf_token %} {% csrf_token %}
{% for field in form %} {% for field in form %}
<div class="mb-4"> <div class="mb-4">
<label for="{{ field.id_for_label }}"
<label for="{{ field.id_for_label }}" class="inline-block text-sm font-medium text-gray-900 dark:text-white ml-2">
class="inline-block text-sm font-medium text-gray-900 dark:text-white ml-2"> {{ field.label }}
{{ field.label }} </label>
</label> {{ field }}
{{ field }}
</div> </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> <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 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" dark:hover:bg-primary-700 dark:focus:ring-primary-800"
> >
{% translate 'Guardar' %} {% translate 'Guardar' %}
</button> </button>
</form> </form>
@@ -2,11 +2,11 @@
{% load static %} {% load static %}
{% for address in customer_addresses %} {% for address in customer_addresses %}
<form <form
id="edit_address_{{ address.pk }}" id="edit_address_{{ address.pk }}"
class="hidden py-2 md:py-4" class="hidden py-2 md:py-4"
hx-post="{% url 'web:update_customer_address' pk=address.pk %}" 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-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" hx-swap="none"
> >
{% csrf_token %} {% csrf_token %}
<div class="mb-4 grid gap-4 sm:grid-cols-2 sm:gap-8 lg:gap-16"> <div class="mb-4 grid gap-4 sm:grid-cols-2 sm:gap-8 lg:gap-16">
@@ -159,14 +159,8 @@
{% translate 'Editar datos' %} {% translate 'Editar datos' %}
</button> </button>
</div> </div>
</div> </div>
{% if not forloop.last %} {% if not forloop.last %}
<hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-700"> <hr class="h-px my-2 bg-gray-200 border-0 dark:bg-gray-700">
{% endif %} {% endif %}
{% endfor %} {% endfor %}
+9 -2
View File
@@ -10,8 +10,15 @@
{% translate 'Mi cuenta' %} {% translate 'Mi cuenta' %}
</h2> </h2>
<div <div
class="mt-4 p-6 border border-gray-100 rounded-md dark:border-gray-600" 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" 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> </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.user = User.objects.create_user("vader", "darth@vader.com", "ihatesand")
self.customer_address = CustomerAddress.objects.create( self.customer_address = CustomerAddress.objects.create(
user=self.user, user=self.user,
full_name='Darth Vader', full_name="Darth Vader",
address_type=CustomerAddress.Types.SHIPPING, address_type=CustomerAddress.Types.SHIPPING,
address="Vader Fortress", address="Vader Fortress",
address_town="Mustafar", address_town="Mustafar",
@@ -33,13 +33,18 @@ class TestCustomerAddresses(TestCase, CreateProductsMixin):
"address_zip": "00001", "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 assert response.status_code == 200
self.customer_address.refresh_from_db() self.customer_address.refresh_from_db()
assert self.customer_address.full_name == data.get('full_name') assert self.customer_address.full_name == data.get("full_name")
assert self.customer_address.address == data.get('address') assert self.customer_address.address == data.get("address")
assert self.customer_address.address_town == data.get('address_town') 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_state == data.get("address_state")
assert self.customer_address.address_phone == data.get('address_phone') 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.test import TestCase
from django.urls import reverse from django.urls import reverse
from shop.models import CustomerAddress
from shop.tests.mixins import CreateProductsMixin from shop.tests.mixins import CreateProductsMixin
@@ -13,12 +12,13 @@ class TestUserInfo(TestCase, CreateProductsMixin):
def test_update_user_info(self): def test_update_user_info(self):
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:user_info_component")) response = self.client.get(reverse("web:user_info_component"))
assert response.status_code == 200 assert response.status_code == 200
data = { data = {
"first_name": "Anakin", "first_name": "Anakin",
"last_name": "Skywalker", "last_name": "Skywalker",
"email": "anakin@skywalker.com" "email": "anakin@skywalker.com",
} }
response = self.client.post(reverse("web:user_info_component"), data) response = self.client.post(reverse("web:user_info_component"), data)
@@ -26,6 +26,6 @@ class TestUserInfo(TestCase, CreateProductsMixin):
assert response.status_code == 200 assert response.status_code == 200
self.user.refresh_from_db() self.user.refresh_from_db()
assert self.user.first_name == data.get('first_name') assert self.user.first_name == data.get("first_name")
assert self.user.last_name == data.get('last_name') assert self.user.last_name == data.get("last_name")
assert self.user.email == data.get('email') assert self.user.email == data.get("email")
+6 -1
View File
@@ -11,7 +11,7 @@ from web.views.components import (
list_wishlisted_products, list_wishlisted_products,
update_customer_address, update_customer_address,
user_info_component, user_info_component,
wishlist_button, wishlist_button, change_password_component,
) )
from web.views.users import ( from web.views.users import (
login, login,
@@ -91,6 +91,11 @@ urlpatterns = [
login_required(user_info_component, login_url=_("/login/")), login_required(user_info_component, login_url=_("/login/")),
name="user_info_component", name="user_info_component",
), ),
path(
"web/components/change-password/",
login_required(change_password_component, login_url=_("/login/")),
name="change_password_component",
),
# api # api
path("web/add-cart-item/", add_cart_item, name="add_cart_item"), 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"), 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 decimal import Decimal
from django.db import models
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views.generic import TemplateView from django.views.generic import TemplateView
from config.mixins import HTMXFormComponent
from shop.filters import ProductFilter from shop.filters import ProductFilter
from shop.models import ( from shop.models import (
CartItem, CartItem,
@@ -13,10 +16,10 @@ from shop.models import (
ShippingMethod, ShippingMethod,
WishlistedProduct, WishlistedProduct,
) )
from users.forms.change_password import ChangePasswordForm
from users.forms.info import UserInfoForm from users.forms.info import UserInfoForm
from web.forms import CustomerAddressForm from web.forms import CustomerAddressForm
from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin
from web.models import WebSettings
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
from web.utils import get_or_create_cart from web.utils import get_or_create_cart
@@ -146,37 +149,39 @@ class WishlistButton(TemplateView):
} }
class UserInfoComponentView(TemplateView): class UserInfoFormComponentView(HTMXFormComponent):
template_name = "components/users/info.html"
form_class = UserInfoForm form_class = UserInfoForm
hx_trigger = "updated_user_info"
def get_context_data(self, **kwargs): def get_object(self) -> models.Model:
web_settings = WebSettings.load() return self.request.user
user = self.request.user
form = self.form_class(
initial={
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
}
)
def get_view_name(self):
return reverse("web:user_info_component")
def get_initial_values(self, instance):
return { 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(): class ChangePasswordFormComponentView(HTMXFormComponent):
user.email = form.cleaned_data.get("email") form_class = ChangePasswordForm
user.first_name = form.cleaned_data.get("first_name") hx_trigger = "changed_password"
user.last_name = form.cleaned_data.get("last_name")
user.save()
return HttpResponse(status=200)
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): class ListCustomerAddressComponent(TemplateView):
@@ -203,5 +208,6 @@ list_products = ListProducts.as_view()
list_wishlisted_products = ListWishlistedProducts.as_view() list_wishlisted_products = ListWishlistedProducts.as_view()
wishlist_button = WishlistButton.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() list_customer_addresses_component = ListCustomerAddressComponent.as_view()