Django class-based views for service layer pattern
Project description
Django Service Views
Django class-based views that support the service layer pattern by preventing automatic form.save() calls.
Why?
Django's generic class-based views automatically call form.save() in form_valid(), which bypasses your service layer. This package provides views that skip the automatic save, allowing you to handle all data operations through your service layer.
Installation
# With pip
pip install django-service-views
# With uv
uv add django-service-views
Quick Start
Basic Usage
from django_service_views import ServiceUpdateView
from myapp.services import product_service
class ProductUpdateView(ServiceUpdateView):
model = Product
fields = ['name', 'price', 'description']
template_name = 'products/update.html'
success_url = '/products/'
def form_valid(self, form):
# Call your service layer instead of form.save()
product_service.update_product(
self.object,
**form.cleaned_data
)
return super().form_valid(form)
Create View
from django_service_views import ServiceCreateView
class ProductCreateView(ServiceCreateView):
model = Product
fields = ['name', 'price', 'description']
template_name = 'products/create.html'
def form_valid(self, form):
# Service layer handles the creation
self.object = product_service.create_product(
user=self.request.user,
**form.cleaned_data
)
return super().form_valid(form)
Delete View
from django_service_views import ServiceDeleteView
from django.urls import reverse_lazy
class ProductDeleteView(ServiceDeleteView):
model = Product
template_name = 'products/delete_confirm.html'
success_url = reverse_lazy('product_list')
def delete_with_service(self, obj):
# Service layer handles the deletion
product_service.delete_product(obj, user=self.request.user)
Views Provided
ServiceModelFormMixin
Base mixin that prevents automatic form.save(). Can be used with any generic view that uses ModelFormMixin.
Important: Must be placed immediately before the generic view in the inheritance order:
class MyView(ServiceModelFormMixin, UpdateView): # ✓ Correct
pass
class MyView(LoginRequiredMixin, ServiceModelFormMixin, UpdateView): # ✓ Correct
pass
class MyView(UpdateView, ServiceModelFormMixin): # ✗ Won't work
pass
ServiceUpdateView
Inherits from ServiceModelFormMixin and Django's UpdateView. Override form_valid() to call your service layer.
ServiceCreateView
Inherits from ServiceModelFormMixin and Django's CreateView. Override form_valid() to call your service layer. Don't forget to set self.object to the created instance.
ServiceDeleteView
Inherits from ServiceModelFormMixin and Django's DeleteView. Must implement delete_with_service(obj) method.
Complete Example with Service Layer
# services.py
from django.db import transaction
class ProductService:
@transaction.atomic
def create_product(self, user, **data):
product = Product.objects.create(**data)
self.log_creation(product, user)
self.notify_admins(product)
return product
@transaction.atomic
def update_product(self, product, **data):
for field, value in data.items():
setattr(product, field, value)
product.save()
self.log_update(product)
return product
def delete_product(self, product, user):
self.log_deletion(product, user)
product.delete()
product_service = ProductService()
# views.py
from django_service_views import (
ServiceCreateView,
ServiceUpdateView,
ServiceDeleteView
)
from .services import product_service
class ProductCreateView(ServiceCreateView):
model = Product
fields = ['name', 'price', 'description']
def form_valid(self, form):
self.object = product_service.create_product(
user=self.request.user,
**form.cleaned_data
)
messages.success(self.request, f"Product {self.object.name} created!")
return super().form_valid(form)
class ProductUpdateView(ServiceUpdateView):
model = Product
fields = ['name', 'price', 'description']
def form_valid(self, form):
product_service.update_product(
self.object,
**form.cleaned_data
)
messages.success(self.request, "Product updated!")
return super().form_valid(form)
class ProductDeleteView(ServiceDeleteView):
model = Product
success_url = reverse_lazy('product_list')
def delete_with_service(self, obj):
product_service.delete_product(obj, user=self.request.user)
messages.success(self.request, "Product deleted!")
Benefits
- Separation of Concerns: Keep business logic in service layer, not in views
- Testability: Service layer can be tested independently of views
- Reusability: Same service methods can be used from views, management commands, API endpoints, etc.
- Transaction Management: Handle transactions in service layer where it belongs
- Consistency: All data operations go through the same service layer code
Requirements
- Python >= 3.8
- Django >= 4.2
License
MIT
Contributing
Issues and pull requests are welcome at https://github.com/yourusername/django-service-views
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_service_views-0.1.0.tar.gz.
File metadata
- Download URL: django_service_views-0.1.0.tar.gz
- Upload date:
- Size: 5.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4255af6451be4d57bb0383f6c91f9f5acf69e3d1736e22206b326f23b174ab93
|
|
| MD5 |
1e187dc98e98ca74336883063350fd9c
|
|
| BLAKE2b-256 |
456220b0ca1c7c1105d61a430492085a06f28ca92b6debd90db028cb81c5770d
|
File details
Details for the file django_service_views-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_service_views-0.1.0-py3-none-any.whl
- Upload date:
- Size: 5.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f58295bf3a3b43a2e783a1d64881373854c4558e6a573e6336207d4581ad7de
|
|
| MD5 |
69aec77c1310b1bebb47bbf364e6d2de
|
|
| BLAKE2b-256 |
0f37e0f9bd0e7555786e2dddeef04d08050644fa4ed35591cdd9498729ae5ea1
|