Commit ba5b66d1 authored by Thom Wiggers's avatar Thom Wiggers 📐
Browse files

Merge branch 'feature/events-api-django-rest' into 'master'

Add first version of api for getting the events and birthday in certain  range

This also serves as an example of how we can get the apis to work, which might be useful for the thalia apps.

See merge request !53
parents 98fb212a bfaaa549
......@@ -3,7 +3,8 @@ django-localflavor==1.3
Pillow
django-static-precompiler>=1.4,<2
django-sendfile==0.3.10
django-template-check # This should be in dev-requirements somehow
django-template-check>=0.3.0 # This should be in dev-requirements somehow
bleach==1.4.3
django-tinymce==2.3.0
pytz
djangorestframework==3.4.4
......@@ -6,7 +6,7 @@ skipsdist = True
changedir={toxinidir}/website
commands =
python manage.py check
python manage.py templatecheck
python manage.py templatecheck --project-only
python manage.py makemigrations --no-input --check --dry-run
python -Wall manage.py test
deps = -r{toxinidir}/requirements.txt
......
from django.utils import timezone
from django.urls import reverse
from rest_framework import serializers
from events.models import Event
class CalenderJSSerializer(serializers.ModelSerializer):
class Meta:
fields = (
'start', 'end', 'all_day', 'is_birthday',
'url', 'title', 'description',
'background_color', 'text_color', 'target_blank'
)
start = serializers.SerializerMethodField('_start')
end = serializers.SerializerMethodField('_end')
all_day = serializers.SerializerMethodField('_all_day')
is_birthday = serializers.SerializerMethodField('_is_birthday')
url = serializers.SerializerMethodField('_url')
title = serializers.SerializerMethodField('_title')
description = serializers.SerializerMethodField('_description')
background_color = serializers.SerializerMethodField('_background_color')
text_color = serializers.SerializerMethodField('_text_color')
target_blank = serializers.SerializerMethodField('_target_blank')
def _start(self, instance):
return timezone.localtime(instance.start)
def _end(self, instance):
return timezone.localtime(instance.end)
def _all_day(self, instance):
return False
def _is_birthday(self, instance):
return False
def _url(self, instance):
raise NotImplementedError
def _title(self, instance):
return instance.title
def _description(self, instance):
return instance.description
def _background_color(self, instance):
pass
def _text_color(self, instance):
pass
def _target_blank(self, instance):
return False
class EventSerializer(CalenderJSSerializer):
class Meta(CalenderJSSerializer.Meta):
model = Event
def _url(self, instance):
return reverse('#')
from rest_framework import routers
from events.api import viewsets
router = routers.SimpleRouter()
router.register(r'events', viewsets.EventViewset)
urlpatterns = router.urls
from rest_framework import viewsets
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from django.utils import timezone
from datetime import datetime
from events.api.serializers import EventSerializer
from events.models import Event
class EventViewset(viewsets.ViewSet):
queryset = Event.objects.all()
def list(self, request):
try:
start = timezone.make_aware(
datetime.strptime(request.query_params['start'], '%Y-%m-%d')
)
end = timezone.make_aware(
datetime.strptime(request.query_params['end'], '%Y-%m-%d')
)
except:
raise ParseError(detail='start or end query parameters invalid')
queryset = self.queryset.filter(
end__gte=start,
start__lte=end,
published=True
)
serializer = EventSerializer(queryset, many=True)
return Response(serializer.data)
from django.urls import reverse
from events.api.serializers import CalenderJSSerializer
from members.models import Member
class MemberBirthdaySerializer(CalenderJSSerializer):
class Meta(CalenderJSSerializer.Meta):
model = Member
def _start(self, instance):
return instance.birthday
def _end(self, instance):
pass
def _all_day(self, instance):
return True
def _is_birthday(self, instance):
return True
def _url(self, instance):
return reverse('#')
def _title(self, instance):
return instance.display_name()
def _description(self, instance):
membership = instance.current_membership
if membership and membership.type == 'honorary':
return instance.membership.get_type_display()
return ''
def _background_color(self, instance):
membership = instance.current_membership
if membership and membership.type == 'honorary':
return '#E62272'
return 'black'
def _text_color(self, instance):
return 'white'
from rest_framework import routers
from members.api import viewsets
router = routers.SimpleRouter()
router.register(r'members', viewsets.MemberViewset)
urlpatterns = router.urls
from django.utils import timezone
from rest_framework import viewsets
from rest_framework.decorators import list_route
from datetime import datetime
import copy
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from members.api.serializers import MemberBirthdaySerializer
from members.models import Member
class MemberViewset(viewsets.ViewSet):
queryset = Member.objects.all()
def _get_birthdays(self, member, start, end):
birthdays = []
start_year = max(start.year, member.birthday.year)
for year in range(start_year, end.year + 1):
bday = copy.deepcopy(member)
bday.birthday = bday.birthday.replace(year=year)
if start.date() <= bday.birthday <= end.date():
birthdays.append(bday)
return birthdays
@list_route()
def birthdays(self, request):
try:
start = timezone.make_aware(
datetime.strptime(request.query_params['start'], '%Y-%m-%d')
)
end = timezone.make_aware(
datetime.strptime(request.query_params['end'], '%Y-%m-%d')
)
except:
raise ParseError(detail='start or end query parameters invalid')
queryset = (
Member
.active_members
.with_birthdays_in_range(start, end)
.filter(show_birthday=True)
)
queryset.prefetch_related('membership_get')
all_birthdays = [
self._get_birthdays(m, start, end)
for m in queryset.all()
]
birthdays = [x for sublist in all_birthdays for x in sublist]
serializer = MemberBirthdaySerializer(birthdays, many=True)
return Response(serializer.data)
......@@ -130,5 +130,15 @@
"direct_debit_authorized": false,
"bank_account": ""
}
},
{
"model": "members.membership",
"pk": 1,
"fields": {
"type": "member",
"user": 1,
"since": "1980-01-01",
"until": null
}
}
]
......@@ -4,6 +4,9 @@ from django.db.models import Q
from django.core import validators
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from datetime import timedelta
import operator
from functools import reduce
from localflavor.generic.countries.sepa import IBAN_SEPA_COUNTRIES
from localflavor.generic.models import IBANField
......@@ -19,6 +22,25 @@ class ActiveMemberManager(models.Manager):
.filter(Q(user__membership__until__isnull=True) |
Q(user__membership__until__gt=timezone.now().date())))
def with_birthdays_in_range(self, from_date, to_date):
queryset = self.get_queryset().filter(birthday__lte=to_date)
if (to_date - from_date).days >= 366:
# 366 is important to also account for leap years
# Everyone that's born before to_date has a birthday
return queryset
delta = to_date - from_date
dates = [from_date + timedelta(days=i) for i in range(delta.days + 1)]
monthdays = [
{"birthday__month": d.month, "birthday__day": d.day}
for d in dates
]
# Don't get me started (basically, we are making a giant OR query with
# all days and months that are in the range)
query = reduce(operator.or_, [Q(**d) for d in monthdays])
return queryset.filter(query)
class Member(models.Model):
"""This class describes a member"""
......
# Create your tests here.
from datetime import datetime
from django.test import TestCase
from django.utils import timezone
from members.models import Member
class MemberBirthdayTest(TestCase):
fixtures = ['members.json']
def _make_date(self, date):
return timezone.make_aware(datetime.strptime(date, '%Y-%m-%d'))
def _get_members(self, start, end):
start_date = self._make_date(start)
end_date = self._make_date(end)
return Member.active_members.with_birthdays_in_range(
start_date, end_date
)
def _assert_none(self, start, end):
members = self._get_members(start, end)
self.assertEqual(len(members), 0)
def _assert_thom(self, start, end):
members = self._get_members(start, end)
self.assertEqual(len(members), 1)
self.assertEqual(members[0].get_full_name(), 'Thom Wiggers')
def test_one_year_contains_birthday(self):
self._assert_thom('2016-03-02', '2016-08-08')
def test_one_year_not_contains_birthday(self):
self._assert_none('2016-01-01', '2016-02-01')
def test_span_year_contains_birthday(self):
self._assert_thom('2015-08-09', '2016-08-08')
def test_span_year_not_contains_birthday(self):
self._assert_none('2015-12-25', '2016-03-01')
def test_span_multiple_years_contains_birthday(self):
self._assert_thom('2012-12-31', '2016-01-01')
def test_range_before_person_born(self):
self._assert_none('1985-12-12', '1985-12-13')
def test_person_born_in_range_in_one_year(self):
self._assert_thom('1993-01-01', '1993-04-01')
def test_person_born_in_range_spanning_one_year(self):
self._assert_thom('1992-12-31', '1993-04-01')
def test_person_born_in_range_spanning_multiple_years(self):
self._assert_thom('1992-12-31', '1995-01-01')
......@@ -46,6 +46,7 @@ INSTALLED_APPS = [
'static_precompiler',
'tinymce',
'django_template_check', # This is only necessary in development
'rest_framework',
# Our apps
'thaliawebsite', # include for admin settings
'members',
......
......@@ -63,6 +63,10 @@ urlpatterns = [
url(r'^career/', include('partners.urls', namespace='partners')),
url(r'^contact$', TemplateView.as_view(template_name='singlepages/contact.html'), name='contact'),
url(r'^private-thumbnails/(?P<size_fit>\d+x\d+_[01])/(?P<path>.*)', private_thumbnails, name='private-thumbnails'),
url(r'^api/', include([
url(r'^', include('events.api.urls')),
url(r'^', include('members.api.urls')),
])),
# Default login helpers
url(r'^', include('django.contrib.auth.urls')),
url(r'^i18n/', include('django.conf.urls.i18n')),
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment