How can I implement incremental wait time for django axes? - django

I'm looking to implement django axes for brute force protection for my application. I want to implement something like this "lockout for 5 minutes after 5 failed attempts, lockout for 10 minutes after another 5 attempts"
Seems like the example in the doc is more straightforward with a fixed wait time, so I want to know if it's possible to implement something like this.

Yes this is possible, but will require several items to setup.
models.py
from axes.models import AccessAttempt
from django.db import models
from django.utils.translation import gettext_lazy as _
class AccessAttemptAddons(models.Model):
accessattempt = models.OneToOneField(AccessAttempt, on_delete=models.CASCADE)
expiration_date = models.DateTimeField(_("Expiration Time"), auto_now_add=False)
This models.py file is located within my login app(directory: project_name/login/models.py). This will create a new table called login_accessattemptaddons and establishes a "one to one" relationship with Django Axes' AccessAttempt table. This is necessary so that each entry in the AccessAttempt table is associated with each entry in the AccessAttemptAddons table; thus, making the expiration_date column in AccessAttemptAddons an extension of the AccessAttempt table.
settings.py
AXES_FAILURE_LIMIT = 5
AXES_LOCK_OUT_AT_FAILURE = True
AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP = True
AXES_LOCKOUT_CALLABLE = "login.views.timeout"
In settings.py the AXES_FAILURE_LIMIT is set to 5, meaning after the 5th failure to login, AXES_LOCKOUT_CALLABLE executes. AXES_LOCKOUT_CALLABLE is set to "login.views.timeout." I am not sure if this is best practice, but the function to run after failure is located within my projects login app, views.py file, and in my views.py file, a method called "timeout."
views.py
import datetime
from axes.models import AccessAttempt
from django.contrib.auth import login, authenticate
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from django.views import View
from .models import AccessAttemptAddons
from .forms import LoginForm
class LoginView(View):
form = LoginForm
context = {"form": form}
base_page = "login/index.html"
auth_base_page = "login/index.auth.html"
def get(self, request):
""" returns the login app index page """
if(request.user.is_authenticated):
return render(request, self.auth_base_page, self.context)
else:
return render(request, self.base_page, self.context)
def post(self, request):
""" Handles logging the user in """
# passing the request to the form to access in our forms.py file
form_post = self.form(request.POST, request=request)
if(form_post.is_valid()):
email = form_post.cleaned_data["email"]
password = form_post.cleaned_data["password"]
user = authenticate(username=email, password=password)
login(request, user, backend="django.contrib.auth.backends.ModelBackend")
return HttpResponseRedirect(reverse("account:index"))
else:
errors = form_post.non_field_errors()
context = {"form": form_post, "errors": errors}
return render(request, self.base_page, context)
def timeout(request):
try:
loginview = LoginView()
username = request.POST.get("email")
ip_address = request.axes_ip_address
account = AccessAttempt.objects.filter(username=username).filter(ip_address=ip_address)
current_time = datetime.datetime.now()
number_of_attempts = account.failures_since_start
threshold = (number_of_attempts / 5) * 5
form = LoginForm(request.POST)
error = f"Access attempts exceeded. Please wait {threshold} minutes"
base_page = "login/index.html"
context = {"form": form, "errors": error}
result = AccessAttempt.objects.raw(
'''
SELECT axes_accessattempt.id, login_accessattemptaddons.expiration_date
FROM axes_accessattempt
INNER JOIN login_accessattemptaddons
ON axes_accessattempt.id = login_accessattemptaddons.accessattempt_id
WHERE axes_accessattempt.username = %s and axes_accessattempt.ip_address = %s
''', [username, ip_address]
)[0]
if(current_time < result.expiration_date):
return render(request, base_page, context)
else:
account.delete()
account_page = loginview.post(request)
return account_page
except IndexError:
expiration_date = current_time + datetime.timedelta(minutes=threshold)
id = AccessAttempt.objects.filter(username=username, ip_address=ip_address)[0].id
addons = AccessAttemptAddons(expiration_date=expiration_date, accessattempt_id=id)
addons.save()
return render(request, base_page, context)
This is a copy of my views.py(directory: project_name/login/views.py). Starting with the timeout method, note that the threshold formula is the number of attempts divided by 5 then the result is multiplied by 5. Example: (10 failed attempts / 5 ) * 5min = 10min timeout. For attempts non divisible by 5(11, 12, .. 14) they are rounded down to 10min lockout. For 15 failed attempts to 19 failed attempts, the lockout time would be 15min.
Continuing with the code, the AccessAttempt table and AccessAttemptAddons table is joined on the one to one field "accessattempt_id." This creates a resultant table where expiration_date information for each related AccessAttempt id can be obtained. If there are no entries for the current failed attempt(brand new account lockout) then an index error occurs. The error is caught and creates a new expiration entry in the AccessAttemptAddons table with the formula above. On any subsequent attempts, the raw sql will pass without index failures because there is now an existing expiration_date entry.
This leads to the the if statement blocks to check to see if the account is ready to be unlocked or return an error page back to the user. If the current time is less than the expiration_date entry within the result table, then return a page with the error message. If the current time is greater, it means that enough time has passed to retry logging into the account. The request is handed off to the login view "post" method. Any failures after this repeats the timeout method steps.
Closing remarks
The timeout method in the views.py is what is necessary; how the request is handled after the lockout expires is up to the developer. I only showed how I handled it with a class based login view.

