From 437134991d652c41588ca402593cf9224983d036 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Sat, 21 Apr 2018 10:28:04 -0400 Subject: [PATCH 1/7] Add initial speaker/talk models, expand Category and Topic to they can be used for this, add a user's default City to their profile --- events/admin.py | 9 +- events/forms.py | 16 ++- events/migrations/0026_add_user_city.py | 34 ++++++ .../0027_add_category_topic_slug.py | 31 +++++ events/migrations/0028_add_speaker_models.py | 113 ++++++++++++++++++ events/models/events.py | 29 ++--- events/models/locale.py | 1 + events/models/profiles.py | 47 +++++++- events/utils.py | 21 ++++ .../get_together/users/edit_profile.html | 20 ++++ .../get_together/users/show_profile.html | 10 +- get_together/views/__init__.py | 5 + 12 files changed, 312 insertions(+), 24 deletions(-) create mode 100644 events/migrations/0026_add_user_city.py create mode 100644 events/migrations/0027_add_category_topic_slug.py create mode 100644 events/migrations/0028_add_speaker_models.py create mode 100644 events/utils.py diff --git a/events/admin.py b/events/admin.py index d0375c4..0d615a5 100644 --- a/events/admin.py +++ b/events/admin.py @@ -24,6 +24,7 @@ class CityAdmin(admin.ModelAdmin): admin.site.register(City, CityAdmin) class ProfileAdmin(admin.ModelAdmin): + raw_id_fields = ('city',) list_display = ('user', 'realname', 'avatar', 'web_url') admin.site.register(UserProfile, ProfileAdmin) @@ -101,12 +102,16 @@ class AttendeeAdmin(admin.ModelAdmin): admin.site.register(Attendee, AttendeeAdmin) class CategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'image') + list_display = ('name', 'slug', 'image') + exclude = ('slug', ) def image(self, obj): return (mark_safe('' % (obj.img_url, obj.name))) image.short_description = 'Image' admin.site.register(Category, CategoryAdmin) class TopicAdmin(admin.ModelAdmin): - list_display = ('name', 'category') + list_display = ('name', 'slug', 'category') list_filter = ('category',) + exclude = ('slug', ) +admin.site.register(Topic, TopicAdmin) + diff --git a/events/forms.py b/events/forms.py index ad28d25..7dba7a0 100644 --- a/events/forms.py +++ b/events/forms.py @@ -262,15 +262,27 @@ class UserForm(forms.ModelForm): class UserProfileForm(forms.ModelForm): class Meta: model = UserProfile - fields = ['avatar', 'realname', 'tz', 'send_notifications'] + fields = ['avatar', 'realname', 'city', 'tz', 'send_notifications'] labels = { 'send_notifications': _('Send me notification emails'), } + widgets = { + 'city': Lookup(source=City), + } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['city'].required = True class ConfirmProfileForm(forms.ModelForm): class Meta: model = UserProfile - fields = ['avatar', 'realname', 'tz'] + fields = ['avatar', 'realname', 'city'] + widgets = { + 'city': Lookup(source=City), + } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['city'].required = True class SendNotificationsForm(forms.ModelForm): class Meta: diff --git a/events/migrations/0026_add_user_city.py b/events/migrations/0026_add_user_city.py new file mode 100644 index 0000000..2d5e919 --- /dev/null +++ b/events/migrations/0026_add_user_city.py @@ -0,0 +1,34 @@ +# Generated by Django 2.0 on 2018-04-21 13:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0025_add_event_series'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='city', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='events.City'), + ), + migrations.AlterField( + model_name='eventseries', + name='end_time', + field=models.TimeField(db_index=True, help_text='Local time that the event ends', verbose_name='End Time'), + ), + migrations.AlterField( + model_name='eventseries', + name='start_time', + field=models.TimeField(db_index=True, help_text='Local time that the event starts', verbose_name='Start Time'), + ), + migrations.AlterField( + model_name='team', + name='description', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/events/migrations/0027_add_category_topic_slug.py b/events/migrations/0027_add_category_topic_slug.py new file mode 100644 index 0000000..af013a9 --- /dev/null +++ b/events/migrations/0027_add_category_topic_slug.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0 on 2018-04-21 14:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0026_add_user_city'), + ] + + operations = [ + migrations.AddField( + model_name='topic', + name='slug', + field=models.CharField(default='-', max_length=256), + preserve_default=False, + ), + migrations.AddField( + model_name='category', + name='slug', + field=models.CharField(default='-', max_length=256), + preserve_default=False, + ), + migrations.AlterField( + model_name='userprofile', + name='city', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='events.City', verbose_name='Home city'), + ), + ] diff --git a/events/migrations/0028_add_speaker_models.py b/events/migrations/0028_add_speaker_models.py new file mode 100644 index 0000000..34d4625 --- /dev/null +++ b/events/migrations/0028_add_speaker_models.py @@ -0,0 +1,113 @@ +# Generated by Django 2.0 on 2018-04-21 14:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0027_add_category_topic_slug'), + ] + + operations = [ + migrations.CreateModel( + name='Presentation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')), + ], + ), + migrations.CreateModel( + name='Speaker', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bio', models.TextField(blank=True)), + ], + ), + migrations.CreateModel( + name='SpeakerRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')), + ], + ), + migrations.CreateModel( + name='Talk', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=256)), + ('abstract', models.TextField()), + ('talk_type', models.SmallIntegerField(choices=[(0, 'Presentation'), (1, 'Workshop'), (2, 'Panel'), (3, 'Roundtable'), (4, 'Q & A'), (5, 'Demonstration')], default=0, verbose_name='Type')), + ('web_url', models.URLField(blank=True, null=True, verbose_name='Website')), + ], + ), + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'Categories'}, + ), + migrations.AlterModelOptions( + name='country', + options={'ordering': ('name',), 'verbose_name_plural': 'Countries'}, + ), + migrations.AlterModelOptions( + name='eventseries', + options={'verbose_name_plural': 'Event series'}, + ), + migrations.AlterField( + model_name='category', + name='slug', + field=models.CharField(blank=True, max_length=256), + ), + migrations.AlterField( + model_name='topic', + name='description', + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name='topic', + name='slug', + field=models.CharField(blank=True, max_length=256), + ), + migrations.AddField( + model_name='talk', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='events.Category'), + ), + migrations.AddField( + model_name='talk', + name='speaker', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Speaker'), + ), + migrations.AddField( + model_name='talk', + name='topics', + field=models.ManyToManyField(blank=True, to='events.Topic'), + ), + migrations.AddField( + model_name='speakerrequest', + name='topics', + field=models.ManyToManyField(blank=True, to='events.Topic'), + ), + migrations.AddField( + model_name='speaker', + name='categories', + field=models.ManyToManyField(blank=True, to='events.Category'), + ), + migrations.AddField( + model_name='speaker', + name='topics', + field=models.ManyToManyField(blank=True, to='events.Topic'), + ), + migrations.AddField( + model_name='speaker', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.UserProfile'), + ), + migrations.AddField( + model_name='presentation', + name='talk', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='events.Talk'), + ), + ] diff --git a/events/models/events.py b/events/models/events.py index 5e0d7cb..ea27091 100644 --- a/events/models/events.py +++ b/events/models/events.py @@ -11,6 +11,7 @@ from recurrence.fields import RecurrenceField from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFill +from ..utils import slugify from .locale import * from .profiles import * from .search import * @@ -19,11 +20,8 @@ from .. import location import re import pytz import datetime -import unicodedata import hashlib -SLUG_OK = '-_~' - class Place(models.Model): name = models.CharField(help_text=_('Name of the Place'), max_length=150) city = models.ForeignKey(City, verbose_name=_('City'), on_delete=models.CASCADE) @@ -192,21 +190,6 @@ def delete_event_searchable(event): pass -def slugify(s, ok=SLUG_OK, lower=True, spaces=False): - # L and N signify letter/number. - # http://www.unicode.org/reports/tr44/tr44-4.html#GC_Values_Table - rv = [] - for c in unicodedata.normalize('NFKC', s): - cat = unicodedata.category(c)[0] - if cat in 'LN' or c in ok: - rv.append(c) - if cat == 'Z': # space - rv.append(' ') - new = ''.join(rv).strip() - if not spaces: - new = re.sub('[-\s]+', '-', new) - return new.lower() if lower else new - class Attendee(models.Model): NORMAL=0 CREW=1 @@ -311,6 +294,8 @@ class CommonEvent(models.Model): return self.name class EventSeries(models.Model): + class Meta: + verbose_name_plural = 'Event series' name = models.CharField(max_length=150, verbose_name=_('Event Name')) team = models.ForeignKey(Team, on_delete=models.CASCADE) parent = models.ForeignKey('CommonEvent', related_name='planned_events', null=True, blank=True, on_delete=models.SET_NULL) @@ -391,4 +376,12 @@ class EventSeries(models.Model): def __str__(self): return u'%s by %s at %s' % (self.name, self.team.name, self.start_time) +class SpeakerRequest(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + topics = models.ManyToManyField('Topic', blank=True) + +class Presentation(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + talk = models.ForeignKey(Talk, on_delete=models.SET_NULL, blank=False, null=True) + start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True) diff --git a/events/models/locale.py b/events/models/locale.py index 7e48fa0..86678ec 100644 --- a/events/models/locale.py +++ b/events/models/locale.py @@ -32,6 +32,7 @@ class Country(models.Model): class Meta: ordering = ('name',) + verbose_name_plural = 'Countries' def __str__(self): return u'%s' % (self.name) diff --git a/events/models/profiles.py b/events/models/profiles.py index ab9bfbf..8dc018f 100644 --- a/events/models/profiles.py +++ b/events/models/profiles.py @@ -10,6 +10,7 @@ from imagekit.processors import ResizeToFill from .locale import * from .. import location +from ..utils import slugify import uuid import pytz @@ -27,6 +28,7 @@ class UserProfile(models.Model): processors=[ResizeToFill(128, 128)], format='PNG', blank=True) + city = models.ForeignKey(City, verbose_name=_('Home city'), null=True, blank=True, on_delete=models.CASCADE) web_url = models.URLField(verbose_name=_('Website URL'), blank=True, null=True) twitter = models.CharField(verbose_name=_('Twitter Name'), max_length=32, blank=True, null=True) @@ -275,17 +277,60 @@ class Member(models.Model): class Category(models.Model): name = models.CharField(max_length=256) description = models.TextField() + slug = models.CharField(max_length=256, blank=True) img_url = models.URLField(blank=False, null=False) + class Meta: + verbose_name_plural = 'Categories' def __str__(self): return self.name + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + super().save(*args, **kwargs) + class Topic(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE, null=False, blank=False) name = models.CharField(max_length=256) - description = models.TextField() + slug = models.CharField(max_length=256, blank=True) + description = models.TextField(blank=True) def __str__(self): return self.name + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + super().save(*args, **kwargs) + +class Speaker(models.Model): + user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) + bio = models.TextField(blank=True) + + categories = models.ManyToManyField('Category', blank=True) + topics = models.ManyToManyField('Topic', blank=True) + +class Talk(models.Model): + PRESENTATION=0 + WORKSHOP=1 + PANEL=2 + ROUNDTABLE=3 + QANDA=4 + DEMO=5 + TYPES = [ + (PRESENTATION, _("Presentation")), + (WORKSHOP, _("Workshop")), + (PANEL, _("Panel")), + (ROUNDTABLE, _("Roundtable")), + (QANDA, _("Q & A")), + (DEMO, _("Demonstration")), + ] + speaker = models.ForeignKey(Speaker, on_delete=models.CASCADE) + title = models.CharField(max_length=256) + abstract = models.TextField() + talk_type = models.SmallIntegerField(_("Type"), choices=TYPES, default=PRESENTATION) + web_url = models.URLField(_("Website"), null=True, blank=True) + + category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) + topics = models.ManyToManyField('Topic', blank=True) + diff --git a/events/utils.py b/events/utils.py new file mode 100644 index 0000000..9a81344 --- /dev/null +++ b/events/utils.py @@ -0,0 +1,21 @@ +import re +import unicodedata + +SLUG_OK = '-_~' + +def slugify(s, ok=SLUG_OK, lower=True, spaces=False): + # L and N signify letter/number. + # http://www.unicode.org/reports/tr44/tr44-4.html#GC_Values_Table + rv = [] + s = re.sub('\s*&\s*', ' and ', s) + for c in unicodedata.normalize('NFKC', s): + cat = unicodedata.category(c)[0] + if cat in 'LN' or c in ok: + rv.append(c) + if cat == 'Z': # space + rv.append(' ') + new = ''.join(rv).strip() + if not spaces: + new = re.sub('[-\s]+', '-', new) + return new.lower() if lower else new + diff --git a/get_together/templates/get_together/users/edit_profile.html b/get_together/templates/get_together/users/edit_profile.html index a9a3440..218b308 100644 --- a/get_together/templates/get_together/users/edit_profile.html +++ b/get_together/templates/get_together/users/edit_profile.html @@ -1,4 +1,5 @@ {% extends "get_together/base.html" %} +{% load static %} {% block content %}
@@ -30,8 +31,27 @@ {% endblock %} {% block javascript %} + diff --git a/get_together/templates/get_together/users/show_profile.html b/get_together/templates/get_together/users/show_profile.html index 562817c..c36cfcf 100644 --- a/get_together/templates/get_together/users/show_profile.html +++ b/get_together/templates/get_together/users/show_profile.html @@ -24,7 +24,7 @@
{% if teams %} -

Your Teams

+

Teams

{% endif %} +

Categories

+
diff --git a/get_together/views/__init__.py b/get_together/views/__init__.py index 32cf08c..9c5e8fe 100644 --- a/get_together/views/__init__.py +++ b/get_together/views/__init__.py @@ -72,6 +72,11 @@ def home(request, *args, **kwards): city_distance += 1 else: city = sorted(nearby_cities, key=lambda city: location.city_distance_from(ll, city))[0] + + if request.user.profile.city is None: + profile = request.user.profile + profile.city = city + profile.save() except: pass # City lookup failed From e45c0370a1ad99d3a4912699880922052238a0c0 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Sat, 21 Apr 2018 22:14:15 -0400 Subject: [PATCH 2/7] Add admin for Speaker, Presenter and Talk --- events/admin.py | 26 ++++++++++++++++++++++++-- events/models/events.py | 7 +++++-- events/models/profiles.py | 5 +++++ get_together/views/events.py | 31 +++++++++++++------------------ 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/events/admin.py b/events/admin.py index 0d615a5..01da08d 100644 --- a/events/admin.py +++ b/events/admin.py @@ -3,9 +3,27 @@ from django.utils.safestring import mark_safe # Register your models here. from .models.locale import Language, Continent, Country, SPR, City -from .models.profiles import UserProfile, Organization, Team, Member, Category, Topic +from .models.profiles import ( + UserProfile, + Organization, + Team, + Member, + Category, + Topic, + Speaker, + Talk, +) from .models.search import Searchable -from .models.events import Place, Event, EventComment, EventSeries, EventPhoto, CommonEvent, Attendee +from .models.events import ( + Place, + Event, + EventComment, + EventSeries, + EventPhoto, + CommonEvent, + Attendee, + Presentation, +) admin.site.register(Language) admin.site.register(Continent) @@ -115,3 +133,7 @@ class TopicAdmin(admin.ModelAdmin): exclude = ('slug', ) admin.site.register(Topic, TopicAdmin) +admin.site.register(Speaker) +admin.site.register(Talk) +admin.site.register(Presentation) + diff --git a/events/models/events.py b/events/models/events.py index ea27091..335ff77 100644 --- a/events/models/events.py +++ b/events/models/events.py @@ -381,7 +381,10 @@ class SpeakerRequest(models.Model): topics = models.ManyToManyField('Topic', blank=True) class Presentation(models.Model): - event = models.ForeignKey(Event, on_delete=models.CASCADE) - talk = models.ForeignKey(Talk, on_delete=models.SET_NULL, blank=False, null=True) + event = models.ForeignKey(Event, related_name='presentations', on_delete=models.CASCADE) + talk = models.ForeignKey(Talk, related_name='presentations', on_delete=models.SET_NULL, blank=False, null=True) start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True) + def __str__(self): + return '%s at %s' % (self.talk.title, self.event.name) + diff --git a/events/models/profiles.py b/events/models/profiles.py index 8dc018f..e595701 100644 --- a/events/models/profiles.py +++ b/events/models/profiles.py @@ -309,6 +309,9 @@ class Speaker(models.Model): categories = models.ManyToManyField('Category', blank=True) topics = models.ManyToManyField('Topic', blank=True) + def __str__(self): + return self.user.__str__() + class Talk(models.Model): PRESENTATION=0 WORKSHOP=1 @@ -333,4 +336,6 @@ class Talk(models.Model): category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) topics = models.ManyToManyField('Topic', blank=True) + def __str__(self): + return self.title diff --git a/get_together/views/events.py b/get_together/views/events.py index 3a1ec5d..26c5da5 100644 --- a/get_together/views/events.py +++ b/get_together/views/events.py @@ -7,8 +7,17 @@ from django.shortcuts import render, redirect, reverse, get_object_or_404 from django.http import HttpResponse, JsonResponse from django.utils import timezone -from events.models.events import Event, CommonEvent, EventSeries, EventPhoto, Place, Attendee, update_event_searchable, \ - delete_event_searchable +from events.models.events import ( + Event, + CommonEvent, + EventSeries, + EventPhoto, + Place, + Attendee, + Presentation, + update_event_searchable, + delete_event_searchable, +) from events.models.profiles import Team, Organization, UserProfile, Member from events.forms import ( TeamEventForm, @@ -20,7 +29,7 @@ from events.forms import ( NewPlaceForm, UploadEventPhotoForm, NewCommonEventForm - ) +) from events import location import datetime @@ -56,6 +65,7 @@ def show_event(request, event_id, event_slug): 'comment_form': comment_form, 'is_attending': request.user.profile in event.attendees.all(), 'attendee_list': Attendee.objects.filter(event=event), + 'presentation_list': event.presentations.all().order_by('start_time'), 'can_edit_event': request.user.profile.can_edit_event(event), } return render(request, 'get_together/events/show_event.html', context) @@ -220,13 +230,6 @@ def add_place_to_series(request, series_id): else: return redirect('home') -def share_event(request, event_id): - event = get_object_or_404(Event, id=event_id) - context = { - 'event': event, - } - return render(request, 'get_together/events/share_event.html', context) - def edit_event(request, event_id): event = get_object_or_404(Event, id=event_id) @@ -391,14 +394,6 @@ def create_common_event(request, org_slug): else: return redirect('home') -def share_common_event(request, event_id): - event = get_object_or_404(CommonEvent, id=event_id) - context = { - 'event': event, - } - return render(request, 'get_together/orgs/share_common_event.html', context) - -@login_required def create_common_event_team_select(request, event_id): teams = request.user.profile.moderating if len(teams) == 1: From 5f16e176f9f1f7b8f74c8bec9268f9c3973032f0 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Tue, 24 Apr 2018 10:22:16 -0400 Subject: [PATCH 3/7] Add user management of their speaker bios and talks --- events/admin.py | 16 +- events/forms.py | 31 ++- events/migrations/0028_add_speaker_models.py | 15 +- events/models/events.py | 15 +- events/models/profiles.py | 30 ++- .../get_together/events/show_event.html | 10 + .../get_together/speakers/create_speaker.html | 20 ++ .../get_together/speakers/create_talk.html | 31 +++ .../get_together/speakers/delete_talk.html | 20 ++ .../get_together/speakers/edit_speaker.html | 46 ++++ .../get_together/speakers/edit_talk.html | 32 +++ .../speakers/list_user_presentations.html | 64 ++++++ .../speakers/list_user_talks.html | 57 +++++ .../get_together/users/show_profile.html | 37 +++- get_together/urls.py | 13 ++ get_together/views/__init__.py | 1 + get_together/views/events.py | 2 +- get_together/views/speakers.py | 200 ++++++++++++++++++ 18 files changed, 625 insertions(+), 15 deletions(-) create mode 100644 get_together/templates/get_together/speakers/create_speaker.html create mode 100644 get_together/templates/get_together/speakers/create_talk.html create mode 100644 get_together/templates/get_together/speakers/delete_talk.html create mode 100644 get_together/templates/get_together/speakers/edit_speaker.html create mode 100644 get_together/templates/get_together/speakers/edit_talk.html create mode 100644 get_together/templates/get_together/speakers/list_user_presentations.html create mode 100644 get_together/templates/get_together/speakers/list_user_talks.html create mode 100644 get_together/views/speakers.py diff --git a/events/admin.py b/events/admin.py index 01da08d..97e95f9 100644 --- a/events/admin.py +++ b/events/admin.py @@ -133,7 +133,17 @@ class TopicAdmin(admin.ModelAdmin): exclude = ('slug', ) admin.site.register(Topic, TopicAdmin) -admin.site.register(Speaker) -admin.site.register(Talk) -admin.site.register(Presentation) +class SpeakerAdmin(admin.ModelAdmin): + list_display = ('title', 'user', 'avatar') +admin.site.register(Speaker, SpeakerAdmin) + +class TalkAdmin(admin.ModelAdmin): + list_display = ('title', 'speaker', 'category') + list_filter = ('category',) +admin.site.register(Talk, TalkAdmin) + +class PresentationAdmin(admin.ModelAdmin): + list_display = ('talk', 'status', 'event') + list_filter = ('status',) +admin.site.register(Presentation, PresentationAdmin) diff --git a/events/forms.py b/events/forms.py index 7dba7a0..b16a66a 100644 --- a/events/forms.py +++ b/events/forms.py @@ -6,8 +6,17 @@ from django.utils import timezone from django.contrib.auth.models import User from .models.locale import Country, SPR, City -from .models.profiles import Team, UserProfile -from .models.events import Event, EventComment ,CommonEvent, EventSeries, Place, EventPhoto +from .models.profiles import Team, UserProfile, Speaker, Talk +from .models.events import ( + Event, + EventComment, + CommonEvent, + EventSeries, + Place, + EventPhoto, + Presentation, + SpeakerRequest, +) import recurrence import pytz @@ -329,3 +338,21 @@ class NewCommonEventForm(forms.ModelForm): 'end_time': DateTimeWidget } +class SpeakerBioForm(forms.ModelForm): + class Meta: + model = Speaker + fields = ['avatar', 'title', 'bio', 'categories'] + +class UserTalkForm(forms.ModelForm): + class Meta: + model = Talk + fields = ['speaker', 'title', 'abstract', 'talk_type', 'web_url', 'category'] + +class DeleteTalkForm(forms.Form): + confirm = forms.BooleanField(label="Yes, delete series", required=True) + +class SchedulePresentationForm(forms.ModelForm): + class Meta: + model = Presentation + fields = ['start_time'] + diff --git a/events/migrations/0028_add_speaker_models.py b/events/migrations/0028_add_speaker_models.py index 34d4625..a71d4b6 100644 --- a/events/migrations/0028_add_speaker_models.py +++ b/events/migrations/0028_add_speaker_models.py @@ -1,7 +1,9 @@ -# Generated by Django 2.0 on 2018-04-21 14:26 +# Generated by Django 2.0 on 2018-04-22 21:23 from django.db import migrations, models import django.db.models.deletion +import django.utils.timezone +import imagekit.models.fields class Migration(migrations.Migration): @@ -15,14 +17,19 @@ class Migration(migrations.Migration): name='Presentation', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')), + ('status', models.SmallIntegerField(choices=[(-1, 'Declined'), (0, 'Proposed'), (1, 'Accepted')], db_index=True, default=0)), + ('start_time', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Start Time')), + ('created_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='events.UserProfile')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='presentations', to='events.Event')), ], ), migrations.CreateModel( name='Speaker', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('avatar', imagekit.models.fields.ProcessedImageField(blank=True, upload_to='avatars', verbose_name='Photo Image')), + ('title', models.CharField(blank=True, max_length=256, null=True)), ('bio', models.TextField(blank=True)), ], ), @@ -108,6 +115,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='presentation', name='talk', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='events.Talk'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='presentations', to='events.Talk'), ), ] diff --git a/events/models/events.py b/events/models/events.py index 335ff77..47aba50 100644 --- a/events/models/events.py +++ b/events/models/events.py @@ -381,9 +381,22 @@ class SpeakerRequest(models.Model): topics = models.ManyToManyField('Topic', blank=True) class Presentation(models.Model): + DECLINED=-1 + PROPOSED=0 + ACCEPTED=1 + + STATUSES = [ + (DECLINED, _("Declined")), + (PROPOSED, _("Proposed")), + (ACCEPTED, _("Accepted")), + ] event = models.ForeignKey(Event, related_name='presentations', on_delete=models.CASCADE) talk = models.ForeignKey(Talk, related_name='presentations', on_delete=models.SET_NULL, blank=False, null=True) - start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True) + status = models.SmallIntegerField(choices=STATUSES, default=PROPOSED, db_index=True) + start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True, null=True, blank=True) + + created_by = models.ForeignKey(UserProfile, on_delete=models.SET_NULL, null=True, blank=False) + created_time = models.DateTimeField(default=timezone.now, db_index=True) def __str__(self): return '%s at %s' % (self.talk.title, self.event.name) diff --git a/events/models/profiles.py b/events/models/profiles.py index e595701..2a8bb67 100644 --- a/events/models/profiles.py +++ b/events/models/profiles.py @@ -86,6 +86,10 @@ class UserProfile(models.Model): def moderating(self): return [member.team for member in Member.objects.filter(user=self, role__in=(Member.ADMIN, Member.MODERATOR))] + @property + def talks(self): + return Talk.objects.filter(speaker__user=self) + def can_create_event(self, team): try: if self.user.is_superuser: @@ -304,12 +308,26 @@ class Topic(models.Model): class Speaker(models.Model): user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) - bio = models.TextField(blank=True) + avatar = ProcessedImageField(verbose_name=_("Photo Image"), + upload_to='avatars', + processors=[ResizeToFill(128, 128)], + format='PNG', + blank=True) + title = models.CharField(max_length=256, blank=True, null=True) + bio = models.TextField(verbose_name=_('Biography'), blank=True) categories = models.ManyToManyField('Category', blank=True) topics = models.ManyToManyField('Topic', blank=True) + def headshot(self): + if self.avatar: + return self.avatar + else: + return self.user.avatar + def __str__(self): + if self.title: + return self.title return self.user.__str__() class Talk(models.Model): @@ -327,7 +345,7 @@ class Talk(models.Model): (QANDA, _("Q & A")), (DEMO, _("Demonstration")), ] - speaker = models.ForeignKey(Speaker, on_delete=models.CASCADE) + speaker = models.ForeignKey(Speaker, verbose_name=_('Speaker Bio'), on_delete=models.CASCADE) title = models.CharField(max_length=256) abstract = models.TextField() talk_type = models.SmallIntegerField(_("Type"), choices=TYPES, default=PRESENTATION) @@ -336,6 +354,14 @@ class Talk(models.Model): category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) topics = models.ManyToManyField('Topic', blank=True) + @property + def future_presentations(self): + return self.presentations.filter(status__gte=0, event__start_time__gt=timezone.now()) + + @property + def past_presentations(self): + return self.presentations.filter(status=1, event__start_time__lte=timezone.now()) + def __str__(self): return self.title diff --git a/get_together/templates/get_together/events/show_event.html b/get_together/templates/get_together/events/show_event.html index e9a32ee..5ab01e7 100644 --- a/get_together/templates/get_together/events/show_event.html +++ b/get_together/templates/get_together/events/show_event.html @@ -94,6 +94,7 @@ {% endif %} @@ -146,6 +147,15 @@ {% endif %} + + Presentations: + + {% for presentation in presentation_list %} +
{{presentation.talk.title}} by {{presentation.talk.speaker}}
+ {% endfor %} + Propose a talk + +
diff --git a/get_together/templates/get_together/speakers/create_speaker.html b/get_together/templates/get_together/speakers/create_speaker.html new file mode 100644 index 0000000..b833928 --- /dev/null +++ b/get_together/templates/get_together/speakers/create_speaker.html @@ -0,0 +1,20 @@ +{% extends "get_together/base.html" %} +{% load static %} + + +{% block content %} +

New Speaker

+ + +
+{% csrf_token %} +
+ + + {{speaker_form}} +
+
+ +
+
+{% endblock %} diff --git a/get_together/templates/get_together/speakers/create_talk.html b/get_together/templates/get_together/speakers/create_talk.html new file mode 100644 index 0000000..1bd3b39 --- /dev/null +++ b/get_together/templates/get_together/speakers/create_talk.html @@ -0,0 +1,31 @@ +{% extends "get_together/base.html" %} +{% load static %} + + +{% block content %} +

New Talk

+ + +
+{% csrf_token %} +
+ + {{talk_form}} +
+
+ +
+
+{% endblock %} + + +{% block javascript %} + +{% endblock %} diff --git a/get_together/templates/get_together/speakers/delete_talk.html b/get_together/templates/get_together/speakers/delete_talk.html new file mode 100644 index 0000000..c716bb8 --- /dev/null +++ b/get_together/templates/get_together/speakers/delete_talk.html @@ -0,0 +1,20 @@ +{% extends "get_together/base.html" %} + +{% block content %} +

Confirm deletion

+{% if talk.future_presentations.count > 0 %} +
+
This talk has {{ talk.future_presentations.count }} pending event!
+
+{% endif %} +Are you sure you want to delete {{talk.title}}? +
+{% csrf_token %} +
+{{ delete_form }} +
+ +
+
+{% endblock %} + diff --git a/get_together/templates/get_together/speakers/edit_speaker.html b/get_together/templates/get_together/speakers/edit_speaker.html new file mode 100644 index 0000000..b28e2f4 --- /dev/null +++ b/get_together/templates/get_together/speakers/edit_speaker.html @@ -0,0 +1,46 @@ +{% extends "get_together/base.html" %} +{% load static %} + + +{% block content %} +

Edit Speaker

+ + +
+{% csrf_token %} +
+ + + {{speaker_form}} +
+
+ +
+
+{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/get_together/templates/get_together/speakers/edit_talk.html b/get_together/templates/get_together/speakers/edit_talk.html new file mode 100644 index 0000000..5a314c0 --- /dev/null +++ b/get_together/templates/get_together/speakers/edit_talk.html @@ -0,0 +1,32 @@ +{% extends "get_together/base.html" %} +{% load static %} + + +{% block content %} +

Edit Talk

+ + +
+{% csrf_token %} +
+ + {{talk_form}} +
+
+ +
+
+Delete + +{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/get_together/templates/get_together/speakers/list_user_presentations.html b/get_together/templates/get_together/speakers/list_user_presentations.html new file mode 100644 index 0000000..5ed864a --- /dev/null +++ b/get_together/templates/get_together/speakers/list_user_presentations.html @@ -0,0 +1,64 @@ +{% extends "get_together/base.html" %} +{% load static tz %} + +{% block styles %} + + +{% endblock %} + +{% block content %} +
+{% if proposed_talks %} +
+{% for presentation in proposed_talks %} +
+
+
+

+ {{presentation.talk.title}} + {% if presentation.status == -1 %} + Declined + {% elif presentation.status == 1 %} + Accepted + {% else %} + Submitted + {% endif %} +

+
+ {{ presentation.talk.speaker }} +
+
+
+
+{% endfor %} +
+
+{% endif %} +
+{% for talk in available_talks %} +
+ {% csrf_token %} +
+
+
+

{{talk.title}}

+
+ {{ talk.speaker }} +
+
+
+
+
+{% endfor %} +
+
+ +
+
+ +{% endblock %} + diff --git a/get_together/templates/get_together/speakers/list_user_talks.html b/get_together/templates/get_together/speakers/list_user_talks.html new file mode 100644 index 0000000..dd7e26b --- /dev/null +++ b/get_together/templates/get_together/speakers/list_user_talks.html @@ -0,0 +1,57 @@ +{% extends "get_together/base.html" %} +{% load static tz %} + +{% block styles %} + + +{% endblock %} + +{% block content %} +
+
+{% for speaker in speaker_bios %} +
+
+
+
+

+ {{speaker.title}} +

+
+ {{ speaker.bio }} +
+
+
+
+{% endfor %} +
+
+ +
+
+ +
+{% for talk in talks %} +
+
+
+

{{talk.title}}

+
+ {{ talk.abstract }} +
+
+
+
+{% endfor %} +
+
+
+ Talk +
+
+
+ +{% endblock %} + diff --git a/get_together/templates/get_together/users/show_profile.html b/get_together/templates/get_together/users/show_profile.html index c36cfcf..ff99f78 100644 --- a/get_together/templates/get_together/users/show_profile.html +++ b/get_together/templates/get_together/users/show_profile.html @@ -1,4 +1,10 @@ {% extends "get_together/base.html" %} +{% load static %} + +{% block styles %} + + +{% endblock %} {% block content %} @@ -6,11 +12,19 @@
-
+
{{user.user}} {% if user.user.id == request.user.id %} - Edit Profile + iCal {% endif %} @@ -21,6 +35,25 @@ {% if user.weburl %}

Homepage: {{user.weburl}}

{% endif %} +{% if user.talks %} +

Talks

+
+
+ {% for talk in user.talks %} +
+
+
+

{{talk.title}}

+
+ {{ talk.speaker }} +
+
+
+
+ {% endfor %} +
+
+{% endif %}
{% if teams %} diff --git a/get_together/urls.py b/get_together/urls.py index 1766586..6bb8510 100644 --- a/get_together/urls.py +++ b/get_together/urls.py @@ -49,6 +49,16 @@ urlpatterns = [ path('profile//', views.show_profile, name='show-profile'), path('profile/.ics', feeds.UserEventsCalendar(), name='user-event-ical'), + path('profile/+add-speaker', views.add_speaker, name='add-speaker'), + path('speaker//+edit', views.edit_speaker, name='edit-speaker'), + path('speaker//+delete', views.delete_speaker, name='delete-speaker'), + + path('profile/+talks', views.list_user_talks, name='user-talks'), + path('profile/+add-talk', views.add_talk, name='add-talk'), + path('talk//', views.show_talk, name='show-talk'), + path('talk//+edit', views.edit_talk, name='edit-talk'), + path('talk//+delete', views.delete_talk, name='delete-talk'), + path('events/', views.events_list, name='events'), path('events/all/', views.events_list_all, name='all-events'), path('teams/', views.teams_list, name='teams'), @@ -70,7 +80,10 @@ urlpatterns = [ path('events//+add_place/', views.add_place_to_event, name='add-place'), path('events//+comment/', event_views.comment_event, name='comment-event'), path('events//+photo/', views.add_event_photo, name='add-event-photo'), + path('events//+propose-talk/', views.propose_event_talk, name='propose-event-talk'), + path('events//+schedule-talks/', views.schedule_event_talks, name='schedule-event-talks'), path('events///', views.show_event, name='show-event'), + path('series//+edit/', views.edit_series, name='edit-series'), path('series//+delete/', views.delete_series, name='delete-series'), path('series//+add_place/', views.add_place_to_series, name='add-place-to-series'), diff --git a/get_together/views/__init__.py b/get_together/views/__init__.py index 9c5e8fe..1410be3 100644 --- a/get_together/views/__init__.py +++ b/get_together/views/__init__.py @@ -27,6 +27,7 @@ from .places import * from .user import * from .new_user import * from .new_team import * +from .speakers import * from .utils import * KM_PER_DEGREE_LAT = 110.574 diff --git a/get_together/views/events.py b/get_together/views/events.py index 26c5da5..0e31d5c 100644 --- a/get_together/views/events.py +++ b/get_together/views/events.py @@ -65,7 +65,7 @@ def show_event(request, event_id, event_slug): 'comment_form': comment_form, 'is_attending': request.user.profile in event.attendees.all(), 'attendee_list': Attendee.objects.filter(event=event), - 'presentation_list': event.presentations.all().order_by('start_time'), + 'presentation_list': event.presentations.filter(status=Presentation.ACCEPTED).order_by('start_time'), 'can_edit_event': request.user.profile.can_edit_event(event), } return render(request, 'get_together/events/show_event.html', context) diff --git a/get_together/views/speakers.py b/get_together/views/speakers.py new file mode 100644 index 0000000..c1318a0 --- /dev/null +++ b/get_together/views/speakers.py @@ -0,0 +1,200 @@ +from django.utils.translation import ugettext_lazy as _ + +from django.contrib import messages +from django.contrib.auth import logout as logout_user +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import HttpResponse, JsonResponse +from django.core.exceptions import ObjectDoesNotExist + +from events.models.profiles import UserProfile, Speaker, Talk +from events.forms import ( + SpeakerBioForm, + UserTalkForm, + DeleteTalkForm, + SchedulePresentationForm, +) + +from events.models.events import Event, Presentation + +import datetime +import simplejson + +from .teams import * +from .events import * + +@login_required +def list_user_talks(request): + profile = request.user.profile + speaker_bios = Speaker.objects.filter(user=profile) + talks = list(Talk.objects.filter(speaker__user=profile)) + context = { + 'speaker_bios': speaker_bios, + 'talks': talks, + } + return render(request, 'get_together/speakers/list_user_talks.html', context) + +def add_speaker(request): + new_speaker = Speaker(user=request.user.profile) + if request.method == 'GET': + speaker_form = SpeakerBioForm(instance=new_speaker) + context = { + 'speaker': new_speaker, + 'speaker_form': speaker_form, + } + return render(request, 'get_together/speakers/create_speaker.html', context) + elif request.method == 'POST': + speaker_form = SpeakerBioForm(request.POST, request.FILES, instance=new_speaker) + if speaker_form.is_valid(): + new_speaker = speaker_form.save() + return redirect('show-talks') + else: + context = { + 'speaker': new_speaker, + 'speaker_form': speaker_form, + } + return render(request, 'get_together/speakers/create_speaker.html', context) + return redirect('home') + +def edit_speaker(request, speaker_id): + speaker = get_object_or_404(Speaker, id=speaker_id) + if request.method == 'GET': + speaker_form = SpeakerBioForm(instance=speaker) + context = { + 'speaker': speaker, + 'speaker_form': speaker_form, + } + return render(request, 'get_together/speakers/edit_speaker.html', context) + elif request.method == 'POST': + speaker_form = SpeakerBioForm(request.POST, request.FILES, instance=speaker) + if speaker_form.is_valid(): + speaker = speaker_form.save() + return redirect('user-talks') + else: + context = { + 'speaker': speaker, + 'speaker_form': speaker_form, + } + return render(request, 'get_together/speakers/edit_speaker.html', context) + return redirect('home') + +def delete_speaker(request, speaker_id): + pass + +def show_talk(request, talk_id): + pass + +def add_talk(request): + new_talk = Talk() + if request.method == 'GET': + talk_form = UserTalkForm(instance=new_talk) + talk_form.fields['speaker'].queryset = request.user.profile.speaker_set + context = { + 'talk': new_talk, + 'talk_form': talk_form, + } + return render(request, 'get_together/speakers/create_talk.html', context) + elif request.method == 'POST': + talk_form = UserTalkForm(request.POST, instance=new_talk) + talk_form.fields['speaker'].queryset = request.user.profile.speaker_set + if talk_form.is_valid(): + new_talk = talk_form.save() + return redirect('user-talks') + else: + context = { + 'talk': new_talk, + 'talk_form': talk_form, + } + return render(request, 'get_together/speakers/create_talk.html', context) + return redirect('home') + +def edit_talk(request, talk_id): + talk = get_object_or_404(Talk, id=talk_id) + if not talk.speaker.user == request.user.profile: + messages.add_message(request, messages.WARNING, message=_('You can not make changes to this talk.')) + return redirect('show-talk', talk_id) + + if request.method == 'GET': + talk_form = UserTalkForm(instance=talk) + talk_form.fields['speaker'].queryset = request.user.profile.speaker_set + context = { + 'talk': talk, + 'talk_form': talk_form, + } + return render(request, 'get_together/speakers/edit_talk.html', context) + elif request.method == 'POST': + talk_form = UserTalkForm(request.POST, instance=talk) + talk_form.fields['speaker'].queryset = request.user.profile.speaker_set + if talk_form.is_valid(): + talk = talk_form.save() + return redirect('user-talks') + else: + context = { + 'talk': talk, + 'talk_form': talk_form, + } + return render(request, 'get_together/speakers/edit_talk.html', context) + return redirect('home') + +def delete_talk(request, talk_id): + talk = get_object_or_404(Talk, id=talk_id) + if not talk.speaker.user == request.user.profile: + messages.add_message(request, messages.WARNING, message=_('You can not make changes to this talk.')) + return redirect('show-talk', talk_id) + + if request.method == 'GET': + form = DeleteTalkForm() + + context = { + 'talk': talk, + 'delete_form': form, + } + return render(request, 'get_together/speakers/delete_talk.html', context) + elif request.method == 'POST': + form = DeleteTalkForm(request.POST) + if form.is_valid() and form.cleaned_data['confirm']: + talk.delete() + return redirect('user-talks') + else: + context = { + 'talk': talk, + 'delete_form': form, + } + return render(request, 'get_together/speakers/delete_talk.html', context) + else: + return redirect('home') + +@login_required +def propose_event_talk(request, event_id): + event = get_object_or_404(Event, id=event_id) + if request.method == 'GET': + profile = request.user.profile + talks = list(Talk.objects.filter(speaker__user=profile)) + presentations = event.presentations.all().order_by('-status') + for presentation in presentations: + if presentation.talk in talks: + talks.remove(presentation.talk) + + context = { + 'event': event, + 'available_talks': talks, + 'proposed_talks': presentations, + } + return render(request, 'get_together/speakers/list_user_presentations.html', context) + elif request.method == 'POST': + talk = get_object_or_404(Talk, id=request.POST.get('talk_id')) + new_proposal = Presentation.objects.create( + event=event, + talk=talk, + status=Presentation.PROPOSED, + start_time=event.local_start_time, + created_by=request.user.profile, + ) + messages.add_message(request, messages.SUCCESS, message=_('Your talk has been submitted to the event organizer.')) + return redirect(event.get_absolute_url()) + else: + redirect('home') + +def schedule_event_talks(request, event_id): + pass + + From 8e7c9559289fc339ffdc906c9337d480ddf97c50 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Wed, 25 Apr 2018 10:36:01 -0400 Subject: [PATCH 4/7] Refactor Speaker/Talk/Presentation models into their own file, add views and templates for deleting speaker and talk, add view and template for showing a talk, improve the workflow for proposing a talk that wasn't previously defined --- events/admin.py | 7 +- events/forms.py | 9 +- events/models/__init__.py | 2 +- events/models/events.py | 24 ---- events/models/profiles.py | 63 ----------- events/models/speakers.py | 104 ++++++++++++++++++ .../get_together/events/show_event.html | 2 +- .../get_together/speakers/create_talk.html | 2 + .../get_together/speakers/delete_speaker.html | 20 ++++ .../get_together/speakers/delete_talk.html | 2 +- .../get_together/speakers/edit_speaker.html | 2 + .../speakers/list_user_presentations.html | 2 +- .../get_together/speakers/show_speaker.html | 0 .../get_together/speakers/show_talk.html | 50 +++++++++ .../get_together/users/show_profile.html | 4 +- get_together/views/events.py | 2 +- get_together/views/speakers.py | 59 ++++++++-- get_together/views/user.py | 3 +- 18 files changed, 251 insertions(+), 106 deletions(-) create mode 100644 events/models/speakers.py create mode 100644 get_together/templates/get_together/speakers/delete_speaker.html create mode 100644 get_together/templates/get_together/speakers/show_speaker.html create mode 100644 get_together/templates/get_together/speakers/show_talk.html diff --git a/events/admin.py b/events/admin.py index 97e95f9..1e27747 100644 --- a/events/admin.py +++ b/events/admin.py @@ -10,8 +10,6 @@ from .models.profiles import ( Member, Category, Topic, - Speaker, - Talk, ) from .models.search import Searchable from .models.events import ( @@ -22,7 +20,12 @@ from .models.events import ( EventPhoto, CommonEvent, Attendee, +) +from .models.speakers import ( + Speaker, + Talk, Presentation, + SpeakerRequest, ) admin.site.register(Language) diff --git a/events/forms.py b/events/forms.py index b16a66a..18552a1 100644 --- a/events/forms.py +++ b/events/forms.py @@ -6,7 +6,7 @@ from django.utils import timezone from django.contrib.auth.models import User from .models.locale import Country, SPR, City -from .models.profiles import Team, UserProfile, Speaker, Talk +from .models.profiles import Team, UserProfile from .models.events import ( Event, EventComment, @@ -14,6 +14,10 @@ from .models.events import ( EventSeries, Place, EventPhoto, +) +from .models.speakers import ( + Speaker, + Talk, Presentation, SpeakerRequest, ) @@ -343,6 +347,9 @@ class SpeakerBioForm(forms.ModelForm): model = Speaker fields = ['avatar', 'title', 'bio', 'categories'] +class DeleteSpeakerForm(forms.Form): + confirm = forms.BooleanField(label="Yes, delete series", required=True) + class UserTalkForm(forms.ModelForm): class Meta: model = Talk diff --git a/events/models/__init__.py b/events/models/__init__.py index ca0026c..3d38a72 100644 --- a/events/models/__init__.py +++ b/events/models/__init__.py @@ -2,4 +2,4 @@ from .profiles import * from .locale import * from .search import * from .events import * - +from .speakers import * diff --git a/events/models/events.py b/events/models/events.py index 47aba50..d564aef 100644 --- a/events/models/events.py +++ b/events/models/events.py @@ -376,28 +376,4 @@ class EventSeries(models.Model): def __str__(self): return u'%s by %s at %s' % (self.name, self.team.name, self.start_time) -class SpeakerRequest(models.Model): - event = models.ForeignKey(Event, on_delete=models.CASCADE) - topics = models.ManyToManyField('Topic', blank=True) - -class Presentation(models.Model): - DECLINED=-1 - PROPOSED=0 - ACCEPTED=1 - - STATUSES = [ - (DECLINED, _("Declined")), - (PROPOSED, _("Proposed")), - (ACCEPTED, _("Accepted")), - ] - event = models.ForeignKey(Event, related_name='presentations', on_delete=models.CASCADE) - talk = models.ForeignKey(Talk, related_name='presentations', on_delete=models.SET_NULL, blank=False, null=True) - status = models.SmallIntegerField(choices=STATUSES, default=PROPOSED, db_index=True) - start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True, null=True, blank=True) - - created_by = models.ForeignKey(UserProfile, on_delete=models.SET_NULL, null=True, blank=False) - created_time = models.DateTimeField(default=timezone.now, db_index=True) - - def __str__(self): - return '%s at %s' % (self.talk.title, self.event.name) diff --git a/events/models/profiles.py b/events/models/profiles.py index 2a8bb67..0ab8c4d 100644 --- a/events/models/profiles.py +++ b/events/models/profiles.py @@ -86,10 +86,6 @@ class UserProfile(models.Model): def moderating(self): return [member.team for member in Member.objects.filter(user=self, role__in=(Member.ADMIN, Member.MODERATOR))] - @property - def talks(self): - return Talk.objects.filter(speaker__user=self) - def can_create_event(self, team): try: if self.user.is_superuser: @@ -306,62 +302,3 @@ class Topic(models.Model): self.slug = slugify(self.name) super().save(*args, **kwargs) -class Speaker(models.Model): - user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) - avatar = ProcessedImageField(verbose_name=_("Photo Image"), - upload_to='avatars', - processors=[ResizeToFill(128, 128)], - format='PNG', - blank=True) - title = models.CharField(max_length=256, blank=True, null=True) - bio = models.TextField(verbose_name=_('Biography'), blank=True) - - categories = models.ManyToManyField('Category', blank=True) - topics = models.ManyToManyField('Topic', blank=True) - - def headshot(self): - if self.avatar: - return self.avatar - else: - return self.user.avatar - - def __str__(self): - if self.title: - return self.title - return self.user.__str__() - -class Talk(models.Model): - PRESENTATION=0 - WORKSHOP=1 - PANEL=2 - ROUNDTABLE=3 - QANDA=4 - DEMO=5 - TYPES = [ - (PRESENTATION, _("Presentation")), - (WORKSHOP, _("Workshop")), - (PANEL, _("Panel")), - (ROUNDTABLE, _("Roundtable")), - (QANDA, _("Q & A")), - (DEMO, _("Demonstration")), - ] - speaker = models.ForeignKey(Speaker, verbose_name=_('Speaker Bio'), on_delete=models.CASCADE) - title = models.CharField(max_length=256) - abstract = models.TextField() - talk_type = models.SmallIntegerField(_("Type"), choices=TYPES, default=PRESENTATION) - web_url = models.URLField(_("Website"), null=True, blank=True) - - category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) - topics = models.ManyToManyField('Topic', blank=True) - - @property - def future_presentations(self): - return self.presentations.filter(status__gte=0, event__start_time__gt=timezone.now()) - - @property - def past_presentations(self): - return self.presentations.filter(status=1, event__start_time__lte=timezone.now()) - - def __str__(self): - return self.title - diff --git a/events/models/speakers.py b/events/models/speakers.py new file mode 100644 index 0000000..bbd4d72 --- /dev/null +++ b/events/models/speakers.py @@ -0,0 +1,104 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone + +from imagekit.models import ImageSpecField +from imagekit.processors import ResizeToFill + +from ..utils import slugify +from .locale import * +from .profiles import * +from .events import * +from .search import * +from .. import location + +import pytz +import datetime + +class Speaker(models.Model): + user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) + avatar = ProcessedImageField(verbose_name=_("Photo Image"), + upload_to='avatars', + processors=[ResizeToFill(128, 128)], + format='PNG', + blank=True) + title = models.CharField(max_length=256, blank=True, null=True) + bio = models.TextField(verbose_name=_('Biography'), blank=True) + + categories = models.ManyToManyField('Category', blank=True) + topics = models.ManyToManyField('Topic', blank=True) + + def headshot(self): + if self.avatar: + return self.avatar + else: + return self.user.avatar + + def __str__(self): + if self.title: + return self.title + return self.user.__str__() + +class Talk(models.Model): + PRESENTATION=0 + WORKSHOP=1 + PANEL=2 + ROUNDTABLE=3 + QANDA=4 + DEMO=5 + TYPES = [ + (PRESENTATION, _("Presentation")), + (WORKSHOP, _("Workshop")), + (PANEL, _("Panel")), + (ROUNDTABLE, _("Roundtable")), + (QANDA, _("Q & A")), + (DEMO, _("Demonstration")), + ] + speaker = models.ForeignKey(Speaker, verbose_name=_('Speaker Bio'), related_name='talks', on_delete=models.CASCADE) + title = models.CharField(max_length=256) + abstract = models.TextField() + talk_type = models.SmallIntegerField(_("Type"), choices=TYPES, default=PRESENTATION) + web_url = models.URLField(_("Website"), null=True, blank=True) + + category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) + topics = models.ManyToManyField('Topic', blank=True) + + @property + def future_presentations(self): + return self.presentations.filter(status__gte=0, event__start_time__gt=timezone.now()) + + @property + def past_presentations(self): + return self.presentations.filter(status=1, event__start_time__lte=timezone.now()) + + def __str__(self): + return self.title + +class SpeakerRequest(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + topics = models.ManyToManyField('Topic', blank=True) + +class Presentation(models.Model): + DECLINED=-1 + PROPOSED=0 + ACCEPTED=1 + + STATUSES = [ + (DECLINED, _("Declined")), + (PROPOSED, _("Proposed")), + (ACCEPTED, _("Accepted")), + ] + event = models.ForeignKey(Event, related_name='presentations', on_delete=models.CASCADE) + talk = models.ForeignKey(Talk, related_name='presentations', on_delete=models.CASCADE, blank=False, null=True) + status = models.SmallIntegerField(choices=STATUSES, default=PROPOSED, db_index=True) + start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True, null=True, blank=True) + + created_by = models.ForeignKey(UserProfile, on_delete=models.SET_NULL, null=True, blank=False) + created_time = models.DateTimeField(default=timezone.now, db_index=True) + + def __str__(self): + try: + return '%s at %s' % (self.talk.title, self.event.name) + except: + return "No talk" + diff --git a/get_together/templates/get_together/events/show_event.html b/get_together/templates/get_together/events/show_event.html index 5ab01e7..9385540 100644 --- a/get_together/templates/get_together/events/show_event.html +++ b/get_together/templates/get_together/events/show_event.html @@ -151,7 +151,7 @@ Presentations: {% for presentation in presentation_list %} - +
{{presentation.talk.title}} by {{presentation.talk.speaker.user}}, {{presentation.talk.speaker.title}}
{% endfor %} Propose a talk diff --git a/get_together/templates/get_together/speakers/create_talk.html b/get_together/templates/get_together/speakers/create_talk.html index 1bd3b39..3772306 100644 --- a/get_together/templates/get_together/speakers/create_talk.html +++ b/get_together/templates/get_together/speakers/create_talk.html @@ -8,6 +8,8 @@
{% csrf_token %} +{% if event %}{% endif %} +
{{talk_form}} diff --git a/get_together/templates/get_together/speakers/delete_speaker.html b/get_together/templates/get_together/speakers/delete_speaker.html new file mode 100644 index 0000000..d6e2163 --- /dev/null +++ b/get_together/templates/get_together/speakers/delete_speaker.html @@ -0,0 +1,20 @@ +{% extends "get_together/base.html" %} + +{% block content %} +

Confirm deletion

+{% if speaker.talks.count > 0 %} +
+
This speaker profile has {{ speaker.talks.count }} talks!
+
+{% endif %} +Are you sure you want to delete {{speaker.title}}? + +{% csrf_token %} +
+{{ delete_form }} +
+ +
+ +{% endblock %} + diff --git a/get_together/templates/get_together/speakers/delete_talk.html b/get_together/templates/get_together/speakers/delete_talk.html index c716bb8..386a076 100644 --- a/get_together/templates/get_together/speakers/delete_talk.html +++ b/get_together/templates/get_together/speakers/delete_talk.html @@ -4,7 +4,7 @@

Confirm deletion

{% if talk.future_presentations.count > 0 %}
-
This talk has {{ talk.future_presentations.count }} pending event!
+
This talk has {{ talk.future_presentations.count }} pending events!
{% endif %} Are you sure you want to delete {{talk.title}}? diff --git a/get_together/templates/get_together/speakers/edit_speaker.html b/get_together/templates/get_together/speakers/edit_speaker.html index b28e2f4..f5bbb21 100644 --- a/get_together/templates/get_together/speakers/edit_speaker.html +++ b/get_together/templates/get_together/speakers/edit_speaker.html @@ -17,6 +17,8 @@ +Delete + {% endblock %} {% block javascript %} diff --git a/get_together/templates/get_together/speakers/list_user_presentations.html b/get_together/templates/get_together/speakers/list_user_presentations.html index 5ed864a..fe4dd21 100644 --- a/get_together/templates/get_together/speakers/list_user_presentations.html +++ b/get_together/templates/get_together/speakers/list_user_presentations.html @@ -55,7 +55,7 @@ diff --git a/get_together/templates/get_together/speakers/show_speaker.html b/get_together/templates/get_together/speakers/show_speaker.html new file mode 100644 index 0000000..e69de29 diff --git a/get_together/templates/get_together/speakers/show_talk.html b/get_together/templates/get_together/speakers/show_talk.html new file mode 100644 index 0000000..1905d92 --- /dev/null +++ b/get_together/templates/get_together/speakers/show_talk.html @@ -0,0 +1,50 @@ +{% extends "get_together/base.html" %} +{% load markup static %} + +{% block content %} +
+
+
+

{{ talk.title }} + {% if talk.speaker.user == request.user.profile %} + Edit Talk + {% endif %} +

+
+ + + + + + + {% if talk.web_url %} + + + + {% endif %} + + + +
Speaker:{{ talk.speaker }}
Category:{{ talk.category }}
Website:{{ talk.web_url }}
Abstract:{{ talk.abstract|markdown }}
+ +
+ +
+
+
+

Events ({{presentations.count}})


+
+ {% for presentation in presentations %} +
+
+
{{presentation.event.name}}
+ {{ presentation.event.local_start_time }} +
+
+ {% endfor %} +
+
+
+
+{% endblock %} + diff --git a/get_together/templates/get_together/users/show_profile.html b/get_together/templates/get_together/users/show_profile.html index ff99f78..1e883c6 100644 --- a/get_together/templates/get_together/users/show_profile.html +++ b/get_together/templates/get_together/users/show_profile.html @@ -35,11 +35,11 @@ {% if user.weburl %}

Homepage: {{user.weburl}}

{% endif %} -{% if user.talks %} +{% if talks %}

Talks

- {% for talk in user.talks %} + {% for talk in talks %}
diff --git a/get_together/views/events.py b/get_together/views/events.py index 0e31d5c..6d54f85 100644 --- a/get_together/views/events.py +++ b/get_together/views/events.py @@ -14,10 +14,10 @@ from events.models.events import ( EventPhoto, Place, Attendee, - Presentation, update_event_searchable, delete_event_searchable, ) +from events.models.speakers import Speaker, Talk, SpeakerRequest, Presentation from events.models.profiles import Team, Organization, UserProfile, Member from events.forms import ( TeamEventForm, diff --git a/get_together/views/speakers.py b/get_together/views/speakers.py index c1318a0..be4ec74 100644 --- a/get_together/views/speakers.py +++ b/get_together/views/speakers.py @@ -6,16 +6,17 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.http import HttpResponse, JsonResponse from django.core.exceptions import ObjectDoesNotExist -from events.models.profiles import UserProfile, Speaker, Talk +from events.models.profiles import UserProfile from events.forms import ( SpeakerBioForm, + DeleteSpeakerForm, UserTalkForm, DeleteTalkForm, SchedulePresentationForm, ) -from events.models.events import Event, Presentation - +from events.models.events import Event +from events.models.speakers import Speaker, Talk, Presentation, SpeakerRequest import datetime import simplejson @@ -46,7 +47,7 @@ def add_speaker(request): speaker_form = SpeakerBioForm(request.POST, request.FILES, instance=new_speaker) if speaker_form.is_valid(): new_speaker = speaker_form.save() - return redirect('show-talks') + return redirect('user-talks') else: context = { 'speaker': new_speaker, @@ -78,10 +79,41 @@ def edit_speaker(request, speaker_id): return redirect('home') def delete_speaker(request, speaker_id): - pass + speaker = get_object_or_404(Speaker, id=speaker_id) + if not speaker.user == request.user.profile: + messages.add_message(request, messages.WARNING, message=_('You can not make changes to this speaker bio.')) + return redirect('show-speaker', speaker_id) + + if request.method == 'GET': + form = DeleteSpeakerForm() + + context = { + 'speaker': speaker, + 'delete_form': form, + } + return render(request, 'get_together/speakers/delete_speaker.html', context) + elif request.method == 'POST': + form = DeleteSpeakerForm(request.POST) + if form.is_valid() and form.cleaned_data['confirm']: + speaker.delete() + return redirect('user-talks') + else: + context = { + 'speaker': speaker, + 'delete_form': form, + } + return render(request, 'get_together/speakers/delete_speaker.html', context) + else: + return redirect('home') def show_talk(request, talk_id): - pass + talk = get_object_or_404(Talk, id=talk_id) + presentations = Presentation.objects.filter(talk=talk, status=Presentation.ACCEPTED).order_by('-event__start_time') + context = { + 'talk': talk, + 'presentations': presentations, + } + return render(request, 'get_together/speakers/show_talk.html', context) def add_talk(request): new_talk = Talk() @@ -92,13 +124,24 @@ def add_talk(request): 'talk': new_talk, 'talk_form': talk_form, } + if 'event' in request.GET and request.GET['event']: + context['event'] = get_object_or_404(Event, id=request.GET['event']) return render(request, 'get_together/speakers/create_talk.html', context) elif request.method == 'POST': talk_form = UserTalkForm(request.POST, instance=new_talk) talk_form.fields['speaker'].queryset = request.user.profile.speaker_set if talk_form.is_valid(): new_talk = talk_form.save() - return redirect('user-talks') + if 'event' in request.POST and request.POST['event']: + event = Event.objects.get(id=request.POST['event']) + Presentation.objects.create( + event=event, + talk=new_talk, + status=Presentation.PROPOSED, + created_by=request.user.profile + ) + return redirect(event.get_absolute_url()) + return redirect('show-talk', new_talk.id) else: context = { 'talk': new_talk, @@ -126,7 +169,7 @@ def edit_talk(request, talk_id): talk_form.fields['speaker'].queryset = request.user.profile.speaker_set if talk_form.is_valid(): talk = talk_form.save() - return redirect('user-talks') + return redirect('show-talk', talk.id) else: context = { 'talk': talk, diff --git a/get_together/views/user.py b/get_together/views/user.py index d3a678f..584cd6c 100644 --- a/get_together/views/user.py +++ b/get_together/views/user.py @@ -33,10 +33,11 @@ def show_profile(request, user_id): user = get_object_or_404(UserProfile, id=user_id) teams = user.memberships.all() - + talks = Talk.objects.filter(speaker__user=user) context = { 'user': user, 'teams': teams, + 'talks': talks, } return render(request, 'get_together/users/show_profile.html', context) From f5ac0c2fdd1411103931b60ebf2937509bb425a5 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Fri, 27 Apr 2018 18:11:05 -0400 Subject: [PATCH 5/7] Add premium fields to Team objects, only show presentations for premium teams --- events/admin.py | 6 ++- .../0029_add_team_premium_fields.py | 49 +++++++++++++++++++ events/models/profiles.py | 5 ++ .../get_together/events/show_event.html | 2 + get_together/views/speakers.py | 4 ++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 events/migrations/0029_add_team_premium_fields.py diff --git a/events/admin.py b/events/admin.py index 1e27747..481e81d 100644 --- a/events/admin.py +++ b/events/admin.py @@ -55,11 +55,15 @@ admin.site.register(Organization, OrgAdmin) class TeamAdmin(admin.ModelAdmin): raw_id_fields = ('country', 'spr', 'city', 'owner_profile', 'admin_profiles', 'contact_profiles') - list_display = ('__str__', 'member_count', 'owner_profile', 'created_date') + list_display = ('__str__', 'active', 'member_count', 'event_count', 'owner_profile', 'created_date', 'is_premium', 'premium_expires') + list_filter = ('active', 'is_premium', 'organization', 'country',) ordering = ('-created_date',) def member_count(self, team): return team.members.all().count() member_count.short_description = 'Members' + def event_count(self, team): + return team.event_set.all().count() + event_count.short_description = 'Events' admin.site.register(Team, TeamAdmin) class SearchableAdmin(admin.ModelAdmin): diff --git a/events/migrations/0029_add_team_premium_fields.py b/events/migrations/0029_add_team_premium_fields.py new file mode 100644 index 0000000..ee955dd --- /dev/null +++ b/events/migrations/0029_add_team_premium_fields.py @@ -0,0 +1,49 @@ +# Generated by Django 2.0 on 2018-04-27 21:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0028_add_speaker_models'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='is_premium', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='team', + name='premium_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='premium_teams', to='events.UserProfile'), + ), + migrations.AddField( + model_name='team', + name='premium_expires', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='team', + name='premium_started', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='presentation', + name='talk', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='presentations', to='events.Talk'), + ), + migrations.AlterField( + model_name='speaker', + name='bio', + field=models.TextField(blank=True, verbose_name='Biography'), + ), + migrations.AlterField( + model_name='talk', + name='speaker', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='talks', to='events.Speaker', verbose_name='Speaker Bio'), + ), + ] diff --git a/events/models/profiles.py b/events/models/profiles.py index 0ab8c4d..836b44c 100644 --- a/events/models/profiles.py +++ b/events/models/profiles.py @@ -224,6 +224,11 @@ class Team(models.Model): category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) topics = models.ManyToManyField('Topic', blank=True) + is_premium = models.BooleanField(default=False) + premium_by = models.ForeignKey(UserProfile, related_name='premium_teams', null=True, on_delete=models.SET_NULL) + premium_started = models.DateTimeField(blank=True, null=True) + premium_expires = models.DateTimeField(blank=True, null=True) + @property def location_name(self): if self.city: diff --git a/get_together/templates/get_together/events/show_event.html b/get_together/templates/get_together/events/show_event.html index 9385540..5380b49 100644 --- a/get_together/templates/get_together/events/show_event.html +++ b/get_together/templates/get_together/events/show_event.html @@ -147,6 +147,7 @@ {% endif %} + {% if event.team.is_premium %} Presentations: @@ -156,6 +157,7 @@ Propose a talk + {% endif %}
diff --git a/get_together/views/speakers.py b/get_together/views/speakers.py index be4ec74..41dd20e 100644 --- a/get_together/views/speakers.py +++ b/get_together/views/speakers.py @@ -209,6 +209,10 @@ def delete_talk(request, talk_id): @login_required def propose_event_talk(request, event_id): event = get_object_or_404(Event, id=event_id) + if not event.team.is_premium: + messages.add_message(request, messages.ERROR, message=_("You can not propose a talk to this team's events.")) + return redirect(event.get_absolute_url()) + if request.method == 'GET': profile = request.user.profile talks = list(Talk.objects.filter(speaker__user=profile)) From a81c9fddf78f389b1298c5c83154338c878258a6 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Fri, 27 Apr 2018 22:12:37 -0400 Subject: [PATCH 6/7] Add ability to accept or decline proposed talks --- events/models/profiles.py | 2 +- get_together/environ_settings.py | 1 + get_together/settings.py | 1 + .../events/schedule_event_talks.html | 112 ++++++++++++++++++ .../get_together/events/show_event.html | 5 +- get_together/views/events.py | 1 + get_together/views/speakers.py | 27 ++++- 7 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 get_together/templates/get_together/events/schedule_event_talks.html diff --git a/events/models/profiles.py b/events/models/profiles.py index 836b44c..7751fbf 100644 --- a/events/models/profiles.py +++ b/events/models/profiles.py @@ -224,7 +224,7 @@ class Team(models.Model): category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) topics = models.ManyToManyField('Topic', blank=True) - is_premium = models.BooleanField(default=False) + is_premium = models.BooleanField(default=settings.EVENTS_TEAMS_DEFAULT_PREMIUM) premium_by = models.ForeignKey(UserProfile, related_name='premium_teams', null=True, on_delete=models.SET_NULL) premium_started = models.DateTimeField(blank=True, null=True) premium_expires = models.DateTimeField(blank=True, null=True) diff --git a/get_together/environ_settings.py b/get_together/environ_settings.py index de0ff71..4bd2279 100644 --- a/get_together/environ_settings.py +++ b/get_together/environ_settings.py @@ -15,3 +15,4 @@ DATABASES['default'].update(dj_database_url.config()) MEDIA_URL = os.environ.get('MEDIA_URL', '/media/') STATIC_URL = os.environ.get('STATIC_URL', '/static/') +EVENTS_TEAMS_DEFAULT_PREMIUM = os.environ.get('EVENTS_TEAMS_DEFAULT_PREMIUM', False) diff --git a/get_together/settings.py b/get_together/settings.py index 9e04858..83146c0 100644 --- a/get_together/settings.py +++ b/get_together/settings.py @@ -29,6 +29,7 @@ ALLOWED_HOSTS = [] SITE_ID=1 ADMINS = [ 'mhall119' ] +EVENTS_TEAMS_DEFAULT_PREMIUM=False # Application definition diff --git a/get_together/templates/get_together/events/schedule_event_talks.html b/get_together/templates/get_together/events/schedule_event_talks.html new file mode 100644 index 0000000..218dd6b --- /dev/null +++ b/get_together/templates/get_together/events/schedule_event_talks.html @@ -0,0 +1,112 @@ +{% extends "get_together/base.html" %} +{% load static tz %} + +{% block styles %} + + +{% endblock %} + +{% block content %} +{% if talks_count == 0 %} +
+
No talks have been proposed for this event.
+
+{% endif %} +
+
+{% for presentation in pending_talks %} +
+ + {% csrf_token %} +
+
+
+

{{presentation.talk.title}}

+
+ {{ presentation.talk.speaker }} +
+ + +
+
+
+
+
+
+{% endfor %} +
+{% if accepted_talks %} +
+
+{% for presentation in accepted_talks %} +
+ + {% csrf_token %} +
+
+
+

+ {{presentation.talk.title}} + {% if presentation.status == -1 %} + Declined + {% elif presentation.status == 1 %} + Accepted + {% else %} + Submitted + {% endif %} +

+
+ {{ presentation.talk.speaker }} +
+ +
+
+
+
+
+
+{% endfor %} +
+{% endif %} +{% if declined_talks %} +
+
+{% for presentation in declined_talks %} +
+ + {% csrf_token %} +
+
+
+

+ {{presentation.talk.title}} + {% if presentation.status == -1 %} + Declined + {% elif presentation.status == 1 %} + Accepted + {% else %} + Submitted + {% endif %} +

+
+ {{ presentation.talk.speaker }} +
+ +
+
+
+
+
+
+{% endfor %} +
+{% endif %} +
+
+ Done +
+
+
+ +{% endblock %} + diff --git a/get_together/templates/get_together/events/show_event.html b/get_together/templates/get_together/events/show_event.html index 5380b49..74530f4 100644 --- a/get_together/templates/get_together/events/show_event.html +++ b/get_together/templates/get_together/events/show_event.html @@ -94,7 +94,7 @@
{% endif %} @@ -155,6 +155,9 @@
{{presentation.talk.title}} by {{presentation.talk.speaker.user}}, {{presentation.talk.speaker.title}}
{% endfor %} Propose a talk + {% if pending_presentations %} + {{pending_presentations}} proposed talks + {% endif %} {% endif %} diff --git a/get_together/views/events.py b/get_together/views/events.py index 6d54f85..64fb18f 100644 --- a/get_together/views/events.py +++ b/get_together/views/events.py @@ -66,6 +66,7 @@ def show_event(request, event_id, event_slug): 'is_attending': request.user.profile in event.attendees.all(), 'attendee_list': Attendee.objects.filter(event=event), 'presentation_list': event.presentations.filter(status=Presentation.ACCEPTED).order_by('start_time'), + 'pending_presentations': event.presentations.filter(status=Presentation.PROPOSED).count(), 'can_edit_event': request.user.profile.can_edit_event(event), } return render(request, 'get_together/events/show_event.html', context) diff --git a/get_together/views/speakers.py b/get_together/views/speakers.py index 41dd20e..96d7938 100644 --- a/get_together/views/speakers.py +++ b/get_together/views/speakers.py @@ -1,5 +1,5 @@ from django.utils.translation import ugettext_lazy as _ - +from django.utils.safestring import mark_safe from django.contrib import messages from django.contrib.auth import logout as logout_user from django.shortcuts import render, redirect, get_object_or_404 @@ -210,7 +210,7 @@ def delete_talk(request, talk_id): def propose_event_talk(request, event_id): event = get_object_or_404(Event, id=event_id) if not event.team.is_premium: - messages.add_message(request, messages.ERROR, message=_("You can not propose a talk to this team's events.")) + messages.add_message(request, messages.ERROR, message=_("You can not manage talks for this event.")) return redirect(event.get_absolute_url()) if request.method == 'GET': @@ -242,6 +242,27 @@ def propose_event_talk(request, event_id): redirect('home') def schedule_event_talks(request, event_id): - pass + event = get_object_or_404(Event, id=event_id) + if not event.team.is_premium: + messages.add_message(request, messages.ERROR, message=mark_safe(_('Upgrade this team to a Premium account to use this feature.'))) + return redirect(event.get_absolute_url()) + if request.method == 'POST': + presentation = get_object_or_404(Presentation, id=request.POST.get('presentation_id')) + if request.POST.get('action') == 'accept': + presentation.status = Presentation.ACCEPTED + elif request.POST.get('action') == 'decline': + presentation.status = Presentation.DECLINED + elif request.POST.get('action') == 'propose': + presentation.status = Presentation.PROPOSED + presentation.save() + + context = { + 'event': event, + 'talks_count': event.presentations.count(), + 'accepted_talks': event.presentations.filter(status=Presentation.ACCEPTED).order_by('start_time'), + 'pending_talks': event.presentations.filter(status=Presentation.PROPOSED).order_by('start_time'), + 'declined_talks': event.presentations.filter(status=Presentation.DECLINED).order_by('start_time'), + } + return render(request, 'get_together/events/schedule_event_talks.html', context) From fa4c0fd1326a5535a88d6060f10c7e851a222f5c Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Fri, 27 Apr 2018 22:49:56 -0400 Subject: [PATCH 7/7] Add show-speaker page --- .../events/schedule_event_talks.html | 4 ++ .../get_together/events/show_event.html | 2 +- .../get_together/speakers/show_speaker.html | 61 +++++++++++++++++++ .../get_together/speakers/show_talk.html | 3 +- get_together/urls.py | 1 + get_together/views/speakers.py | 10 +++ 6 files changed, 79 insertions(+), 2 deletions(-) diff --git a/get_together/templates/get_together/events/schedule_event_talks.html b/get_together/templates/get_together/events/schedule_event_talks.html index 218dd6b..f35f2dc 100644 --- a/get_together/templates/get_together/events/schedule_event_talks.html +++ b/get_together/templates/get_together/events/schedule_event_talks.html @@ -13,6 +13,9 @@
{% endif %}
+

Manage talks for: {{event.name}}

+{% if pending_talks %} +
{% for presentation in pending_talks %}
@@ -35,6 +38,7 @@
{% endfor %}
+{% endif %} {% if accepted_talks %}
diff --git a/get_together/templates/get_together/events/show_event.html b/get_together/templates/get_together/events/show_event.html index 74530f4..846ff9f 100644 --- a/get_together/templates/get_together/events/show_event.html +++ b/get_together/templates/get_together/events/show_event.html @@ -152,7 +152,7 @@ Presentations: {% for presentation in presentation_list %} -
{{presentation.talk.title}} by {{presentation.talk.speaker.user}}, {{presentation.talk.speaker.title}}
+ {% endfor %} Propose a talk {% if pending_presentations %} diff --git a/get_together/templates/get_together/speakers/show_speaker.html b/get_together/templates/get_together/speakers/show_speaker.html index e69de29..541fdd9 100644 --- a/get_together/templates/get_together/speakers/show_speaker.html +++ b/get_together/templates/get_together/speakers/show_speaker.html @@ -0,0 +1,61 @@ +{% extends "get_together/base.html" %} +{% load markup static %} + +{% block styles %} + + +{% endblock %} + +{% block content %} +
+
+
+
+

{{ speaker.user }}

+
{{ speaker.title }}
+

+ {{ speaker.bio|markdown }} +

+ +{% if talks %} +
+

Talks

+
+
+ {% for talk in talks %} +
+
+
+

{{talk.title}}

+
+ {{ talk.speaker }} +
+
+
+
+ {% endfor %} +
+
+{% endif %} +
+
+
+
+

Events ({{presentations.count}})


+
+ {% for presentation in presentations %} +
+
+
{{presentation.event.name}}
+ {{ presentation.event.team }}
+ {{ presentation.event.local_start_time }} +
+
+ {% endfor %} +
+
+ +
+
+{% endblock %} + diff --git a/get_together/templates/get_together/speakers/show_talk.html b/get_together/templates/get_together/speakers/show_talk.html index 1905d92..bfe0f66 100644 --- a/get_together/templates/get_together/speakers/show_talk.html +++ b/get_together/templates/get_together/speakers/show_talk.html @@ -12,7 +12,7 @@ - + @@ -38,6 +38,7 @@
{{presentation.event.name}}
+ {{ presentation.event.team }}
{{ presentation.event.local_start_time }}
diff --git a/get_together/urls.py b/get_together/urls.py index 6bb8510..24da216 100644 --- a/get_together/urls.py +++ b/get_together/urls.py @@ -50,6 +50,7 @@ urlpatterns = [ path('profile/.ics', feeds.UserEventsCalendar(), name='user-event-ical'), path('profile/+add-speaker', views.add_speaker, name='add-speaker'), + path('speaker//', views.show_speaker, name='show-speaker'), path('speaker//+edit', views.edit_speaker, name='edit-speaker'), path('speaker//+delete', views.delete_speaker, name='delete-speaker'), diff --git a/get_together/views/speakers.py b/get_together/views/speakers.py index 96d7938..217badd 100644 --- a/get_together/views/speakers.py +++ b/get_together/views/speakers.py @@ -34,6 +34,16 @@ def list_user_talks(request): } return render(request, 'get_together/speakers/list_user_talks.html', context) +def show_speaker(request, speaker_id): + speaker = get_object_or_404(Speaker, id=speaker_id) + + context = { + 'speaker': speaker, + 'talks': Talk.objects.filter(speaker=speaker), + 'presentations': Presentation.objects.filter(talk__speaker=speaker, status=Presentation.ACCEPTED), + } + return render(request, 'get_together/speakers/show_speaker.html', context) + def add_speaker(request): new_speaker = Speaker(user=request.user.profile) if request.method == 'GET':
Speaker:{{ talk.speaker }}Speaker:{{ talk.speaker.user }}, {{ talk.speaker.title }}
Category:{{ talk.category }}