Business days in Django - django

I need to implement a conditional break which uses business days. I have a class with a DateField, and if that date is less than 5 business days in the future, something (action a) happens, else b happens. How can I determine the number of business days between two objects?
Obviously I'll need to calculate what 5 business days from today is. Finding 5 days in the future would be easy, using a simple time-delta, but to account for business days, it gets more complicated. I think I can safely ignore holidays for now (it's not the best case, but I think I can make due with just having Business days be Monday-Friday). Can anyone give me some guidance as to how I could do something like: target = today + 5 business_days?
Thanks

Here's a generic solution, even though your case is embarrassingly simple ;P
from datetime import timedelta, date
def add_business_days(from_date, number_of_days):
to_date = from_date
while number_of_days:
to_date += timedelta(1)
if to_date.weekday() < 5: # i.e. is not saturday or sunday
number_of_days -= 1
return to_date
And the result.
>>> date.today()
datetime.date(2013, 7, 25)
>>> add_business_days(date.today(), 6)
datetime.date(2013, 8, 2)
Bonus marks if you check whether a date falls on a holiday in the if statement.

This Example is when you are making a django project as an API
You can get business days with Numpy which uses SCIPY(check for more details)
You will have to install numpy through pip
Python 2.7
pip install numpy
Python 3
pip3 install numpy
In models.py add depending on where you want to put your property method :
class WorkingDay(models.Model):
# YY-MM-DD
start = models.DateField(null=True, verbose_name="start")
end = models.DateField(null=True, verbose_name="end")
#property
def workdays(self):
total = np.busday_count(self.start, self.end)
return total
Then in your serializers.py you might have something like this.
class WorkingDaySerializer(serializers.ModelSerializer):
workdays = serializers.IntegerField(read_only=True)
class Meta:
model = WorkingDay
fields = '__all__'
read_only_fields = ['workdays']
In your views you might do something like this.
class WorkingDayAPI(APIView):
"""
{ "start":"2018-01-01",
"end":"2018-05-01"
}
GET work days between dates
"""
serializer_class = WorkingDaySerializer
def get(self, request, format=None):
business_days = WorkingDay.objects.all()
serializer = WorkingDaySerializer(business_days, many=True)
return Response(serializer.data)
def post(self, request, *args, **kwargs):
start = request.data.get('start')
end = request.data.get('end')
# bizdays = request.data.get('bizdays')
business_days = WorkingDay.objects.create(**request.data)
business_days.start = start
business_days.end = end
business_days.workdays = workdays
business_days.save()
serializer = WorkingDaySerializer(business_days)
return Response(serializer.data, status=status.HTTP_201_CREATED)

Related

Filter by multiple years in DRF

So I'm trying to do filter by year and so it could use multiple values like this
filter?year=2000,2021
And with this I should get all objects with year 2000 or 2021
My filter.py
from django_filters import(
BaseInFilter,
NumberFilter,
FilterSet,
CharFilter,
)
from .models import Research
class YearInFilter(BaseInFilter, NumberFilter):
pass
class ResarchFilter(FilterSet):
year = YearInFilter(field_name='date', lookup_expr='year')
category = CharFilter(field_name='category', lookup_expr='iexact')
class Meta:
model = Research
fields = ['date', 'category']
It looks almost like example from django-filter.
But when I'm trying to use it i've got an error Field 'None' expected a number but got [Decimal('2000'), Decimal('2021')].
My views.py
class ResarchCategoryYear(generics.ListCreateAPIView):
queryset = Research.objects.all()
serializer_class = ResearchSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = ResarchFilter
So what's wrong here? Why here is Decimal? Why it's not number? And how make it work like expected?
If you want all data between this two year, use range
if you want only the data that is exists in those years, use in
example:
year = YearInFilter(field_name='date', lookup_expr='range')
year = YearInFilter(field_name='date', lookup_expr='in')
Please check how you can exactly apply those filters in Django_filter
edit:
you can make a custom search method in this way:
def custom_filter(self, queryset, value, *args, **kwargs):
if not value:
return queryset
handleValue = value.split(',')
queryset = YourModel.objects.filter(your_field__in=handleValue).distinct()
return queryset
use it like this:
rest_framework.CharFilter(method=custom_filter)
and you can replace CharFilter based on what you want
Best of luck

Django DateField with only month and year