Related

How to remove an already selected option from options list to avoid double bookings

I've been scratching my head with this for 2 days hoping for a sudden brainwave and getting nowhere. I've completely drawn a blank with my logic and all attempts have resulted in me breaking my code. I'm trying to get it so that on a specific date, if a user has already selected a time slot with a selected barber, that that time slot will be removed from the list of time slots, so it cannot be selected again by another user.
From models.py
from django.db import models
import datetime
from django.core.validators import MinValueValidator
from django.contrib.auth.models import User
SERVICES = (
('Gents Cut', 'Gents Cut'),
('Kids Cut', 'Kids Cut'),
('Cut and Shave', 'Cut and Shave'),
('Shave Only', 'Shave Only'),
)
TIME_SLOTS = (
('9.00 - 10.00', '9.00 - 10.00'),
('10.00 - 11.00', '10.00 - 11.00'),
('11.00 - 12.00', '11.00 - 12.00'),
('12.00 - 13.00', '12.00 - 13.00'),
('13.00 - 14.00', '13.00 - 14.00'),
('14.00 - 15.00', '14.00 - 15.00'),
('15.00 - 16.00', '15.00 - 16.00'),
('16.00 - 17.00', '16.00 - 17.00'),
('17.00 - 18.00', '17.00 - 18.00'),
)
BARBER_NAME = (
('Nathan', 'Nathan'),
('Chris', 'Chris'),
('Ben', 'Ben'),
('Dan', 'Dan'),
)
class Booking(models.Model):
date = models.DateField(validators=[MinValueValidator(datetime.date.today)])
time = models.CharField(max_length=50, null=True, choices=TIME_SLOTS)
barber = models.CharField(max_length=50, null=True, choices=BARBER_NAME)
service = models.CharField(max_length=50, null=True, choices=SERVICES)
customer = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return f"{self.customer} has booked {self.service} on {self.date} at {self.time} with {self.barber}"
from views.py
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.views import generic, View
from django.contrib import messages
from django.http import HttpResponseRedirect
from .models import Booking
from .forms import BookingForm
#login_required()
def make_booking(request):
if request.method == "POST":
form = BookingForm(request.POST)
if form.is_valid():
booking_form = form.save(commit=False)
booking_form.customer = request.user
booking_form.save()
messages.success(request, ('Your booking is awaiting confirmation'))
return HttpResponseRedirect('/bookings')
else:
form = BookingForm()
return render(request, 'bookings.html', {'form': form})
from forms.py
from .models import Booking
import datetime
from django import forms
class DatePicker(forms.DateInput):
input_type = 'date'
class BookingForm(forms.ModelForm):
class Meta:
model = Booking
fields = ('date', 'time', 'barber', 'service',)
widgets = {
'date': DatePicker(),
}
One way or another you will need another view to accept the selected date and return the available time slots. If you were to do this via AJAX then your view might look something like this:
import json
from datetime import datetime
def get_time_slots(request):
# get the date in python format, sent via ajax request
date = datetime.strptime(request.GET.get('date'), '%Y-%m-%d')
# get all times from bookings on the provided date
booked_slots = Booking.objects.filter(date=date).values_list('time', flat=True)
available_slots = []
for slots in TIME_SLOTS:
if slots[0] not in booked_slots:
available_slots.append(slots)
times_as_json = json.dumps(available_slots)
return JsonResponse(times_as_json)
A better way, imo, would be to create new models for time slots, services, and barbers. Firstly, you are not hardcoding information and can dynamically change things in the DB. Secondly, you can generate a very slick query to get the available time slots for a day, barber, service, etc. You can also set general availability by linking certain barbers to certain time slots, depending on the hours they work.

