I have 2 models - for example, Book and Page.
Page has a foreign key to Book.
Each page can be marked as "was_read" (boolean), and I want to prevent deleting pages that were read (in the admin).
In the admin - Page is an inline within Book (I don't want Page to be a standalone model in the admin).
My problem - how can I achieve the behavior that a page that was read won't be deleted?
I'm using Django 1.4 and I tried several options:
Override "delete" to throw a ValidationError - the problem is that the admin doesn't "catch" the ValidationError on delete and you get an error page, so this is not a good option.
Override in the PageAdminInline the method - has_delete_permission - the problem here -it's per type so either I allow to delete all pages or I don't.
Are there any other good options without overriding the html code?
Thanks,
Li
The solution is as follows (no HTML code is required):
In admin file, define the following:
from django.forms.models import BaseInlineFormSet
class PageFormSet(BaseInlineFormSet):
def clean(self):
super(PageFormSet, self).clean()
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
data = form.cleaned_data
curr_instance = form.instance
was_read = curr_instance.was_read
if (data.get('DELETE') and was_read):
raise ValidationError('Error')
class PageInline(admin.TabularInline):
model = Page
formset = PageFormSet
You could disable the delete checkbox UI-wise by creating your own custom
formset for the inline model, and set can_delete to False there. For
example:
from django.forms import models
from django.contrib import admin
class MyInline(models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInline, self).__init__(*args, **kwargs)
self.can_delete = False
class InlineOptions(admin.StackedInline):
model = InlineModel
formset = MyInline
class MainOptions(admin.ModelAdmin):
model = MainModel
inlines = [InlineOptions]
Another technique is to disable the DELETE checkbox.
This solution has the benefit of giving visual feedback to the user because she will see a grayed-out checkbox.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
if some_criteria_to_prevent_deletion:
form.fields['DELETE'].disabled = True
This code leverages the Field.disabled property added in Django 1.9. As the documentation says, "even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data," so you don't need to add more code to prevent deletion.
In your inline, you can add the flag can_delete=False
EG:
class MyInline(admin.TabularInline):
model = models.mymodel
can_delete = False
I found a very easy solution to quietly avoid unwanted deletion of some inlines. You can just override delete_forms property method.
This works not just on admin, but on regular inlines too.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
#property
def deleted_forms(self):
deleted_forms = super(MyInlineFormSet, self).deleted_forms
for i, form in enumerate(deleted_forms):
# Use form.instance to access object instance if needed
if some_criteria_to_prevent_deletion:
deleted_forms.pop(i)
return deleted_forms
Related
I have a request from a client to have page admin fields that they can add/read numbers into with commas such as 1,000,000.
The Django model field to store the value would be a django.db.models.fields.DecimalField instance.
From looking at the Django docs, this is something that’s supported by the django.forms.fields.DecimalField localized property, but I can’t find a way of enforcing it in the Wagtail admin, even when subclassing the Wagtail BaseFieldPanel __init__ function with self.bound_field.field.localize = True.
You can override the fields that Wagtail FieldPanel uses by customising generated forms documented here:
http://docs.wagtail.io/en/v1.13/advanced_topics/customisation/page_editing_interface.html#wagtail.wagtailadmin.forms.WagtailAdminPageForm
Basic example below - myapp/models.py
from django import forms
from django.db import models
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
from wagtail.wagtailcore.models import Page
class MyCustomPageForm(WagtailAdminPageForm):
# fields here are django.forms fields
# when set to localize, renders as a TextInput widget
total_amount = forms.DecimalField(localize=True)
# when left with defaults, renders as a NumberInput widget
# total_amount = forms.DecimalField()
# can also set any kind of widget here
# total_amount = forms.DecimalField(widget=MyCustomDecimalWidget)
class MyCustomPage(Page):
# fields here are django.db.models fields
total_amount = models.DecimalField()
content_panels = Page.content_panels + [
FieldPanel('total_amount'),
]
base_form_class = MyCustomPageForm # important: must set this
When you set the form field to localize=True it appears to follow the intended behaviour as per the Django Docs which is to render a TextInput widget.
You could also set your own widget for any field in your page's form class if you want to do some more complicated functionality.
Thanks #lb-ben-johnston - also to the Wagtail team who pointed me in a similar direction.
I have it working now in a loosely coupled fashion with this:
from django.forms.fields import DecimalField
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
class ProductPageForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
super(ProductPageForm, self).__init__(*args, **kwargs)
for key, field in self.fields.items():
if isinstance(field, DecimalField):
field.localize = True
field.widget.is_localized = True
# The Page class to localize
class LocalizedPage(Page):
base_form_class = ProductPageForm
# Rest of page class
Make sure you explictly pass a TextInput widget to the field handler:
FieldPanel('localized_field', widget=TextInput)
Finally, make sure that the thousand separator flag is explicitly set in your settings.py or base.py:
USE_THOUSAND_SEPARATOR = True
I'm becoming increasingly bewildered by the range of answers on offer to the seemingly simple problem of adding custom fields to the django-registration register form/flow. This should be a default, documented aspect of the package (not to sound ungrateful, just that it is such a well-equipped package), but solutions to the problem are dizzying.
Can anyone give me the most simple solution to getting UserProfile model data included in the default registration register page?
Update:
I eventually used Django Registration's own signals to give me this hacky fix. It is particularly ugly because, I had to use try on the POST attribute dealing with my Boolean since I found that the checkbox returned nothing if left empty.
Would appreciate any advice on improving this, or best practice.
My app / models.py
from registration.signals import user_registered
from django.dispatch import receiver
class UserProfile(models.Model):
user = models.OneToOneField(User)
event_commitments = models.ManyToManyField(Event, null=True, blank=True)
receive_email = models.BooleanField(default=True)
#receiver(user_registered)
def registration_active_receive_email(sender, user, request, **kwargs):
user_id = user.userprofile.id
user = UserProfile.objects.get(pk=user_id)
try:
if request.POST['receive_email']:
pass
except:
user.receive_email = False
user.save()
Registration app / forms.py
class RegistrationForm(forms.Form):
# default fields here, followed by my custom field below
receive_email = forms.BooleanField(initial=True, required=False)
Thanks
What you have looks like a workable approach.
I've looked through the django-registration code, and based on the following comments in the register view I've come up with another solution. I'm not totally sure this is cleaner, but if you aren't a fan of signals this is good. This also provides a much easier avenue if you intend to make more customizations.
# from registration.views.register:
"""
...
2. The form to use for account registration will be obtained by
calling the backend's ``get_form_class()`` method, passing the
``HttpRequest``. To override this, see the list of optional
arguments for this view (below).
3. If valid, the form's ``cleaned_data`` will be passed (as
keyword arguments, and along with the ``HttpRequest``) to the
backend's ``register()`` method, which should return the new
``User`` object.
...
"""
You could create a custom backend and override those mentioned methods:
# extend the provided form to get those fields and the validation for free
class CustomRegistrationForm(registration.forms.RegistrationForm):
receive_email = forms.BooleanField(initial=True, required=False)
# again, extend the default backend to get most of the functionality for free
class RegistrationBackend(registration.backends.default.DefaultBackend):
# provide your custom form to the registration view
def get_form_class(self, request):
return CustomRegistrationForm
# replace what you're doing in the signal handler here
def register(self, request, **kwargs):
new_user = super(RegistrationBackend, self).register(request, **kwargs)
# do your profile stuff here
# the form's cleaned_data is available as kwargs to this method
profile = new_user.userprofile
# use .get as a more concise alternative to try/except around [] access
profile.receive_email = kwargs.get('receive_email', False)
profile.save()
return new_user
To use the custom backend, you can then provide separate urls. Before including the default urls, write 2 confs that point at your custom backend. Urls are tested in the order defined, so if you define these two before including the defaults, these two will capture before the default ones are tested.
url(r'^accounts/activate/(?P<activation_key>\w+)/$',
activate,
{'backend': 'my.app.RegistrationBackend'},
name='registration_activate'),
url(r'^accounts/register/$',
register,
{'backend': 'my.app.RegistrationBackend'},
name='registration_register'),
url(r'^accounts/', include('registration.backends.default.urls')),
The docs actually describe all this, but they aren't particularly accessible (no readthedocs). They are all included in the project, and I was browsing them here.
I eventually used Django Registration's own signals to give me this fix.
I will clean up the try/except flow at some point. dokkaebi also points out above that I might be able to assess the request.GET parameters for when a checkbox is left empty.
My app / models.py
from registration.signals import user_registered
from django.dispatch import receiver
class UserProfile(models.Model):
user = models.OneToOneField(User)
event_commitments = models.ManyToManyField(Event, null=True, blank=True)
receive_email = models.BooleanField(default=True)
#receiver(user_registered)
def registration_active_receive_email(sender, user, request, **kwargs):
user_id = user.userprofile.id
user = UserProfile.objects.get(pk=user_id)
try:
if request.POST['receive_email']:
pass
except:
user.receive_email = False
user.save()
Registration app / forms.py
class RegistrationForm(forms.Form):
# default fields here, followed by my custom field below
receive_email = forms.BooleanField(initial=True, required=False)
For example.
class One(models.Model):
text=models.CharField(max_length=100)
class Two(models.Model):
test = models.Integer()
many = models.ManyToManyField(One, blank=True)
When I try save my object in admin panel, I take error such as:
"'Two' instance needs to have a primary key value before a many-to-many relationship can be used."
I use django 1.3. I tried add AutoField to Two class, but it's not work too.
This is my code.
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response, redirect
from django.template import RequestContext
from django.core.urlresolvers import reverse
from project.foo.forms import FooForm
from project.foo.models import Foo
from project.fooTwo.views import fooTwoView
def foo(request, template_name="foo_form.html"):
if request.method == 'POST':
form = FooForm(data=request.POST)
if form.is_valid():
foo = Foo()
foo.name = request.POST.get("name")
foo.count_people = request.POST.get("count_people")
foo.date_time = request.POST.get("date_time")
foo.save()
return fooTwoView(request)
else:
form = FooForm()
return render_to_response(template_name, RequestContext(request, {
"form": form,
}))
P.S. I find my fail. It is in model. I used many-to-many in save method. I add checking before using, but it's not help.
class Foo(models.Model):
name = models.CharField(max_length=100, null=False, blank=False)
count_people = models.PositiveSmallIntegerField()
menu = models.ManyToManyField(Product, blank=True, null=True)
count_people = models.Integer()
full_cost = models.IntegerField(blank=True)
def save(self, *args, **kwargs):
if(hasattr(self,'menu')):
self.full_cost = self.calculate_full_cost()
super(Foo, self).save(*args, **kwargs)
def calculate_full_cost(self):
cost_from_products = sum([product.price for product in self.menu.all()])
percent = cost_from_products * 0.1
return cost_from_products + percent
I try hack in save method such as
if(hasattr(self,Two)):
self.full_cost = self.calculate_full_cost()
This is help me, but i dont think that is the django way. What is interesting, that is without this checking admin panel show error, but create object. Now, if i select item from Two and save, my object does not have full_cost, but when i view my object, admin panel remember my choice and show me my Two item, what i select... I dont know why.
How do i save this?
There are quite a few problems with your code. The most obvious one are
1/ in your view, using a form for user inputs validation/sanitization/conversion then ignoring the santized/converted data and getting unsanitized inputs directly from the request. Use form.cleaned_data instead of request.POST to get your data, or even better use a ModelForm which will take care of creating a fully populated Foo instance for you.
2/ there's NO implicit "this" (or "self" or whatever) pointer in Python methods, you have to explicitely use "self" to get at the instance attributes. Here's what your model's "save" method really do:
def save(self, *args, **kwargs):
# test the truth value of the builtin "id" function
if(id):
# create a local variable "full_cost"
full_cost = self.calculate_full_cost()
# call on super with a wrong base class
super(Banquet, self).save(*args, **kwargs)
# and exit, discarding the value of "full_cost"
Now with regard to your question: Foo.save is obviously not the right place to compute someting based on m2m related objects. Either write a distinct method that run the computation AND update Foo AND save it and call it after the m2m are saved (hint : a ModelForm will take care of saveing the m2m related objects for you), or just use the m2m_changed signal.
This being said, I strongly suggest you spend a few hours learning Python and Django - it will save you a lot of time.
Why not use "OneToOneField" instead of Many-to-Many
I'm trying to store the username from the current request's session into a db object. How can I do this from within a class-based view? Is there a "clean" way to do this? What should I override/subclass?
I have a model that looks like this:
from django.contrib.auth.models import User
class Entry(django.db.models.Model):
...
author = models.ForeignKey(User, editable=False)
I also have a view based on the built-in generic view django.views.generic.CreateView. I'm also using the default ModelForm class that goes with my model, and the default {{ form }} in my template. AFAIK, the session and authentication apps/middleware are set up properly---as per default in new Django projects.
I found this post, which is getting at about the same thing, but from the wrong angle, and using function views instead.
My thinking so far was to override something in the form class and insert the username into the cleaned data. Is there a better way? Is there a right way?
Edit: Solution so far, non-working, with an IntegrityError: author_id cannot be null
from django.views.generic import CreateView
class Index(CreateView):
model = magicModel
template_name = "index.html"
success_url = "/magicWorked"
...
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.author = request.user
return super(Index, self).form_valid(form)
I wrote this based on what I found in django/views/generic/edit.py, which uses this implementation for class ModelFormMixin:
def form_valid(self, form):
self.object = form.save()
return super(ModelFormMixin, self).form_valid(form)
This is the method called by super().form_valid() above.
Edit: The problem with my solution was my understanding of Python's inheritance model. When the super-class calls form_valid(), it calls its own version, not my override; my code was never running at all.
The "correct" way to do this is to write your own view for object creation if the generic view doesn't suffice. Creation views are relatively short and there are numerous examples of how to save foreign keys.
Incidentally, Django's 1.3 docs say somewhere in there that modifications to the authentication model used by the admin app are being "discussed," such as adding per-instance permissions. (The current auth model supports only per model permissions.) The dev's might also add an implementation for what I'm trying to achieve. After all, user-associated data is used by nearly all websites.
I import
from django.contrib.sites.models import Site in models.py file.
In have this following in admin.py file:
class SitesAdmin(admin.ModelAdmin):
pass
admin.site.unregister(Site)
admin.site.register(Site, SitesAdmin)**
I want to attach validation to the site.domain field in admin.py, How i can accomplish this? please help.
First, specifying an empty ModelAdmin class is unnecessary, the following will work if you don't need to customize the admin:
admin.site.register(Site) # Notice that no ModelAdmin is passed
Now, to your question. You have to create a custom form. Then, you override the clean_domain method of the ModelForm. You can validate any field with the method(s) clean_FOO, where FOO is the field name.
from django import forms
class SiteAdminForm(forms.ModelForm):
def clean_domain(self):
domain = self.cleaned_data.get('domain')
# Custom validation here
return domain
class SiteAdmin(admin.ModelAdmin):
form = SiteAdminForm
admin.site.unregister(Site)
admin.site.register(Site, SiteAdmin)