diff --git a/events/activity_pub/urls.py b/events/activity_pub/urls.py new file mode 100644 index 0000000..84ad6c6 --- /dev/null +++ b/events/activity_pub/urls.py @@ -0,0 +1,12 @@ +""" +ActivityPub compliant views +""" +from django.urls import path, include + +from . import views + +urlpatterns = [ + + path('events.json', views.events_list, name='ap-events-list'), + path('places.json', views.places_list, name='ap-places-list'), +] diff --git a/events/activity_pub/views.py b/events/activity_pub/views.py new file mode 100644 index 0000000..da395a0 --- /dev/null +++ b/events/activity_pub/views.py @@ -0,0 +1,140 @@ +import pytz +from collections import Mapping, OrderedDict + +from django.db import models +from django.shortcuts import resolve_url +from django.contrib.sites.models import Site +from django.utils import timezone + +from rest_framework import serializers +from rest_framework.decorators import api_view, throttle_classes +from rest_framework.response import Response +from rest_framework.utils import representation +from rest_framework.utils.serializer_helpers import ReturnDict + +from events.models import Event, Place, Team + + +def localized_time(dt, tz='UTC'): + event_tz = pytz.timezone(tz) + print("Searchable timezone: %s" % tz) + return dt.astimezone(event_tz).strftime("%Y-%m-%dT%H:%M:%S%z") + + +class CollectionSerializer(serializers.ListSerializer): + def to_representation(self, data): + """ + List of object instances -> List of dicts of primitive datatypes. + """ + # Dealing with nested relationships, data can be a Manager, + # so, first get a queryset from the Manager if needed + iterable = data.all() if isinstance(data, models.Manager) else data + + repr_data = OrderedDict({ + "@context": "https://www.w3.org/ns/activitystreams", + "summary": self.child.verbose_name_plural, + "type": "Collection", + "totalItems": len(iterable), + "items": [self.child.to_representation(item) for item in iterable], + }) + repr_data.move_to_end('@context', last=False) + repr_data.move_to_end('items') + return repr_data + + @property + def data(self): + ret = super(serializers.ListSerializer, self).data + return ReturnDict(ret, serializer=self) + + +class APGroupSerializer(serializers.ModelSerializer): + verbose_name_plural = 'Groups' + + context = serializers.CharField(label='context', default='https://www.w3.org/ns/activitystreams') + type = serializers.CharField(label='type', default='Group') + id = serializers.CharField(source='get_absolute_url') + name = serializers.CharField(source='__str__') + url = serializers.CharField(source='web_url') + + class Meta: + list_serializer_class = CollectionSerializer + model = Team + fields = ('context', 'type', 'id', 'name', 'url') + + def to_representation(self, instance): + data = super(APGroupSerializer, self).to_representation(instance) + data['@context'] = data['context'] + del data['context'] + data.move_to_end('@context', last=False) + + data['id'] = 'http://%s%s' % (Site.objects.get_current().domain, data['id']) + return data + + +class APPlaceSerializer(serializers.ModelSerializer): + verbose_name_plural = 'Places' + + context = serializers.CharField(label='context', default='https://www.w3.org/ns/activitystreams') + type = serializers.CharField(label='type', default='Place') + id = serializers.CharField(source='get_absolute_url') + name = serializers.CharField(source='__str__') + latitude = serializers.DecimalField(max_digits=10, decimal_places=5) + longitude = serializers.DecimalField(max_digits=10, decimal_places=5) + url = serializers.URLField(source='place_url') + + class Meta: + list_serializer_class = CollectionSerializer + model = Place + fields = ('context', 'type', 'id', 'name', 'latitude', 'longitude', 'url') + + def to_representation(self, instance): + data = super(APPlaceSerializer, self).to_representation(instance) + data['@context'] = data['context'] + del data['context'] + data.move_to_end('@context', last=False) + + data['id'] = 'http://%s%s' % (Site.objects.get_current().domain, data['id']) + return data + + +class APEventSerializer(serializers.ModelSerializer): + verbose_name_plural = 'Events' + + context = serializers.CharField( default='https://www.w3.org/ns/activitystreams') + type = serializers.CharField(default='Event') + id = serializers.CharField(source='get_absolute_url') + name = serializers.CharField() + attributedTo = APGroupSerializer(source='team') + location = APPlaceSerializer(source='place') + startTime = serializers.DateTimeField(source='start_time') + endTime = serializers.DateTimeField(source='end_time') + image = serializers.URLField(source='team.card_img_url') + url = serializers.URLField(source='web_url') + published = serializers.DateTimeField(source='created_time') + + class Meta: + list_serializer_class = CollectionSerializer + model = Event + fields = ('context', 'type', 'id', 'name', 'startTime', 'endTime', 'attributedTo', 'location', 'image', 'url', 'published') + + def to_representation(self, instance): + data = super(APEventSerializer, self).to_representation(instance) + data['@context'] = data['context'] + del data['context'] + data.move_to_end('@context', last=False) + + if data['image'].startswith('/'): + data['image'] = '/'.join(data['id'].split('/')[:3]) + data['image'] + + data['id'] = 'http://%s%s' % (Site.objects.get_current().domain, data['id']) + return data + +@api_view(['GET']) +def events_list(request): + serializer = APEventSerializer(Event.objects.filter(end_time__gte=timezone.now()), many=True) + return Response(serializer.data) + +@api_view(['GET']) +def places_list(request): + serializer = APPlaceSerializer(Place.objects.all(), many=True) + return Response(serializer.data) diff --git a/events/models/events.py b/events/models/events.py index 0da4f92..5ee8032 100644 --- a/events/models/events.py +++ b/events/models/events.py @@ -33,6 +33,9 @@ class Place(models.Model): place_url = models.URLField(help_text=_('URL for the Place Homepage'), verbose_name=_('URL of the Place'), max_length=200, blank=True, null=True) cover_img = models.URLField(_("Place photo"), null=True, blank=True) + def get_absolute_url(self): + return reverse('show-place', kwargs={'place_id': self.id}) + def __str__(self): return u'%s, %s' % (self.name, self.city.name) diff --git a/events/models/profiles.py b/events/models/profiles.py index 81c73d6..ff92fd5 100644 --- a/events/models/profiles.py +++ b/events/models/profiles.py @@ -3,6 +3,7 @@ from django.contrib.sites.models import Site from django.contrib.auth.models import User, Group, AnonymousUser from django.contrib.staticfiles.templatetags.staticfiles import static from django.utils.translation import ugettext_lazy as _ +from django.shortcuts import reverse from django.utils import timezone from django.conf import settings @@ -300,6 +301,9 @@ class Team(models.Model): def moderators(self): return [member.user for member in Member.objects.filter(team=self, role__in=(Member.ADMIN, Member.MODERATOR))] + def get_absolute_url(self): + return reverse('show-team', kwargs={'team_id': self.id}) + def __str__(self): return u'%s' % (self.name) diff --git a/events/models/search.py b/events/models/search.py index 90846b5..3c60633 100644 --- a/events/models/search.py +++ b/events/models/search.py @@ -36,7 +36,7 @@ class Searchable(models.Model): @property def local_start_time(self, val=None): if val is not None: - self.start_time = val.astimezone(python.utc) + self.start_time = val.astimezone(pytz.utc) else: if self.start_time is None: return None @@ -46,7 +46,7 @@ class Searchable(models.Model): @property def local_end_time(self, val=None): if val is not None: - self.end_time = val.astimezone(python.utc) + self.end_time = val.astimezone(pytz.utc) else: if self.end_time is None: return None diff --git a/get_together/urls.py b/get_together/urls.py index bfec869..43d5e27 100644 --- a/get_together/urls.py +++ b/get_together/urls.py @@ -110,6 +110,8 @@ urlpatterns = [ path('oauth/', include('social_django.urls', namespace='social')), + path('activity_pub/', include('events.activity_pub.urls')), + path('/', views.show_team_by_slug, name='show-team-by-slug'), ] if settings.DEBUG: