I've been using send_mail() for when someone creates a Group (model) or updates it, but I've been having trouble figuring out the function for deleting.
This works for when someone creates a Group:
class CreateGroup(CreateView):
model = Group
form_class = GroupForm
template_name = 'create_group.html'
def form_valid(self, form):
instance = form.save()
account = self.request.user.account
send_mail (
f"Thanks for creating {instance.name}",
'create group',
NOTIFICATION_EMAIL,
[account.user.email],
fail_silently=False
)
return HttpResponseRedirect(reverse('group_detail', args=[str(instance.pk)]))
class UpdateGroup(UpdateView):
model = Group
form_class = EditGroupForm
template_name = 'update_group.html'
def form_valid(self, form):
instance = form.save()
account = self.request.user.account
send_mail (
f"Thanks for updating {instance.name}",
'update group',
NOTIFICATION_EMAIL,
[account.user.email],
fail_silently=False
)
return HttpResponseRedirect(reverse('group_detail', args=[str(instance.pk)]))
That works, but how do I do it for DeleteView:
class DeleteGroup(DeleteView):
model = Group
template_name = 'delete_group.html'
success_url = reverse_lazy('home')
Is there something like form_valid() for DeleteView? Or does form_valid() work for those views as well if there's a form associated. I assume a view needs a form for form valid and I thought there wasn't a form since I'm just deleting a Group.
Is there something like form_valid() for DeleteView?
As of django-4.0, a DeleteView [Django-doc] uses the FormMixin [Django-doc], and thus has a .form_valid(…) method [Django-doc]. It works with a simple Form, that has no fields, and this is thus only used to simplify the logic, and will let Django validate the CSRF-token, and then delete the object.
Since django-4.0, you thus can use .form_valid(…):
# since Django 4.0
class DeleteGroup(DeleteView):
model = Group
template_name = 'delete_group.html'
success_url = reverse_lazy('home')
def form_valid(self, form):
send_mail (
f'Thanks for deleting{self.object}',
'delete group',
NOTIFICATION_EMAIL,
[self.request.user.email],
fail_silently=False
)
return super().form_valid(form)
Before django-4.0, you can override the .delete(…) method [Django-doc], and send the email, so:
# beforeDjango 4.0
from django.http import HttpResponseRedirect
class DeleteGroup(DeleteView):
model = Group
template_name = 'delete_group.html'
success_url = reverse_lazy('home')
def delete(self, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
send_mail (
f'Thanks for deleting{self.object}',
'delete group',
NOTIFICATION_EMAIL,
[self.request.user.email],
fail_silently=False
)
return HttpResponseRedirect(success_url)
For deleting you send a POST or DELETE request to the view.
Related
My code is:
views.py
class supplierListView(LoginRequiredMixin, ListView):
template_name = "supplier/Supplier_list.html"
def get_queryset(self):
organisation = self.request.user.userprofile.company
return Supplier.objects.filter(organisation=organisation)
class supplierCreateView(LoginRequiredMixin, CreateView):
template_name = "supplier/supplier_create.html"
form_class = SupplierModelForm
def get_success_url(self):
return reverse("supplier:supplier_list")
def form_valid(self, form):
supplier = form.save(commit=False)
supplier.organisation = self.request.user.userprofile.company
supplier.supplier_created_by = self.request.user
supplier.save()
my urls:
from awesomeinventory.supplier.views import (
supplierListView,
supplierDetailView,
supplierCreateView,
supplierContactListView,
supplierContactCreateView,
supplierContactDetailView,
)
app_name = "supplier"
urlpatterns = [
path("supplier_list/", view=supplierListView.as_view(), name="supplier_list"),
path("supplier_create/", view=supplierCreateView.as_view(), name="supplier_create"),
path("<int:pk>/detail/", view=supplierDetailView.as_view(), name="supplier_detail"),
path("<int:pk>/update/", view=supplierDetailView.as_view(), name="supplier_update"),
path("<int:pk>/delete/", view=supplierDetailView.as_view(), name="supplier_delete"),
path("supplierContact_list/", view=supplierContactListView.as_view(), name="supplierContact_list"),
path("<int:suppk>/supplierContact_create/", view=supplierContactCreateView.as_view(), name="supplierContact_create"), # int is supplier_id
path("<int:pk>/Contact/detail/", view=supplierContactDetailView.as_view(), name="supplierContact_detail"),
]
I m able to go to supplier:supplier_list page and it works well.
But when I want to create a supplier with supplierCreateView, supplier is create but it seems to have an issue with get_success_url as I have error
The view awesomeinventory.supplier.views.supplierCreateView didn't return an HttpResponse object. It returned None instead
The method form_valid is supposed to return the response or redirect the user. In your implementation you only save the object and return nothing essentially returning None which gives you an error. Instead of using form.save(commit=False) you can simply modify the instance wrapped by the form and leave all the processing to the super classes form_valid:
class supplierCreateView(LoginRequiredMixin, CreateView):
template_name = "supplier/supplier_create.html"
form_class = SupplierModelForm
def get_success_url(self):
return reverse("supplier:supplier_list")
def form_valid(self, form):
form.instance.organisation = self.request.user.userprofile.company
form.instance.supplier_created_by = self.request.user
return super().form_valid(form)
Note: A class name should ideally be in PascalCase so SupplierCreateView instead of supplierCreateView
This will work for you.
from django.urls import reverse_lazy
class supplierCreateView(LoginRequiredMixin, CreateView):
template_name = "supplier/supplier_create.html"
form_class = SupplierModelForm
def get_success_url(self):
# pay attention I'm using reverse_lazy instead of reverse
return reverse_lazy("supplier:supplier_list")
You can read more about reverse_lazy here.
I'm new to Django and having trouble redirecting after the AddContactEvent form has been filled out. After submitting the form, here is the redirect error:
No URL to redirect to. Either provide a url or define a
get_absolute_url method on the Model.
I am having trouble figuring out how to redirect it since the AddContactEvent url path('contacts/<int:pk1>/addcontactevent)
only has one pk. In the EventDetail url there are clearly two pk which would have the contact pk and the event pk. The EventDetail page seems to be creating, but I can't get it to redirect to that page due to multiple PK. how would you handle the redirect?
urls.py
path('contacts/<int:pk>', contact_detail.as_view(), name="contact_detail"),
path('contacts/<int:pk1>/addcontactevent', AddContactEvent.as_view(), name="addcontactevent"),
path('contacts/<int:pk1>/event/<int:pk2>/update', UpdateContactEvent.as_view(), name="updatecontactevent"),
path('contacts/<int:pk1>/event/<int:pk2>', EventDetail.as_view(), name="eventdetail"),
views.py
class AddContactEvent(CreateView):
form_class = ContactEventForm
template_name = 'crm/contactevent.html'
def dispatch(self, request, *args, **kwargs):
"""
Overridden so we can make sure the `Ipsum` instance exists
before going any further.
"""
self.contact = get_object_or_404(Contact, pk=kwargs['pk1'])
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
""" Save the form instance. """
contact = get_object_or_404(Contact, pk=self.kwargs['pk1'])
form.instance.contact = contact
form.instance.created_by = self.request.user
return super().form_valid(form)
class UpdateContactEvent(UpdateView):
model = Event
def get_object(self):
pk1 = self.kwargs['pk1']
pk2 = self.kwargs['pk2']
contact = get_object_or_404(Contact, pk=pk1)
event = get_object_or_404(Event, pk=pk2)
return event
class DeleteContactEvent(DeleteView):
model = Event
class EventDetail(DetailView):
template_name = 'crm/eventdetail.html'
model = Event
def get_object(self):
pk1 = self.kwargs['pk1']
pk2 = self.kwargs['pk2']
contact = get_object_or_404(Contact, pk=pk1)
event = get_object_or_404(Event, pk=pk2)
return event
one way to get rid of the error is to define a get absolute url in contact model
def get_absolute_url(self):
return reverse("contact_detail", kwargs={"pk": self.pk})
You have a saved Event object (which has a pk) and you have the contact pk
def get_success_url(self):
return reverse('eventdetail', kwargs={'pk1': self.kwargs['pk1'], 'pk2': self.object.pk})
I have used SuccessMessageMixin class but then also in the createview I did'nt get the success message but in the updateview it is working when I return the super().form_valid(form)
class DepartmentCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin ,CreateView):
template_name = 'departments/create_department.html'
form_class = DepartmentForm
success_url = reverse_lazy('departments:departments')
permission_required = ('departments.add_department',)
def form_valid(self, form):
department = form.save(commit=False)
department.created_by = self.request.user
department.updated_by = self.request.user
department.slug = slugify(uuid.uuid4())
department.save()
message = '[{"created": {}}]'
# retriving ContentType object
ct_obj = ContentType.objects.get(model='department')
# creating history object
history = History.objects.create(
action_time=timezone.now(),
action='created',
user=department.created_by,
content_type=ct_obj,
object_id=department.id,
change_message=message,
)
history.save()
return super().form_valid(form)
You haven't actually set a success message.
class DepartmentCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin ,CreateView):
success_message = "Department was created successfully"
...
Note, your form_valid is saving things twice. You should do:
def form_valid(self, form):
form.instance.slug = slugify(uuid.uuid4())
return super().form_valid(form)
Using inlineformset_factory I am able to add / remove phone numbers related to a single customer. Only problem is, I want to require at least 1 valid phone number for each customer.
Here is some demo code:
Models:
class Customer( models.Model ):
name = models.CharField( max_length=255 )
class PhoneNumber( models.Model ):
customer = models.ForeignKey( Customer )
number = models.CharField( max_length=10 )
Forms:
class CustomerForm( ModelForm ):
class Meta:
model = Customer
fields = ['name']
class PhoneNumberForm( ModelForm ):
class Meta:
model = PhoneNumber
fields = ['number']
Ok, so that's pretty straight forward.
Then in my view:
class Create( View ):
template_name = 'path_to_template'
CustomerForm = forms.CustomerForm
PhoneNumberFormSet = inlineformset_factory (
parent_model = Customer,
model = PhoneNumber,
form = PhoneNumberForm,
extra = 1,
)
def get(self, request):
# Return empty forms
context = {
'customer_form': self.CustomerForm,
'phone_number_formset': self.PhoneNumberFormSet
}
render( request, self.template_name, context)
def post(self, request):
this_customer_form = self.CustomerForm( request.POST )
if this_customer_form.is_valid():
new_customer.save(commit=False)
this_phone_number_formset = self.PhoneNumberFormSet(request.POST, instance=new_customer)
if this_phone_number_formset.is_valid():
new_customer.save()
this_phone_number_formset.save()
return HttpResponseRedirect(reverse_lazy('customer-detail', kwargs={'pk': new_customer.pk}))
# Something is not right, show the forms again
this_phone_number_formset = self.PhoneNumberFormSet(request.POST)
context = {
'customer_form': this_customer_form,
'phone_number_formset': this_phone_number_formset
}
render( request, self.template_name, context)
You get the point I think. Same thing for the Edit/Update view of the customer. Only then the forms are prepopulated.
At this point all I need is a way to require at least 1 valid PhoneNumber per Customer.
I found something like:
class RequiredFormSet(BaseFormSet):
def __init__(self, *args, **kwargs):
super(RequiredFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False
from https://stackoverflow.com/questions/2406537/django-formsets-make-first-required
but it doesnt seem to work when I apply this on a BaseInlineFormSet class.
Django 1.7 seems to answer my wishes, but not for a InlineModelFormSet so far..
Any ideas?
If you just want to set the minimum or maximum, you can set them directly in inlineformset_factory, here's my code for minimum of one entry
from django.forms import inlineformset_factory
SubUnitFormSet = inlineformset_factory(
Unit, SubUnit, form=SubUnitForm, min_num=1, validate_min=True, extra=0)
You need to properly handle this in your view. I'm using CBV and this is my code for your reference
class UnitCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
permission_required = "core.add_unit"
model = Unit
form_class = UnitForm
template_name = 'core/basic-info/unit_form.html'
success_url = reverse_lazy('core:units')
success_message = _("%(code)s was added successfully")
def get_context_data(self, **kwargs):
data = super(UnitCreateView, self).get_context_data(**kwargs)
if self.request.POST:
data['subunits'] = SubUnitFormSet(self.request.POST, )
else:
data['subunits'] = SubUnitFormSet()
return data
def form_valid(self, form):
context = self.get_context_data()
subunits = context['subunits']
with transaction.atomic():
if subunits.is_valid():
self.object = form.save()
subunits.instance = self.object
subunits.save()
else:
return self.render_to_response(self.get_context_data(form=form))
return super(UnitCreateView, self).form_valid(form)
Thank you kezabella ( django irc ).
Seems I found a solution by subclassing BaseInlineFormset:
class RequiredFormSet(BaseInlineFormSet):
def clean(self):
for form in self.initial_forms:
if not form.is_valid() or not (self.can_delete and form.cleaned_data.get('DELETE')):
return
for form in self.extra_forms:
if form.has_changed():
return
raise ValidationError("No initial or changed extra forms")
Btw, these validation errors do not show up in {{ formset.error }} but in:
{{ formset.non_form_errors }}
I am looking for a simple answer by example to this common problem. The answers I found so far leave out critical points for us beginners.
I have an app where almost every model has a ForeignKey to User, and there is a unique_together constraint, where one of the fields is always 'user'.
For example:
class SubscriberList(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=70)
date_created = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = (
('user', 'name',),
)
def __unicode__(self):
return self.name
A SubscriberList is always created by a logged in User, and thus in the form to create a Subscriber List, I exclude the user field and give it a value of self.request.user when saving the form, like so:
class SubscriberListCreateView(AuthCreateView):
model = SubscriberList
template_name = "forms/app.html"
form_class = SubscriberListForm
success_url = "/app/lists/"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
return super(SubscriberListCreateView, self).form_valid(form)
And here is the accompanying form:
class SubscriberListForm(ModelForm):
class Meta:
model = SubscriberList
exclude = ('user')
With this code, valid data is fine. When I submit data that is not unique_together, I get an Integrity Error from the database. The reason is clear to me - Django doesn't validate the unique_together because the 'user' field is excluded.
How do I change my existing code, still using CreateView, so that submitted data that is not unique_together throws a form validation error, and not an Integrity Error from the db.
Yehonatan's example got me there, but I had to call the messages from within the ValidationError of form_valid, rather than a separate form_invalid function.
This works:
class SubscriberCreateView(AuthCreateView):
model = Subscriber
template_name = "forms/app.html"
form_class = SubscriberForm
success_url = "/app/subscribers/"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
try:
self.object.full_clean()
except ValidationError:
#raise ValidationError("No can do, you have used this name before!")
#return self.form_invalid(form)
from django.forms.util import ErrorList
form._errors["email"] = ErrorList([u"You already have an email with that name man."])
return super(SubscriberCreateView, self).form_invalid(form)
return super(SubscriberCreateView, self).form_valid(form)
Taking from the docs at:
https://docs.djangoproject.com/en/dev/ref/models/instances/?from=olddocs#validating-objects
You should only need to call a model’s full_clean() method if you plan to handle validation errors yourself, or if you have excluded fields from the ModelForm that require validation.
Taking from the docs at:
https://docs.djangoproject.com/en/dev/ref/class-based-views/#formmixin
Views mixing FormMixin must provide an implementation of form_valid() and form_invalid().
This means that in order to view the error (which isn't form related) you'll need to implement your own form_invalid, add the special error message there, and return it.
So, running a full_clean() on your object should raise the unique_together error, so your code could look like this:
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
# validate unique_together constraint
try:
self.object.full_clean()
except ValidationError:
# here you can return the same view with error messages
# e.g.
return self.form_invalid(form)
return super(SubscriberListCreateView, self).form_valid(form)
def form_invalid(self, form):
# using messages
# from django.contrib import messages
# messages.error('You already have a list with that name')
# or adding a custom error
from django.forms.util import ErrorList
form._errors["name"] = ErrorList([u"You already have a list with that name"])
return super(SubscriberListCreateView, self).form_invalid(form)
HTH
adding another example that might be a bit easier for noobs.
forms.py
class GroupItemForm(ModelForm):
def form_valid(self):
self.object = self.save(commit=False)
try:
self.object.full_clean()
except ValidationError:
# here you can return the same view with error messages
# e.g. field level error or...
self._errors["sku"] = self.error_class([u"You already have an email with that name."])
# ... form level error
self.errors['__all__'] = self.error_class(["error msg"]
return False
return True
views.py
def add_stock_item_detail(request, item_id, form_class=GroupItemForm, template_name="myapp/mytemplate.html"):
item = get_object_or_404(Item, pk=item_id)
product = Product(item=item)
if request.method == 'POST':
form = form_class(request.POST, instance=product)
if form.is_valid() and form.form_valid():
form.save()
return HttpResponseRedirect('someurl')
else:
form = form_class(instance=product)
ctx.update({
"form" : form,
})
return render_to_response(template_name, RequestContext(request, ctx))