User matching query does not exist. I want to add data if user is first time coming?

Actually I m working on Blood Donation system. I m checking that if user is already exist then I checking that He or She completed 90 days Or not if He or She completed then His or Her request will be accepted. if didn't complete then I will show message U have to Complete 90 days.
I add data through data admin side and checked on the base of the CNIC that he or she completed 90 days completed or not and its Working But Now the main problem is that If add New Data from Front End he said User matching query does not exist. Now I have to Add else part But I don't know How? please tell me about that I m stuck from past 4 days.
#forms.py
from django.core.exceptions import ValidationError
from django.forms import ModelForm
from django.shortcuts import redirect
from .models import User
from datetime import datetime,timedelta
class UserForm(ModelForm):
class Meta:
model = User
fields = "__all__"
def clean_cnic(self):
cnic = self.cleaned_data['cnic']
print("This is a cnic",cnic)
existuser = User.objects.get(cnic = cnic)
if existuser:
previous_date = existuser.last_donation
current_date = datetime.now().astimezone()
print(previous_date,"-----_---",current_date)
final = current_date - previous_date
print("The final is -> ",final)
if final < timedelta(days= 90):
raise ValidationError("U have to wait 90 days to complete")
return final
#views.py
from django.shortcuts import render
from .models import *
from .forms import UserForm
def home(request):
return render(request, 'home.html')
def donor(request):
if request.method == "POST":
userform = UserForm(request.POST)
if userform.is_valid():
userform.save()
else:
userform = UserForm()
return render(request, 'donor.html',{'userform':userform})
Simply, you need to make sure the user has exists.
def clean_cnic(self):
cnic = self.cleaned_data['cnic']
try:
existuser = User.objects.get(cnic=cnic)
except User.DoesNotExist:
raise ValidationError("User not found!")
previous_date = existuser.last_donation
current_date = datetime.now().astimezone()
final = current_date - previous_date
if final < timedelta(days= 90):
raise ValidationError("U have to wait 90 days to complete")
return final

Django modelform NameError: name 'check_in' is not defined

