I have a similar issue to the post here in regards to setting up the receiver function for the paypal-ipn
What we are trying to do(I don't know this other person but I assume we stand together on this topic) is to understand how to deduce a path to receiving a paypal IPN signal in which we then can update the django database.
I have implemented the django-paypal API by following the directions here
Overall what I do is I create a view in my views.py as follows
def payment_page(request):
"""This fucntion returns payment page for the form"""
if not request.session.get('form-submitted', False):
return HttpResponseRedirect(reverse('grap_main:error_page'))
else:
amount = set_payment()
paypal_dict = {
"business": "business#gmail.com",
"amount": str(amount),
"item_name": "2017 USGF Championships",
"notify_url": "https://15b6b6cb.ngrok.io" + reverse('paypal-ipn'),
"return_url": "https://15b6b6cb.ngrok.io/Confirm",
"cancel_return": "https://15b6b6cb.ngrok.io/RegistrationForm",
}
form = PayPalPaymentsForm(initial=paypal_dict)
context = confirm_information()
context["amount"] = amount
context["form"] = form
request.session['form-submitted'] = False
valid_ipn_received.connect(show_me_the_money)
return render(request, "grap_main/payment.html", context)
Where my then I have payment.html which then create the paypal button simply by using the line as advised in the documentation
{{ form.render }}
Now I can receive a POST to the paypal url that I specified in the documentation however I don't know where I should put my signal function that will grab the IPNs once someone has completed a purchase.
def show_me_the_money(sender, **kwargs):
"""signal function"""
ipn_obj = sender
if ipn_obj.payment_status == ST_PP_COMPLETED:
print 'working'
else:
print "not working"
Right now I am calling this function in the view payment_page() however I know this is before the POST to the paypal and is not correct
I don't understand where I should call the show_me_the_money() function. I am used to creating a views in which is called from the html script as shown below
def register(request):
"""aquire information from new entry"""
if request.method != 'POST':
form = RegisterForm()
else:
if 'refill' in request.POST:
form = RegisterForm()
else:
form = RegisterForm(data=request.POST)
if form.is_valid():
form.save()
request.session['form-submitted'] = True
return HttpResponseRedirect(reverse('grap_main:payment_page'))
.html
<form action="{% url 'grap_main:register' %}" method='post' class="form">
{% csrf_token %}
{% bootstrap_form form %}
<br>
{% buttons %}
<center>
<button name='submit' class="btn btn-primary">Submit</button>
</center>
{% endbuttons %}
</form>
I believe I need to call the function after a person has completed a purchase however I don't know how to target that time window in my code. I want to make sure I handle a case in which the user once there are done paying don't always return to the merchant site.
Any help on this subject would not only benefit me but also the earlier poster. I hope to make a tutorial when I figure this out to help others that might get stuck as well.
Please note that I have also use ngrok to make sure my project is accessible to the paypal IPN service. I am using two urls.py files as well, i which the main one looks as so
urlpatterns = [
url(r'^paypal/', include('paypal.standard.ipn.urls')),
url(r'^admin/', admin.site.urls),
url(r'', include('grap_main.urls', namespace='grap_main')),
]
where the 'grap_main.urls' are all the specific views for my site, ex.
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^Confirm', views.confirm, name='confirm'),
url(r'^Events', views.events, name='events'),
.........]
[UPDATE]: Forgot to mention where and how to put the code that you wrote (the handler if you like). Inside the handlers.py file write it like this:
# grap_main/signals/handlers.py
from paypal.standard.ipn.signals import valid_ipn_received, invalid_ipn_received
#receiver(valid_ipn_received)
def show_me_the_money(sender, **kwargs):
"""Do things here upon a valid IPN message received"""
...
#receiver(invalid_ipn_received)
def do_not_show_me_the_money(sender, **kwargs):
"""Do things here upon an invalid IPN message received"""
...
Although signals (and handlers) can live anywhere, a nice convention is to store them inside your app's signals dir. Thus, the structure of your grap_main app should look like this:
project/
grap_main/
apps.py
models.py
views.py
...
migrations/
signals/
__init__.py
signals.py
handlers.py
Now, in order for the handlers to be loaded, write (or add) this inside grap_main/apps.py
# apps.py
from django.apps.config import AppConfig
class GrapMainConfig(AppConfig):
name = 'grapmain'
verbose_name = 'grap main' # this name will display in the Admin. You may translate this value if you want using ugettex_lazy
def ready(self):
import grap_main.signals.handlers
Finally, in your settings.py file, under the INSTALLED_APPS setting, instead of 'grap_main' use this:
# settings.py
INSTALLED_APPS = [
... # other apps here
'grap_main.apps.GrapMainConfig',
... # other apps here
]
Some side notes:
Instead of using standard forms to render the paypal button use encrypted buttons. That way you will prevent any potential modification of the form (mostly change of prices).
I was exactly in your track a few months ago using the terrific django-paypal package. However, I needed to upgrade my project to Python 3. But because I used the encrypted buttons I was unable to upgrade. Why? Because encrypted buttons depend on M2Crypto which doesn't support (yet) Python 3. What did I so? I dropped django-paypal and joined Braintree which is a PayPal company. Now, not only I can accept PayPal payments but also credit card ones. All Python 3!
This is is embarrassing but the problem was that I was not using a test business account... I implemented the answer nik_m suggegested as well so I would advise doing the same. Put the function:
def show_me_the_money(sender, **kwargs):
"""signal function"""
ipn_obj = sender
if ipn_obj.payment_status == ST_PP_COMPLETED:
print 'working'
else:
print "not working"
valid_ipn_received.connect(show_me_the_money)
in handlers.py
Related
In django rest_auth password reset, default email content look like following:-
You're receiving this email because you requested a password reset for your user account at localhost:8000.
Please go to the following page and choose a new password:
http://localhost:8000/api/reset/Kih/89a-23809182347689312b123/
Your username, in case you've forgotten: test
Thanks for using our site!
The localhost:8000 team
How to customize content of this email ?
I recently needed to implement the same thing in one of my projects and could not find a thorough answer anywhere.
So I'm leaving my solution here for anyone who needs it in the future.
Expanding on mariodev's suggestion:
1. Subclass PasswordResetSerializer and override save method.
yourproject_app/serializers.py
from django.conf import settings
from rest_auth.serializers import PasswordResetSerializer as _PasswordResetSerializer
class PasswordResetSerializer(_PasswordResetSerializer):
def save(self):
request = self.context.get('request')
opts = {
'use_https': request.is_secure(),
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
###### USE YOUR TEXT FILE ######
'email_template_name': 'example_message.txt',
'request': request,
}
self.reset_form.save(**opts)
2. Configure AUTH_USER_MODEL
yourproject/settings.py
###### USE YOUR USER MODEL ######
AUTH_USER_MODEL = 'yourproject_app.ExampleUser'
3. Connect custom PasswordResetSerializer to override default
yourproject/settings.py
REST_AUTH_SERIALIZERS = {
'PASSWORD_RESET_SERIALIZER':
'yourproject_app.serializers.PasswordResetSerializer',
}
4. Add the path to the directory where your custom email message text file is located to TEMPLATES
yourproject/settings.py
TEMPLATES = [
{
...
'DIRS': [os.path.join(BASE_DIR, 'yourproject/templates')],
...
}
]
5. Write custom email message (default copied from Django)
yourproject/templates/example_message.txt
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset
for your user account at {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}
UPDATE: This solution was written for an older version of django-rest-auth (v0.6.0). As I can tell from the comments, it seems there have been some updates made to the source package that more readily handle custom email templates out-of-box. It is always better to use methods defined in a package rather than overriding them like in my solution. Though once a necessity, it may not be so any longer.
You can inherit PasswordResetSerializer and override the get_email_options method. For example:
from rest_auth.serializers import PasswordResetSerializer
class CustomPasswordResetSerializer(PasswordResetSerializer):
def get_email_options(self):
return {
'subject_template_name': 'registration/password_reset_subject.txt',
'email_template_name': 'registration/password_reset_message.txt',
'html_email_template_name': 'registration/'
'password_reset_message.html',
'extra_email_context': {
'pass_reset_obj': self.your_extra_reset_obj
}
}
You need to hook up your own reset password serializer (PASSWORD_RESET_SERIALIZER) with customized save method.
(ref: https://github.com/Tivix/django-rest-auth/blob/v0.6.0/rest_auth/serializers.py#L123)
Unfortunately you need to override the whole save method, due to how the e-mail options are used. We we'll make it a bit more flexible in the next release (0.7.0)
A simple solution is
Create over templates directory:
-templates
-registration
password_reset_email.html
with content you want.
Django rest-auth use django.contrib.auth templates.
So for the dj-rest-auth, this is how I did it:
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.urls.base import reverse
from allauth.account import app_settings
from allauth.account.adapter import get_adapter
from allauth.account.utils import user_pk_to_url_str, user_username
from allauth.utils import build_absolute_uri
from dj_rest_auth.forms import AllAuthPasswordResetForm
from dj_rest_auth.serializers import PasswordResetSerializer
class CustomAllAuthPasswordResetForm(AllAuthPasswordResetForm):
def save(self, request, **kwargs):
current_site = get_current_site(request)
email = self.cleaned_data['email']
token_generator = kwargs.get('token_generator',
default_token_generator)
for user in self.users:
temp_key = token_generator.make_token(user)
# save it to the password reset model
# password_reset = PasswordReset(user=user, temp_key=temp_key)
# password_reset.save()
# send the password reset email
path = reverse(
'password_reset_confirm',
args=[user_pk_to_url_str(user), temp_key],
)
url = build_absolute_uri(None, path) # PASS NONE INSTEAD OF REQUEST
context = {
'current_site': current_site,
'user': user,
'password_reset_url': url,
'request': request,
}
if app_settings.AUTHENTICATION_METHOD != app_settings.AuthenticationMethod.EMAIL:
context['username'] = user_username(user)
get_adapter(request).send_mail('account/email/password_reset_key',
email, context)
return self.cleaned_data['email']
class CustomPasswordResetSerializer(PasswordResetSerializer):
#property
def password_reset_form_class(self):
return CustomAllAuthPasswordResetForm
# settings.py
REST_AUTH_SERIALIZERS = {
'PASSWORD_RESET_SERIALIZER':
'api.users.api.serializers.CustomPasswordResetSerializer',
}
By passing None to build_absolute_uri instead of the original request, it will take the value you have in django.contrib.sites module with SITE_ID=1. So whatever you have defined as your domain in the Django admin will now be the domain in the reset URL. This makes sense if you want to have the password reset URL point to your frontend, that might be a React application running on a different domain.
Edit:
My PR regarding this issue was merged, with the next release this will be possible to set in your settings. Checkout the docs for dj-rest-auth to see which setting you need to set.
if you want to use a html email template, an update to Brian's answer would be to add
'html_email_template_name': 'account/email/example_message.html',
just below
###### USE YOUR TEXT FILE ######
'email_template_name': 'account/email/example_message.txt',
this way you can the email with a html template
You can see why this happens by inspecting the send_mail method of the PasswordResetForm class
class PasswordResetForm(forms.Form):
email = forms.EmailField(label=_("Email"), max_length=254)
def send_mail(self, subject_template_name, email_template_name,
context, from_email, to_email, html_email_template_name=None):
"""
Send a django.core.mail.EmailMultiAlternatives to `to_email`.
"""
subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)
email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
if html_email_template_name is not None:
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, 'text/html')
email_message.send()```
Create directory with path as following in your template folder
templates/admin/registration/
Now copy all files in django/contrib/admin/templates/registration/ into this directory you just created. You can find this directory where you have installed django. In linux, it can be find here
/usr/local/lib/python2.7/dist-packages/django/contrib/admin/templates/registration
You will need root priviliges for accessing this.
Now when you will send email, templates in you just copied in your project will be used.
This link might be helpful. With it I was able to find where the email templates were and how to customize them.
You can find the info at the bottom of the page under
Customize the email message
http://www.sarahhagstrom.com/2013/09/the-missing-django-allauth-tutorial/#Customize_the_email_message
I wrote a function that allows the user to delete his article on a blog website. The problem is, if he plays a little with the url, he can access to another article and delete it.
What is the common strategy to avoid such cases with django?
here are the codes I wrote for the fonction:
views.py
def delete_article(request, id):
deleted = False
logged_user = get_logged_user_from_request(request) #that line allow to ensure that the user is connected. I use the session to achieve that instead of extending the User model
offer = get_object_or_404(Offer, id=id)
if request.method == 'POST':
offer.delete()
deleted = True
return render(request, 'offers/delete_article.html', locals())
urls.py
urlpatterns = patterns('article.views',
url(r'^send_article$', 'send_article', name='send_article'),
url(r'^my_articles$', 'show_my_articles', name='my_articles'),
url(r'^article/(?P<id>\d+)$', 'read', name='read'),
url(r'^articles$', 'show_articles', name='articles'),
url(r'^search_article$', 'search', name='search'),
url(r'^delete_article/(?P<id>\d+)$', 'delete_offer', name='delete_offer'),
)
delete_article.html
{% if not deleted %}
Hey, are you sure you want to delete {{ article.title }}?
<form method="POST">
{% csrf_token %}
<button type="submit" class="deleting_offer_button">delete</button>
</form>
{% elif deleted %}
<p>the article was successfully deleted</p>
get back to the homepage<br />
{% endif %}
As you can see, if the user change the numer of the id in the url, he can delete other article when he is directed to the confirmation of deleting page.
What webmasters are doing to ensure users cannot interfere with objects of other users?
HttpResponseForbidden can be used here which uses a 403 status code. A 403 response generally used when authentication was provided, but the authenticated user is not permitted to perform the requested operation.
Assuming you have author as an foreign key in Offer model, you can change your views like this:
In your views.py you have to import :
from django.http import HttpResponseForbidden
And then in your delete_article method use this code
offer = get_object_or_404(Offer, id=id)
if offer.author != request.user:
return HttpResponseForbidden()
When you get the article/offer. Make sure that the owner of that article is the authenticated user.
I'm not sure what your models look like but it would be something like
offer = get_object_or_404(Offer, id=id, author=logged_user)
This way if they don't own the article, it will 404
I would like to display the list of the authenticated users.
On the documentation: http://docs.djangoproject.com/en/dev/topics/auth/
class models.User
is_authenticated()¶
Always returns True. This is a way to tell if the user has been authenticated. ...
You can know on the template side is the current User is authenticated or not:
{% if user.is_authenticated %}
{% endif %}
But I didn't found the way the get the list of the authenticated users.
Any idea?
Going along with rz's answer, you could query the Session model for non-expired sessions, then turn the session data into users. Once you've got that you could turn it into a template tag which could render the list on any given page.
(This is all untested, but hopefully will be close to working).
Fetch all the logged in users...
from django.contrib.auth.models import User
from django.contrib.sessions.models import Session
from django.utils import timezone
def get_all_logged_in_users():
# Query all non-expired sessions
# use timezone.now() instead of datetime.now() in latest versions of Django
sessions = Session.objects.filter(expire_date__gte=timezone.now())
uid_list = []
# Build a list of user ids from that query
for session in sessions:
data = session.get_decoded()
uid_list.append(data.get('_auth_user_id', None))
# Query all logged in users based on id list
return User.objects.filter(id__in=uid_list)
Using this, you can make a simple inclusion template tag...
from django import template
from wherever import get_all_logged_in_users
register = template.Library()
#register.inclusion_tag('templatetags/logged_in_user_list.html')
def render_logged_in_user_list():
return { 'users': get_all_logged_in_users() }
logged_in_user_list.html
{% if users %}
<ul class="user-list">
{% for user in users %}
<li>{{ user }}</li>
{% endfor %}
</ul>
{% endif %}
Then in your main page you can simply use it where you like...
{% load your_library_name %}
{% render_logged_in_user_list %}
EDIT
For those talking about the 2-week persistent issue, I'm assuming that anyone wanting to have an "active users" type of listing will be making use of the SESSION_EXPIRE_AT_BROWSER_CLOSE setting, though I recognize this isn't always the case.
Most reliable solution would only be the something you store when user logs in or logs out. I saw this solution and i think its worth sharing.
models.py
from django.contrib.auth.signals import user_logged_in, user_logged_out
class LoggedUser(models.Model):
user = models.ForeignKey(User, primary_key=True)
def __unicode__(self):
return self.user.username
def login_user(sender, request, user, **kwargs):
LoggedUser(user=user).save()
def logout_user(sender, request, user, **kwargs):
try:
u = LoggedUser.objects.get(user=user)
u.delete()
except LoggedUser.DoesNotExist:
pass
user_logged_in.connect(login_user)
user_logged_out.connect(logout_user)
views.py
logged_users = LoggedUser.objects.all().order_by('username')
Sounds similiar with this solution, you can create a custom middleware to do it. I found awesome OnlineNowMiddleware here.
Where you will get these;
{{ request.online_now }} => display all list of online users.
{{ request.online_now_ids }} => display all online user ids.
{{ request.online_now.count }} => display total online users.
How to set up?
Create file middleware.py where location of settings.py has been saved, eg:
projectname/projectname/__init__.py
projectname/projectname/middleware.py
projectname/projectname/settings.py
Then following this lines;
from django.core.cache import cache
from django.conf import settings
from django.contrib.auth.models import User
from django.utils.deprecation import MiddlewareMixin
ONLINE_THRESHOLD = getattr(settings, 'ONLINE_THRESHOLD', 60 * 15)
ONLINE_MAX = getattr(settings, 'ONLINE_MAX', 50)
def get_online_now(self):
return User.objects.filter(id__in=self.online_now_ids or [])
class OnlineNowMiddleware(MiddlewareMixin):
"""
Maintains a list of users who have interacted with the website recently.
Their user IDs are available as ``online_now_ids`` on the request object,
and their corresponding users are available (lazily) as the
``online_now`` property on the request object.
"""
def process_request(self, request):
# First get the index
uids = cache.get('online-now', [])
# Perform the multiget on the individual online uid keys
online_keys = ['online-%s' % (u,) for u in uids]
fresh = cache.get_many(online_keys).keys()
online_now_ids = [int(k.replace('online-', '')) for k in fresh]
# If the user is authenticated, add their id to the list
if request.user.is_authenticated:
uid = request.user.id
# If their uid is already in the list, we want to bump it
# to the top, so we remove the earlier entry.
if uid in online_now_ids:
online_now_ids.remove(uid)
online_now_ids.append(uid)
if len(online_now_ids) > ONLINE_MAX:
del online_now_ids[0]
# Attach our modifications to the request object
request.__class__.online_now_ids = online_now_ids
request.__class__.online_now = property(get_online_now)
# Set the new cache
cache.set('online-%s' % (request.user.pk,), True, ONLINE_THRESHOLD)
cache.set('online-now', online_now_ids, ONLINE_THRESHOLD)
Finally update your MIDDLEWARE inside file of projectname/projectname/settings.py:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
....
# Custom middleware
'projectname.middleware.OnlineNowMiddleware',
]
For other condition, you can also check the current user is online or not with:
{% if request.user in request.online_now %}
{# do stuff #}
{% endif %}
There is no easy, built-in way to do what you want that I know of. I'd try a combination of expiring sessions and filtering on last_login. Maybe even write a custom manager for that.
I am trying to use comments application in my project.
I tried to use code ({% render_comment_form for event %}), shown in the documentation here:
Django comments
And the question is how to make the form redirect to the same page, after the submission.
Also the big question is:
Currently if we have any error found in the for, then we're redirected to preview template.
Is that possible to avoid this behaviour and display errors over the same form (on the same page)?
I will show you how I resolved it in my blog, so you could do something similar. My comments are for Entry model in entries application.
First add new method for your Entry (like) object.
def get_absolute_url(self):
return "/%i/%i/%i/entry/%i/%s/" % (self.date.year, self.date.month, self.date.day, self.id, self.slug)
It generates url for entry objects. URL example: /2009/12/12/entry/1/lorem-ipsum/
To urls.py add 1 line:
(r'^comments/posted/$', 'smenteks_blog.entries.views.comment_posted'),
So now you should have at least 2 lines for comments in your urls.py file.
(r'^comments/posted/$', 'smenteks_blog.entries.views.comment_posted'),
(r'^comments/', include('django.contrib.comments.urls')),
For entries (like) application in views.py file add function:
from django.contrib.comments import Comment #A
...
def comment_posted(request):
if request.GET['c']:
comment_id = request.GET['c'] #B
comment = Comment.objects.get( pk=comment_id )
entry = Entry.objects.get(id=comment.object_pk) #C
if entry:
return HttpResponseRedirect( entry.get_absolute_url() ) #D
return HttpResponseRedirect( "/" )
A) Import on top of file to have
access for comment object,
B) Get
comment_id form REQUEST,
C) Fetch
entry object,
D) Use
get_absolute_url method to make
proper redirect.
Now:
Post button in comment form on entry site redirects user on the same (entry) site.
Post button on preview site redirects user on the proper (entry) site.
Preview button in comment form on entry site and on preview site redirects user on preview site
Thankyou page is not more in use (That page was quite annoying in my opinion).
Next thing good to do is to override preview.html template:
Go to django framework dir, under linux it could by /usr/share/pyshared/.
Get original preview.html template from DJANGO_DIR/contrib/comments/templates/comments/preview.html
Copy it to templates direcotry in your project PROJECT_DIR/templates/comments/entries_preview.html
From now on, it shoud override default template, You can change extends in this way: {% extends "your_pagelayout.html" %} to have your layout and all css files working.
Take a look at "Django-1.4/django/contrib/comments/templates/comments/" folder and you will see in the "form.html" file, there is the line
{% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
Therefore, in the Article-Detail view, you can include the "next" attribute in the context data, and then the comment framework will do the rest
class ArticleDetailView(DetailView):
model = Article
context_object_name = 'article'
def get_context_data(self, **kwargs):
context = super(ArticleDetailView, self).get_context_data(**kwargs)
context['next'] = reverse('blogs.views.article_detail_view',
kwargs={'pk':self.kwargs['pk'], 'slug': self.kwargs['slug']})
return context
Simplify Django’s Free Comments Redirection
Update: Now have the option to redirect as part of the comment form: see https://django-contrib-comments.readthedocs.io/en/latest/quickstart.html#redirecting-after-the-comment-post
This is a really simple redirect to implement. It redirects you back to the page where the comment was made.
When a comment is posted, the url comments/posted/ calls the view comment_posted which then redirects back to the referer page.
Be sure to replace [app_name] with your application name.
views.py
from urlparse import urlsplit
def comment_posted( request ):
referer = request.META.get('HTTP_REFERER', None)
if referer is None:
pass
try:
redirect_to = urlsplit(referer, 'http', False)[2]
except IndexError:
pass
return HttpResponseRedirect(redirect_to)
urls.py
( r'^comments/posted/$', '[app_name].views.comment_posted' ),
I'm rather new to Django and I'm using Django 1.0.
I have this:
forms.py:
class MyForm(forms.Form):
extra_cheeze = forms.BooleanField(required=False,
initial=False,
label='Extra cheeze')
views.py:
def order_something(request):
form = MyForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
# do stuff...
The problem is that the form is not valid unless the checkbox is checked, so there doesn't seem to be a way to get a False value from the field.
As far as I can understand from the docs, it should work. It works if I add a CharField to my form...
Am I misunderstanding something here or is this a bug? (Yes, I have googled but found nothing relevant)
Update: As suggested by #Dominic Rodger, I tried adding a hidden field
dummy = forms.CharField(initial='dummy', widget=forms.widgets.HiddenInput())
and that makes the form valid. This workaround enables me to move on right now, but it would still be interesting to know if I'm misunderstanding something...
There was a bug in the code in my question. Thanks to #d0ugal for helping me spot it by including a slightly different example. The problem was here:
form = MyForm(request.POST or None) # <- PROBLEM HERE!!!!!!!!!!!!!!!!
if request.method == 'POST' and form.is_valid():
# do stuff...
The bug was that I assumed that request.POST would evaluate to True if it was a post. But since browsers don't post anything for a not-checked checkbox, and that was the only field, the POST data was an empty dictionary, which evaluates to False. This caused None to be used as initialization data, causing the form to be unbound and not valid.
#d0ugal's example does the safe thing and tests request.method first.
This also works for me on 1.1, 1.0.3 and 1.0 (I have these three Virtual environments setup). I only tested this in FireFox so if its a browser issue thats another matter but as far as I know they all handle POST data with checkboxes the same.
Here is the full code for the project so you can reproduce at your leisure and compare with yours to see the difference.
Setting up in Ubuntu
$ django-admin.py startproject testing
$ cd testing/
$ python manage.py startapp myfirst
Then in the myfirst app folder;
/myfirst/views.py
from django.shortcuts import render_to_response
from myfirst.forms import MyForm
def testing(request):
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
result = "valid"
else:
result = "not valid"
else:
form = MyForm()
result = "no post"
return render_to_response('test.html', {'form':form, 'result':result,})
/myfirst/forms.py
from django import forms
class MyForm(forms.Form):
extra_cheeze = forms.BooleanField(required=False,initial=False,label='Extra cheeze')
/myfirst/templates/test.html
<html>
<head>
</head>
<body>
<form action="." method="POST">
{{ form }}
<input type="submit" value="test">
</form>
{{ result }}
</body>
</html>
/urls.py
from django.conf.urls.defaults import *
from myfirst.views import testing
urlpatterns = patterns('',
(r'.*', testing),
)
Then just run the project $ python manage.py runserver and browse to http://localhost:8000/. You'll actually find that required doesn't do anything with the checkbox, since you can't leave it blank - a 'blank' answer is effectively 'no'. If you want to make sure a user selects an answer have a multiple choice where the user has to select yes or no. You could force them to answer with radio buttons too.
This is not an answer, it describes the Django behaviour only.
Django 3.0.7 on Python 3.8.4rc1, Debian 10/testing.
My scenario is html <form> with more django forms.Form rendered classes inside, one of them has BooleanField only. (There are other small <forms> in page like a search form).
class OrderInvoiceTypeForm(forms.Form):
is_business = forms.BooleanField(label="business", initial=False) # with or without required=False
I submit via submit button which IS NOT in OrderInvoiceTypeForm but IS in the html .
With required=False form is always valid and is_business key is in .cleaned_data.
Without required (or with =True):
checked:
form_with_boolean.is_valid() # True
form_with_boolean.cleaned_data # {'is_business': True}
unchecked:
form_with_boolean.is_valid() # False
form_with_boolean.cleaned_data # {}
In 2nd case in dev tools I can see that browser (Chrome) doesn't send the is_business value POST variable. Adding of hidden field will not help.