diff --git a/src/bornhack/asgi.py b/src/bornhack/asgi.py new file mode 100644 index 00000000..af900642 --- /dev/null +++ b/src/bornhack/asgi.py @@ -0,0 +1,7 @@ +import os +from channels.asgi import get_channel_layer + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bornhack.settings") + +channel_layer = get_channel_layer() + diff --git a/src/bornhack/routing.py b/src/bornhack/routing.py new file mode 100644 index 00000000..7776c0e7 --- /dev/null +++ b/src/bornhack/routing.py @@ -0,0 +1,8 @@ +from program.consumers import ScheduleConsumer + + +channel_routing = [ + ScheduleConsumer.as_route(path=r"^/schedule/"), +] + + diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index 4006ef4e..aa0d6823 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -28,6 +28,8 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'django.contrib.sites', + 'channels', + 'profiles', 'camps', 'shop', @@ -108,9 +110,27 @@ MIDDLEWARE = [ if DEBUG: EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' - INSTALLED_APPS += ['debug_toolbar', ] + INSTALLED_APPS += [ + 'debug_toolbar', + 'channels_panel' + ] MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE INTERNAL_IPS = "127.0.0.1" + DEBUG_TOOLBAR_PANELS = [ + 'debug_toolbar.panels.versions.VersionsPanel', + 'debug_toolbar.panels.timer.TimerPanel', + 'debug_toolbar.panels.settings.SettingsPanel', + 'debug_toolbar.panels.headers.HeadersPanel', + 'debug_toolbar.panels.request.RequestPanel', + 'debug_toolbar.panels.sql.SQLPanel', + 'debug_toolbar.panels.staticfiles.StaticFilesPanel', + 'debug_toolbar.panels.templates.TemplatesPanel', + 'debug_toolbar.panels.cache.CachePanel', + 'debug_toolbar.panels.signals.SignalsPanel', + 'debug_toolbar.panels.logging.LoggingPanel', + 'debug_toolbar.panels.redirects.RedirectsPanel', + 'channels_panel.panel.ChannelsDebugPanel', + ] LOGGING = { 'version': 1, @@ -142,3 +162,9 @@ LOGGING = { }, } +CHANNEL_LAYERS = { + "default": { + "BACKEND": "asgiref.inmemory.ChannelLayer", + "ROUTING": "bornhack.routing.channel_routing", + }, +} diff --git a/src/program/admin.py b/src/program/admin.py index 81e2ee27..d72759d5 100644 --- a/src/program/admin.py +++ b/src/program/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Event, Speaker, EventType, EventInstance, EventLocation, SpeakerProposal, EventProposal +from .models import Event, Speaker, EventType, EventInstance, EventLocation, SpeakerProposal, EventProposal, Favorite @admin.register(SpeakerProposal) @@ -43,6 +43,11 @@ class SpeakerAdmin(admin.ModelAdmin): pass +@admin.register(Favorite) +class FavoriteAdmin(admin.ModelAdmin): + raw_id_fields = ('event_instance',) + + class SpeakerInline(admin.StackedInline): model = Speaker.events.through diff --git a/src/program/consumers.py b/src/program/consumers.py new file mode 100644 index 00000000..0501ea9f --- /dev/null +++ b/src/program/consumers.py @@ -0,0 +1,43 @@ +from channels.generic.websockets import JsonWebsocketConsumer + +from .models import EventInstance, Favorite + + +class ScheduleConsumer(JsonWebsocketConsumer): + http_user = True + + def connection_groups(self, **kwargs): + return ['schedule_users'] + + def connect(self, message, **kwargs): + self.send({"accept": True}) + + def raw_receive(self, message, **kwargs): + content = self.decode_json(message['text']) + action = content.get('action') + data = {} + + if action == 'get_event_instance': + event_instance_id = content.get('event_instance_id') + event_instance = EventInstance.objects.get(id=event_instance_id) + data['action'] = 'event_instance' + data['event_instance'] = event_instance.to_json(user=message.user) + + if action == 'favorite': + event_instance_id = content.get('event_instance_id') + event_instance = EventInstance.objects.get(id=event_instance_id) + Favorite.objects.create( + user=message.user, + event_instance=event_instance + ) + + if action == 'unfavorite': + event_instance_id = content.get('event_instance_id') + event_instance = EventInstance.objects.get(id=event_instance_id) + favorite = Favorite.objects.get(event_instance=event_instance, user=message.user) + favorite.delete() + + self.send(data) + + def disconnect(self, message, **kwargs): + pass diff --git a/src/program/migrations/0038_favorite.py b/src/program/migrations/0038_favorite.py new file mode 100644 index 00000000..0b9b8334 --- /dev/null +++ b/src/program/migrations/0038_favorite.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-15 23:21 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('program', '0037_eventtype_include_in_event_list'), + ] + + operations = [ + migrations.CreateModel( + name='Favorite', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='program.EventInstance')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/program/models.py b/src/program/models.py index e8c1c98a..33f98139 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -14,6 +14,7 @@ from django.apps import apps from django.core.files.base import ContentFile import icalendar +import CommonMark from utils.models import CreatedUpdatedModel, CampRelatedModel @@ -246,7 +247,7 @@ class EventProposal(UserSubmittedModel): # loop through the speakerproposals linked to this eventproposal and associate any related speaker objects with this event for sp in self.speakers.all(): if sp.speaker: - event.speaker_set.add(sp.speaker) + event.speakers.add(sp.speaker) self.proposal_status = eventproposalmodel.PROPOSAL_APPROVED self.save() @@ -375,8 +376,8 @@ class Event(CampRelatedModel): @property def speakers_list(self): - if self.speaker_set.exists(): - return ", ".join(self.speaker_set.all().values_list('name', flat=True)) + if self.speakers.exists(): + return ", ".join(self.speakers.all().values_list('name', flat=True)) return False def get_absolute_url(self): @@ -448,6 +449,33 @@ class EventInstance(CampRelatedModel): ievent['location'] = icalendar.vText(self.location.name) return ievent + def to_json(self, user=None): + parser = CommonMark.Parser() + renderer = CommonMark.HtmlRenderer() + ast = parser.parse(self.event.abstract) + abstract = renderer.render(ast) + + data = { + 'title': self.event.title, + 'event_slug': self.event.slug, + 'abstract': abstract, + 'from': self.when.lower.isoformat(), + 'to': self.when.lower.isoformat(), + 'url': str(self.event.get_absolute_url()), + 'id': self.id, + 'speakers': [ + { 'name': speaker.name + , 'url': str(speaker.get_absolute_url()) + } for speaker in self.event.speakers.all()] + } + + if user and user.is_authenticated: + is_favorited = user.favorites.filter(event_instance=self).exists() + data['is_favorited'] = is_favorited + + return data + + def get_speaker_picture_upload_path(instance, filename): """ We want speaker pictures are saved as MEDIA_ROOT/public/speakers/camp-slug/speaker-slug/filename """ @@ -501,6 +529,7 @@ class Speaker(CampRelatedModel): Event, blank=True, help_text='The event(s) this speaker is anchoring', + related_name='speakers' ) proposal = models.OneToOneField( @@ -526,3 +555,10 @@ class Speaker(CampRelatedModel): return reverse_lazy('speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) +class Favorite(models.Model): + user = models.ForeignKey('auth.User', related_name='favorites') + event_instance = models.ForeignKey('program.EventInstance') + + class Meta: + unique_together = ['user', 'event_instance'] + diff --git a/src/program/templates/event_list.html b/src/program/templates/event_list.html index 216aa945..c3ffac7a 100644 --- a/src/program/templates/event_list.html +++ b/src/program/templates/event_list.html @@ -28,7 +28,7 @@ {{ event.title }} - {% for speaker in event.speaker_set.all %} + {% for speaker in event.speakers.all %} {{ speaker.name }}
{% empty %} N/A diff --git a/src/program/templates/schedule_event_detail.html b/src/program/templates/schedule_event_detail.html index c53a6028..61b95a70 100644 --- a/src/program/templates/schedule_event_detail.html +++ b/src/program/templates/schedule_event_detail.html @@ -22,10 +22,10 @@
- {% if event.speaker_set.exists %} + {% if event.speakers.exists %}

Speakers

- {% for speaker in event.speaker_set.all %} + {% for speaker in event.speakers.all %}

{{ speaker.name }}

{% endfor %}
diff --git a/src/program/templates/schedule_overview.html b/src/program/templates/schedule_overview.html index 6cc55748..f95f3ece 100644 --- a/src/program/templates/schedule_overview.html +++ b/src/program/templates/schedule_overview.html @@ -1,6 +1,11 @@ {% extends 'schedule_base.html' %} {% load commonmark %} +{% load staticfiles %} + +{% block extra_head %} + +{% endblock %} {% block schedule_content %} {% if eventinstances %} @@ -12,9 +17,8 @@ + data-eventinstance-id="{{ eventinstance.id }}" + > {{ eventinstance.when.lower|date:"H:i" }} - {{ eventinstance.when.upper|date:"H:i" }} &#x{{ eventinstance.location.icon }};
@@ -23,20 +27,24 @@
{% endif %} -