Hi everyone I am a beginner in django, building a lodge reservation system with django3.0 and i can't seem to get my modelform to generate a proper queryset to get data in my Reservations model, i keep getting the NameError error message everytime i try enter a new date through my view and right now im not quite sure how to properly solve this error
here is my models.py:
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
now = timezone.now
end = timezone.now() + timezone.timedelta(days=2)
room_choices = [
('single_room', 'Single Room'),
('double_room', 'Double Room'),
('executive_room', 'Executive Room'),
]
class Reservation(models.Model):
room_type = models.CharField(max_length=30, choices=room_choices, default=room_choices[1])
check_in = models.DateField(default=timezone.now)
check_out = models.DateField(default=end)
class Meta:
verbose_name = 'Reservation'
verbose_name_plural = 'Reservations'
I would like to add validation statements later in views.py for price and number of rooms depending on the room_type the user selected so disregard the field for now
here is my forms.py:
from django import forms
from .models import Reservation
class AvailabilityForm(forms.ModelForm):
class Meta:
model = Reservation
fields = [
"room_type",
"check_in",
"check_out",
]
widgets = {
'check_in': forms.DateInput(format='%m/%d/%Y'),
'check_out': forms.DateInput(format='%m/%d/%Y'),
}
If someone knows how to get the widgets to work properly outside of admin, displaying the calender onclick and not not just a charfield with a datefield inside, please also help me fix this problem as well
here is my views.py:
from django.shortcuts import get_object_or_404, render
from datetime import timedelta, date
from django.contrib.auth.models import User
from django.contrib import messages
from . import forms
from .models import Reservation
def availability(request):
form = forms.AvailabilityForm()
reservation = Reservation
if request.method == 'POST':
form = forms.AvailabilityForm(request.POST or None)
if form.is_valid:
reserve = form.save(commit=False)
reserve.reservation = reservation
# check whether the dates are valid
# case 1: a room is booked before the check_in date, and checks out after the requested check_in date
case_1 = Reservation.objects.filter(check_in__lte=reserve.check_in).filter(check_out__gte=check_in)
# case 2: oom is booked before the requested check_out date and check_out date is after requested check_out date
case_2 = Reservation.objects.filter(check_in__lte=check_out, check_out__gte=check_out).exists()
#case3: room is booked in a date which lies between the two requested check-in/check-out dates
case_3 = Reservation.objects.filter(check_in__gte=check_in, check_out__lte=check_out).exists()
# if either of these is true, abort and render the error
if case_1 or case_2 or case_3:
return render(request, "availability.html", {"errors": "This room is not available on your selected dates", "form": form})
# else dates are valid
reserve.save()
messages.add_message(request, messages.SUCCESS, 'Room is available for your stay here')
return redirect("/complete_booking")
return render(request, "availability.html", {"form": form})
i have tried different methods to try get my view to validate the check_in and check_out objects as you can see case1 and case2 use different filtering techniques but neither seem to work, any help would be appreciated as to where i'm going wrong and how to fix it
From the comments, the error is:
File "C:\Users\Karabo\Documents\technocrats2\core\views.py", line 160, in availability
case_1 = Reservation.objects.filter(check_in__lte=check_in).filter(check_out__gte=check_out).exists()
NameError: name 'check_in' is not defined
After checking the traceback, it is clear that you haven't define any check_in variable. You need to define the variable before using it.
check_in = "18-01-2020" # value you want to use in queryset.
case_1 = Reservation.objects.filter(check_in__lte=reserve.check_in).filter(check_out__gte=check_in)

Django-CMS 3.0.3 Publishing a page duplicates data from plugin django-cms-saq