I post this because I could not find an answer/example that satisfied me and I solved it a way I haven't read in there. Maybe it can be useful or can be discussed.
Basically, my app displays formsets where users can create objects. Once validated, the formset is displayed again and user can add new objects.
Specifying a day is not relevant.
I first started to use some javascript to do it, then played with Django custom model (to subclass models.DateField).
I could not succeed to do it with the later (my post updated linked to this one)
As I have deadlines, I did it with simple ChoiceField storing objects as Integers and then, use a #property method to build a proper date out of those user inputs (add 1 as day).
This way I avoided to deep dive in Django steam pipes and even got rid of a datepicker!
But thanks to the #property, I can still keep the convenience of formatting dates in Django templates and make date calculation etc...
models.py :
NOTE : make use to return None if the date cannot be built out of filled fields, otherwise you'll have an unbound error when trying to render those #property field in a template.
class MyModel(models.Model):
YEAR_CHOICES = [(y,y) for y in range(1968, datetime.date.today().year+1)]
MONTH_CHOICE = [(m,m) for m in range(1,13)]
start_year = models.IntegerField(choices=YEAR_CHOICES,
default=datetime.datetime.now().year,)
start_month = models.IntegerField(choices=MONTH_CHOICE,
default=datetime.datetime.now().month,)
end_year = models.IntegerField(choices=YEAR_CHOICES,
default=datetime.datetime.now().year,)
end_month = models.IntegerField(choices=MONTH_CHOICE,
default=datetime.datetime.now().month,)
#property
def start_date(self):
if self.start_year and self.start_month:
return datetime.date(self.start_year, self.start_month, 1)
elif self.start_year:
return datetime.date(self.start_year, 12, 31)
else:
return None
#property
def end_date(self):
if self.end_year and self.end_month:
return datetime.date(self.end_year, self.end_month, 1)
elif self.end_year:
return datetime.date(self.end_year, 12, 31)
else:
return None
forms.py
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
exclude = ("blabla",)
Now, if you want to display a date in a form template, you can use :
{{ form.instance.start_date }}
Hope it helps & Suggestions are welcome!
it was not too clear for me, what are you doing exactly?
if you want to show a date string which has only years and month. that would be achieved by using template-filter date and a format e.g: {{ obj.created_at|date:'%y %m' }}.
but if I'm wrong and may you want to have a custom filed in your modelForm to pick a date(which only has year and month) then something like you've already did is the idea, but the code is the matter
I think your start_date and end_date function need to be changed:
def start_date(self):
if self.start_year and self.start_month:
return datetime.date(self.start_year, self.start_month, 1)
elif self.start_year:
# return Jan 1st
return datetime.date(self.start_year, 1, 1)
else:
return None
def end_date(self):
if self.end_year and self.end_month:
if self.end_month == 12:
# if month is December, return December 31st
return datetime.date(self.end_year, self.end_month, 31)
else:
# else, get first day of next month and subtract 1 day.
return datetime.date(self.end_year, self.end_month+1, 1) - datetime.timedelta(days=1)
elif self.end_year:
return datetime.date(self.end_year, 12, 31)
else:
return None

Django ORM: Conditional filtering

In a Django app, I keep daily scores of users in such a model:
class Score(models.Model):
user = models.ForeignKey(User)
score = models.PositiveIntegerField(default=0)
date = models.DateField(auto_now_add=True)
I want to find out the days when a user's score has changed drastically compared to a consequent day. That is, if for example, the user scores 10 times higher than the previous day.
How can I include such a condition in a query filter using Django ORM? Is it possible with a single query using conditional expressions as described here: https://docs.djangoproject.com/en/1.9/ref/models/conditional-expressions/
Thanks.
If you change your Score class slightly to include the previous day's score (which is admittedly pretty wasteful), you can pack the query into one line using F expressions.
Your new class:
class Score(models.Model):
user = models.ForeignKey(User)
score = models.PositiveIntegerField(default=0)
lastscore = models.PositiveIntegerField(default=0)
date = models.DateField(auto_now_add=True)
Then the filter becomes:
from django.db.models import F
daily_chg = 10
big_changes = Score.objects.filter(score__gt=daily_chg*F('lastscore'))
Instead of using timedeltas to search for and set the previous day's score field, I'd look into establishing an ordering via a Meta class and calling latest() when saving the current day's score.
Using timedelta we can test for the last week's days for a given user as such:
from my_app.models import Score
import datetime
def some_view(request):
days_with_score_boost = []
today = datetime.date.today()
for each_day in xrange(0,7):
day_to_test, day_before_to_test = today - datetime.timedelta(days=each_day), today - datetime.timedelta(days=each_day + 1)
day_before_score = Score.objects.get(user=request.user,date=today - datetime.timedelta(days=each_day)).score # will need to catch the exception raised if .get return None and process appropriately
if Score.objects.filter(user=request.user,score__gte=days_before_score * 10,date=day_before_to_test).count() > 0:
days_with_score_boost.append(day_to_test)
days_with_score_boost will be a list of datetime.date objects where the score increased by 10 or more from the day before.
In reponse to your comment, I'd make a measure that checks at save time whether the score boost has occured. However, I would get rid of auto_now_add in favor of writing in the save method.
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist
class Score(models.Model):
user = models.ForeignKey(User)
score = models.PositiveIntegerField(default=0)
date = models.DateField(null=True,blank=True)
increased_ten_over_previous_day = models.BooleanField(null=True,blank=True)
def save(self, *args, **kwargs):
self.date = timezone.now().today()
try:
yesterday_score = Score.objects.get(date=self.date-timezone.timedelta(days=1)).score
self.increased_ten_over_previous_day = (yesterday_score * 10) <= self.score
except ObjectDoesNotExist: # called if Score.object.get returns no object; requires you only have one score per user per date
self.increased_ten_over_previous_day = False
super(self, Score).save(*args, **kwargs)
Then you could filter objects for a date_range where increased_ten_over_previous_day is True.

