From 26767d7d9ae22b56446b4ce30beb5ae9b168c56b Mon Sep 17 00:00:00 2001 From: Luuk Scholten Date: Wed, 24 Aug 2016 22:19:31 +0200 Subject: [PATCH 1/5] Add django rest framework as dependency --- requirements.txt | 1 + website/thaliawebsite/settings/settings.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 875459cd..2cb96698 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ django-template-check # This should be in dev-requirements somehow bleach==1.4.3 django-tinymce==2.3.0 pytz +djangorestframework==3.4.4 diff --git a/website/thaliawebsite/settings/settings.py b/website/thaliawebsite/settings/settings.py index 74217618..0aef63ab 100644 --- a/website/thaliawebsite/settings/settings.py +++ b/website/thaliawebsite/settings/settings.py @@ -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', -- GitLab From 841970ffdbd287ea2818e342a81ac1f63a19d1df Mon Sep 17 00:00:00 2001 From: Luuk Scholten Date: Wed, 24 Aug 2016 22:22:16 +0200 Subject: [PATCH 2/5] Add api for getting events in date range --- website/events/api/__init__.py | 0 website/events/api/serializers.py | 63 +++++++++++++++++++++++++++++++ website/events/api/urls.py | 7 ++++ website/events/api/viewsets.py | 32 ++++++++++++++++ website/thaliawebsite/urls.py | 3 ++ 5 files changed, 105 insertions(+) create mode 100644 website/events/api/__init__.py create mode 100644 website/events/api/serializers.py create mode 100644 website/events/api/urls.py create mode 100644 website/events/api/viewsets.py diff --git a/website/events/api/__init__.py b/website/events/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/website/events/api/serializers.py b/website/events/api/serializers.py new file mode 100644 index 00000000..247dee07 --- /dev/null +++ b/website/events/api/serializers.py @@ -0,0 +1,63 @@ +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('#') diff --git a/website/events/api/urls.py b/website/events/api/urls.py new file mode 100644 index 00000000..b9afc535 --- /dev/null +++ b/website/events/api/urls.py @@ -0,0 +1,7 @@ +from rest_framework import routers + +from events.api import viewsets + +router = routers.SimpleRouter() +router.register(r'events', viewsets.EventViewset) +urlpatterns = router.urls diff --git a/website/events/api/viewsets.py b/website/events/api/viewsets.py new file mode 100644 index 00000000..b0071a0a --- /dev/null +++ b/website/events/api/viewsets.py @@ -0,0 +1,32 @@ +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) diff --git a/website/thaliawebsite/urls.py b/website/thaliawebsite/urls.py index ebc89453..52b3c1ab 100644 --- a/website/thaliawebsite/urls.py +++ b/website/thaliawebsite/urls.py @@ -63,6 +63,9 @@ 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\d+x\d+_[01])/(?P.*)', private_thumbnails, name='private-thumbnails'), + url(r'^api/', include([ + url(r'^', include('events.api.urls')), + ])), # Default login helpers url(r'^', include('django.contrib.auth.urls')), url(r'^i18n/', include('django.conf.urls.i18n')), -- GitLab From cee45c7c27d5aab8ed44513d1423d7e39993abcb Mon Sep 17 00:00:00 2001 From: Luuk Scholten Date: Wed, 24 Aug 2016 22:26:58 +0200 Subject: [PATCH 3/5] Add api for getting birthdays of users --- website/members/api/__init__.py | 0 website/members/api/serializers.py | 42 ++++++++++++++++++++ website/members/api/urls.py | 7 ++++ website/members/api/viewsets.py | 56 ++++++++++++++++++++++++++ website/members/fixtures/members.json | 10 +++++ website/members/models.py | 22 +++++++++++ website/members/tests.py | 57 ++++++++++++++++++++++++++- website/thaliawebsite/urls.py | 1 + 8 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 website/members/api/__init__.py create mode 100644 website/members/api/serializers.py create mode 100644 website/members/api/urls.py create mode 100644 website/members/api/viewsets.py diff --git a/website/members/api/__init__.py b/website/members/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/website/members/api/serializers.py b/website/members/api/serializers.py new file mode 100644 index 00000000..cb953ff9 --- /dev/null +++ b/website/members/api/serializers.py @@ -0,0 +1,42 @@ +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' diff --git a/website/members/api/urls.py b/website/members/api/urls.py new file mode 100644 index 00000000..35d75933 --- /dev/null +++ b/website/members/api/urls.py @@ -0,0 +1,7 @@ +from rest_framework import routers + +from members.api import viewsets + +router = routers.SimpleRouter() +router.register(r'members', viewsets.MemberViewset) +urlpatterns = router.urls diff --git a/website/members/api/viewsets.py b/website/members/api/viewsets.py new file mode 100644 index 00000000..7ef80233 --- /dev/null +++ b/website/members/api/viewsets.py @@ -0,0 +1,56 @@ +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) diff --git a/website/members/fixtures/members.json b/website/members/fixtures/members.json index 3bd6f1c7..0902efbd 100644 --- a/website/members/fixtures/members.json +++ b/website/members/fixtures/members.json @@ -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 + } } ] diff --git a/website/members/models.py b/website/members/models.py index f5233b7c..eb92c90b 100644 --- a/website/members/models.py +++ b/website/members/models.py @@ -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""" diff --git a/website/members/tests.py b/website/members/tests.py index a39b155a..83feb290 100644 --- a/website/members/tests.py +++ b/website/members/tests.py @@ -1 +1,56 @@ -# 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.assertEquals(len(members), 0) + + def _assert_thom(self, start, end): + members = self._get_members(start, end) + self.assertEquals(len(members), 1) + self.assertEquals(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') diff --git a/website/thaliawebsite/urls.py b/website/thaliawebsite/urls.py index 52b3c1ab..74e8b66e 100644 --- a/website/thaliawebsite/urls.py +++ b/website/thaliawebsite/urls.py @@ -65,6 +65,7 @@ urlpatterns = [ url(r'^private-thumbnails/(?P\d+x\d+_[01])/(?P.*)', 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')), -- GitLab From 1b57de86d0ae2f36e1555d8002fb4f7b130821da Mon Sep 17 00:00:00 2001 From: Joost Rijneveld Date: Thu, 25 Aug 2016 11:15:01 +0200 Subject: [PATCH 4/5] Config CI to ignore errors in 3rd party templates This is necessary to ignore errors in the django rest framework. --- requirements.txt | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2cb96698..cf006cd8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ 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 diff --git a/tox.ini b/tox.ini index 10103f16..d177fe05 100644 --- a/tox.ini +++ b/tox.ini @@ -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 -- GitLab From bfaaa5491e9c176e9f895231a4c2421bb4abbe84 Mon Sep 17 00:00:00 2001 From: Thom Wiggers Date: Thu, 25 Aug 2016 13:01:04 +0200 Subject: [PATCH 5/5] Fix warnings about assertEqual vs Equals --- website/members/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/members/tests.py b/website/members/tests.py index 83feb290..f84acfe4 100644 --- a/website/members/tests.py +++ b/website/members/tests.py @@ -21,12 +21,12 @@ class MemberBirthdayTest(TestCase): def _assert_none(self, start, end): members = self._get_members(start, end) - self.assertEquals(len(members), 0) + self.assertEqual(len(members), 0) def _assert_thom(self, start, end): members = self._get_members(start, end) - self.assertEquals(len(members), 1) - self.assertEquals(members[0].get_full_name(), 'Thom Wiggers') + 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') -- GitLab