Django-cms-saq is tested for 2.4.x. I'm trying to update the program to work with 3.0.X.
So far, I've updated all the imports but am coming across an unusual error. When I add a question (a plugin) to a page and hit publish, it creates two copies of the question in the database (viewable through the admin site). Deleting either copy removes both from the published page but leaves the question in edit mode.
How would I go about trouble shooting this?
I will include some of the files here. Please let me know if you need any other files.
Note that I am trying to add a multiple-choice question.
From models.py:
from django.db import models
from django.db.models import Max, Sum
from cms.models import CMSPlugin, Page, Placeholder
from cms.models.fields import PageField
from taggit.managers import TaggableManager
from djangocms_text_ckeditor.models import AbstractText
...
class Question(CMSPlugin):
QUESTION_TYPES = [
('S', 'Single-choice question'),
('M', 'Multi-choice question'),
('F', 'Free-text question'),
]
slug = models.SlugField(
help_text="A slug for identifying answers to this specific question "
"(allows multiple only for multiple languages)")
tags = TaggableManager(blank=True)
label = models.CharField(max_length=512, blank=True)
help_text = models.CharField(max_length=512, blank=True)
question_type = models.CharField(max_length=1, choices=QUESTION_TYPES)
optional = models.BooleanField(
default=False,
help_text="Only applies to free text questions",
)
depends_on_answer = models.ForeignKey(
Answer, null=True, blank=True, related_name='trigger_questions')
def copy_relations(self, oldinstance):
for answer in oldinstance.answers.all():
answer.pk = None
answer.question = self
answer.save()
self.depends_on_answer = oldinstance.depends_on_answer
#staticmethod
def all_in_tree(page):
root = page.get_root()
# Remember that there might be questions on the root page as well!
tree = root.get_descendants() | Page.objects.filter(id=root.id)
placeholders = Placeholder.objects.filter(page__in=tree)
return Question.objects.filter(placeholder__in=placeholders)
#staticmethod
def all_in_page(page):
placeholders = Placeholder.objects.filter(page=page)
return Question.objects.filter(placeholder__in=placeholders)
def score(self, answers):
if self.question_type == 'F':
return 0
elif self.question_type == 'S':
return self.answers.get(slug=answers).score
elif self.question_type == 'M':
answers_list = answers.split(',')
return sum([self.answers.get(slug=a).score for a in answers_list])
#property
def max_score(self):
if not hasattr(self, '_max_score'):
if self.question_type == "S":
self._max_score = self.answers.aggregate(
Max('score'))['score__max']
elif self.question_type == "M":
self._max_score = self.answers.aggregate(
Sum('score'))['score__sum']
else:
self._max_score = None # don't score free-text answers
return self._max_score
def percent_score_for_user(self, user):
if self.max_score:
try:
score = Submission.objects.get(
question=self.slug,
user=user,
).score
except Submission.DoesNotExist:
return 0
return 100.0 * score / self.max_score
else:
return None
def __unicode__(self):
return self.slug
...
From cms_plugins.py
import itertools
import operator
from django.contrib import admin
from django.utils.translation import ugettext as _
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from cms_saq.models import Question, Answer, GroupedAnswer, Submission, \
FormNav, ProgressBar, SectionedScoring, ScoreSection, BulkAnswer, \
QuestionnaireText, SubmissionSetReview
from djangocms_text_ckeditor.cms_plugins import TextPlugin
from djangocms_text_ckeditor.models import Text
from bs4 import BeautifulSoup
...
class QuestionPlugin(CMSPluginBase):
model = Question
module = "SAQ"
inlines = [AnswerAdmin]
exclude = ('question_type',)
def render(self, context, instance, placeholder):
user = context['request'].user
submission_set = None
triggered = True
depends_on = None
if instance.depends_on_answer:
depends_on = instance.depends_on_answer.pk
try:
Submission.objects.get(
user=user,
question=instance.depends_on_answer.question.slug,
answer=instance.depends_on_answer.slug,
submission_set=submission_set,
)
triggered = True
except:
triggered = False
extra = {
'question': instance,
'answers': instance.answers.all(),
'triggered': triggered,
'depends_on': depends_on,
}
if user.is_authenticated():
try:
extra['submission'] = Submission.objects.get(
user=user,
question=instance.slug,
submission_set=submission_set,
)
except Submission.DoesNotExist:
pass
context.update(extra)
return context
def save_model(self, request, obj, form, change):
obj.question_type = self.question_type
super(QuestionPlugin, self).save_model(request, obj, form, change)
...
class MultiChoiceQuestionPlugin(QuestionPlugin):
name = "Multi Choice Question"
render_template = "cms_saq/multi_choice_question.html"
question_type = "M"
exclude = ('question_type', 'help_text')
...
plugin_pool.register_plugin(SingleChoiceQuestionPlugin)
plugin_pool.register_plugin(MultiChoiceQuestionPlugin)
plugin_pool.register_plugin(DropDownQuestionPlugin)
plugin_pool.register_plugin(GroupedDropDownQuestionPlugin)
plugin_pool.register_plugin(FreeTextQuestionPlugin)
plugin_pool.register_plugin(FreeNumberQuestionPlugin)
plugin_pool.register_plugin(FormNavPlugin)
plugin_pool.register_plugin(SubmissionSetReviewPlugin)
plugin_pool.register_plugin(SectionedScoringPlugin)
plugin_pool.register_plugin(ProgressBarPlugin)
plugin_pool.register_plugin(BulkAnswerPlugin)
plugin_pool.register_plugin(SessionDefinition)
plugin_pool.register_plugin(QuestionnaireTextPlugin)
plugin_pool.register_plugin(TranslatedTextPlugin)
From cms_app.py:
from cms.app_base import CMSApp
from cms.apphook_pool import apphook_pool
from django.utils.translation import ugettext_lazy as _
class CMSSaq(CMSApp):
name = _("Self Assessment")
urls = ["cms_saq.urls"]
apphook_pool.register(CMSSaq)
Additional information:
This duplicative observation creates an issue when trying get the question object via its slug Question.objects.get(slug=question_slug). Such a query should only return one question. What we get here is two questions returned.
import re
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST, require_GET
from django.views.decorators.cache import never_cache
from django.utils import simplejson, datastructures
from django.conf import settings
from cms_saq.models import Question, Answer, Submission, SubmissionSet
ANSWER_RE = re.compile(r'^[\w-]+(,[\w-]+)*$')
#require_POST
def _submit(request):
post_data = datastructures.MultiValueDict(request.POST)
submission_set_tag = post_data.pop('submission_set_tag', '')
for question_slug, answers in post_data.iteritems():
# validate the question
try:
question = Question.objects.get(
slug=question_slug,
#placeholder__page__publisher_is_draft=False,
)
except Question.DoesNotExist:
return HttpResponseBadRequest(
"Invalid question '%s'" % question_slug,
)
# check answers is a list of slugs
if question.question_type != 'F' and not ANSWER_RE.match(answers):
return HttpResponseBadRequest("Invalid answers: %s" % answers)
# validate and score the answer
try:
score = question.score(answers)
except Answer.DoesNotExist:
return HttpResponseBadRequest(
"Invalid answer '%s:%s'" % (question_slug, answers)
)
# save, but don't update submissions belonging to an existing set
filter_attrs = {
'user': request.user.id,
'question': question_slug,
'submission_set': None,
}
attrs = {'answer': answers, 'score': score}
rows = Submission.objects.filter(**filter_attrs).update(**attrs)
if not rows:
attrs.update(filter_attrs)
Submission.objects.create(**attrs)
# Create submission set if requested
if submission_set_tag:
submission_set_tag = submission_set_tag[0]
if submission_set_tag:
_create_submission_set(
request, submission_set_tag
)
return HttpResponse("OK")
If you create a page in CMS, and add plugins to it, once you hit publish the CMS creates a copy of each of those plugins as they are at that point in time. This gives the page a live version, and enables you to then make changes to it which are held in draft mode, until you hit publish again.
This allows you to have a draft version of the site where edits/changes can be made and finalised before making them public.
Seeing two copies for each plugin therefore, isn't a problem.
As a side note, I'd strongly recommend you join the CMS user group on Google Plus if you're developing CMS sites; https://plus.google.com/communities/107689498573071376044
update
Ok, so a plugin in CMS is attached to a placeholder which is on a page, and it's the page that has two version of itself. Because each plugin attaches to a page, you can use that relationship to filter your plugins.
If you register your plugin with admin, or to object calls on it you're just looking at what is stored in your table, which as I say, includes the draft and live version of plugins.
So when you're querying for your plugin do this;
questions = Question.objects.filter(placeholder__page__publisher_is_draft=True)
This will get you all the questions which are attached to draft pages.
The only drawback to this is that plugins are guaranteed to be attached to a Page() object unless the plugin is set to page_only = True but I've only ever been interested in plugins attached to pages so it works for me. See the docs for more info on this.
Furthermore, if you're ever adding CMS page links to any of your plugins, please don't forget to add a similar argument to your model field to ensure that you limit the page objects to draft only;
page_link = models.ForeignKey(
Page,
limit_choices_to={'publisher_is_draft': True},
help_text=_("Link to another page on the site."),
on_delete=models.SET_NULL,
related_name='myapp_page_link'
)