Is it bad practice to return a tuple from a Django Manager method rather than a queryset?

I have some complex business logic that I have placed in a custom ModelManager. The manager method returns a tuple of values rather than a queryset. Is this considered bad practice? if so, what is the recommended approach. I do not want the logic in the View, and Django has no Service tier. Plus, my logic needs to potentially perform multiple queries.
The logic needs to select an Event closest to the current time, plus 3 events either side. When placed in the template, it is helpful to know the closest event as this is the event initially displayed in a full-screen slider.
The current call is as follows:
closest_event, previous_events, next_events = Event.objects.closest()
The logic does currently work fine. I am about to convert my app. to render the Event data as JSON in the template so that I can bootstrap a backbone.js View on page load. I plan to use TastyPie to render a Resource server-side into the template. Before I refactor my code, it would be good to know my current approach is not considered bad practice.
This is how my app. currently works:
views.py
class ClosestEventsListView(TemplateView):
template_name = 'events/event_list.html'
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
closest_event, previous_events, next_events = Event.objects.closest()
context['closest_event'] = closest_event
context['previous_events'] = previous_events
context['next_events'] = next_events
return self.render_to_response(context)
models.py
from datetime import timedelta
from django.db import models
from django.utils import timezone
from model_utils.models import TimeStampedModel
class ClosestEventsManager(models.Manager):
def closest(self, **kwargs):
"""
We are looking for the closest event to now plus the 3 events either side.
First select by date range until we have a count of 7 or greater
Initial range is 1 day eithee side, then widening by another day, if required
Then compare delta for each event data and determine the closest
Return closest event plus events either side
"""
now = timezone.now()
range_in_days = 1
size = 0
while size < 7:
start_time = now + timedelta(days=-range_in_days)
end_time = now + timedelta(days=range_in_days)
events = self.filter(date__gte=start_time, date__lte=end_time, **kwargs).select_related()
size = events.count()
range_in_days += 1
previous_delta = None
closest_event = None
previous_events = None
next_events = None
position = 0
for event in events:
delta = (event.date - now).total_seconds()
delta = delta * -1 if delta < 0 else delta
if previous_delta and previous_delta <= delta:
# we have found the closest event. Now, based on
# position get events either size
next_events = events[:position-1]
previous_events = events[position:]
break
previous_delta = delta
closest_event = event
position += 1
return closest_event, previous_events, next_events
class Event(TimeStampedModel):
class Meta:
ordering = ['-date']
topic = models.ForeignKey(Topic)
event_type = models.ForeignKey(EventType)
title = models.CharField(max_length=100)
slug = models.SlugField()
date = models.DateTimeField(db_index=True)
end_time = models.TimeField()
location = models.ForeignKey(Location)
twitter_hashtag = models.CharField(null=True, blank=True, max_length=100)
web_link = models.URLField(null=True, blank=True)
objects = ClosestEventsManager()
def __unicode__(self):
return self.title
I don't think it's bad practice to return a tuple. The first example in the ModelManager docs returns a list.
Saying that, if you want to build a queryset instead then you could do something like this -
def closest(self, **kwargs):
# get the events you want
return self.filter(pk__in=([event.id for event in events]))
It's fine, even Django's own get_or_create does it. Just make sure it's clear to whoever's using the function that it's not chainable (ie doesn't return a queryset).

How to extend Django generic year archive view?

I'm using Django's generic year archive view to display event objects by year. This may or may not be the best way to do this since I found that Django restricts the object list to the year being passed; my date range spans the current year into the next.
Here's my view:
class VisitingScholarsYearView(YearArchiveView):
allow_empty = True
allow_future = True
date_field = 'event_date'
template_name = "events/dvs_test.html"
context_object_name = 'event_list'
make_object_list = True
def get_queryset(self):
return Event.school_year_events.from_year(self.get_year()).filter(event_type__title='Distinguished Visiting Scholars Series')
Here's the manager on my model (an Event object with a DateField called event_date):
class SchoolYearManager(models.Manager):
def live_events(self, start_date, end_date):
return self.filter(status=self.model.LIVE).filter(event_date__range=(start_date, end_date))
def this_year(self):
now = datetime.datetime.now()
current_year = now.year
start_date = datetime.date(current_year, 7, 1)
end_date = datetime.date((current_year + 1), 6, 30)
return self.live_events(start_date, end_date)
def from_year(self, year):
start_date = datetime.date(int(year), 7, 1)
end_date = datetime.date((int(year) + 1), 6, 30)
return self.live_events(start_date, end_date)
And finally, my url for the view:
url(r'^distinguished-visiting-scholars-series/(?P<year>\d{4})/$', VisitingScholarsYearView.as_view()),
When I hit the API, I get the events I expect. But the YearArchiveView appears to limit the returned events to the year I give it; this is also expected, but I'd like it to span the range I refer to in the manager (ie July 1 to June 30 ).
How can I change this behavior? Or should I attempt a different view (ListView)?
I don't think you should use YearArchiveView as the base here - there's too much built-in logic around getting the objects for that date.
Instead, use ListView with YearMixin:
class VisitingScholarsYearView(YearMixin, ListView):