Django models __unicode__: How to return a value containing localized datetimes?

I have a localized Django App, localisation works well and the configuration is ok…
For forms needs, I use __unicode__ method for model to render ModelChoiceFields, but how do I format localized date in the unicode return?
In this method I have no access to current timezone, how to display my TimeSpanList correctly to my users? Currently, it displays UTC. I tryed django.template.defaultfilters.date and Simon Charette's django.utils.formats.localize which didn't helped as they probably lacked contextual data…
class TimeSpan(models.Model):
start = models.DateTimeField(_("start"))
end = models.DateTimeField(_("end"))
def __unicode__(self):
return u"from UTC:{0} to UTC:{1}".format(self.start, self.end)
class TimeSpanChooserForm(forms.Form):
time_span = forms.ModelChoiceField(
label=_("time span"), queryset=TimeSpan.objects.all())
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(TimeSpanChooserForm, self).__init__(*args, **kwargs)
How to know current locale without current request object to localize those datetimes? (If there is a way)
note: to me __unicode__ seems like the only way to display entries in ModelChoiceField.
note 2: to me, Yuji 'Tomita' Tomita comment is the best current answer but it lacks a usable exemple…
Thanks for reminding me about this question. It's a bit involved for a complete answer.
I've created a demo method that shows this in action. Run test() to see output.
The remaining challenge is getting the client timezone. This will probably be done with some JavaScript that adds a parameter to your GET/POST or adds a custom header to be read in the view.
Since this is an unknown, I just stubbed it out with a method that returns a random time zone. You should update it to reflect your client timezone retrieval method.
Here's a self documenting example:
Models
from django.utils.formats import localize
class TimeSpan(models.Model):
start = models.DateTimeField("start")
end = models.DateTimeField("end")
def __unicode__(self):
return u"from UTC:{0} to UTC:{1}".format(self.start, self.end)
Form
from django import forms
class TimeSpanChooserForm(forms.Form):
time_span = forms.ModelChoiceField(
label=("time span"), queryset=TimeSpan.objects.all())
def __init__(self, *args, **kwargs):
# get request and TZ from view. This is a special form..
self.request = kwargs.pop("request", None)
self.tz = kwargs.pop('tz', None)
super(TimeSpanChooserForm, self).__init__(*args, **kwargs)
# modify the choices attribute to add custom labels.
self.fields['time_span'].choices = [
self.make_tz_aware_choice(self.request, timespan) for timespan in self.fields['time_span'].queryset
]
def make_tz_aware_choice(self, request, timespan):
""" Generate a TZ aware choice tuple.
Must return (value, label). In the case of a ModelChoiceField, (instance id, label).
"""
start = timespan.start.replace(tzinfo=self.tz)
end = timespan.end.replace(tzinfo=self.tz)
label = "From {tz} {start} to: {tz} {end}".format(
start=start,
end=end,
tz=self.tz,
)
return (timespan.id, label)
View
import random
import pytz
from django import http
from django import template
def view(request):
""" Render a form with overridden choices respecting TZ
"""
def get_tz_from_request(request):
""" Get the client timezone from request.
How you do this is up to you. Likely from JS passed as a parmeter.
"""
random_timezone = random.choice(pytz.all_timezones)
return pytz.timezone(random_timezone)
form = TimeSpanChooserForm(request.POST or None, request=request, tz=get_tz_from_request(request))
ctx = template.Context({
'form': form,
})
rendered_template = template.Template("{{ form }}").render(ctx)
return http.HttpResponse(rendered_template)
def test():
from django.test import RequestFactory
rf = RequestFactory()
r = rf.get('/')
for i in range(10):
print str(view(r))
You can use the django.utils.formats.localize function.
from django.db import models
from django.utils.formats import localize
from django.utils.translation import ugettext
class TimeSpan(models.Model):
start = models.DateTimeField(_('start'))
end = models.DateTimeField(_('end'))
def __unicode__(self):
return ugettext("from %(start)s to %(end)s") % {
'start': localize(self.start),
'end': localize(self.end),
}
You can test the following work with those manipulation.
from django.utils import timezone
from django.utils.translation import override
with override('fr'):
print(TimeSpan(start=timezone.now(), end=timezone.now()))
with override('en'):
print(TimeSpan(start=timezone.now(), end=timezone.now()))
Both should display different formating.
If you want to make sure dates are displayed in a specific timezone you must make sure USE_TZ = True in your settings and set the TIME_ZONE setting to the one you want to use. You can set this timezone per request by using a middleware that calls django.utils.timezone.activate with the desired timezone.
You can also use the django-sundial package which takes care of this for you.
Try this:
def __unicode__(self):
return u'%s %s'%(self.start,self.end)
Buy using this it will return combined start and end datetime objects.This worked for me