diff --git a/events/admin.py b/events/admin.py index d0375c4..481e81d 100644 --- a/events/admin.py +++ b/events/admin.py @@ -3,9 +3,30 @@ 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, +) 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, +) +from .models.speakers import ( + Speaker, + Talk, + Presentation, + SpeakerRequest, +) admin.site.register(Language) admin.site.register(Continent) @@ -24,6 +45,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) @@ -33,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): @@ -101,12 +127,30 @@ 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) + +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 ad28d25..18552a1 100644 --- a/events/forms.py +++ b/events/forms.py @@ -7,7 +7,20 @@ 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.events import ( + Event, + EventComment, + CommonEvent, + EventSeries, + Place, + EventPhoto, +) +from .models.speakers import ( + Speaker, + Talk, + Presentation, + SpeakerRequest, +) import recurrence import pytz @@ -262,15 +275,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: @@ -317,3 +342,24 @@ class NewCommonEventForm(forms.ModelForm): 'end_time': DateTimeWidget } +class SpeakerBioForm(forms.ModelForm): + class Meta: + 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 + 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/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..a71d4b6 --- /dev/null +++ b/events/migrations/0028_add_speaker_models.py @@ -0,0 +1,120 @@ +# 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): + + 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')), + ('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)), + ], + ), + 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, related_name='presentations', to='events.Talk'), + ), + ] 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/__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 5e0d7cb..d564aef 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) 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..7751fbf 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) @@ -222,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=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) + @property def location_name(self): if self.city: @@ -275,17 +282,28 @@ 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) 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/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/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..f35f2dc --- /dev/null +++ b/get_together/templates/get_together/events/schedule_event_talks.html @@ -0,0 +1,116 @@ +{% 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 %} +
+

Manage talks for: {{event.name}}

+{% if pending_talks %} +
+
+{% for presentation in pending_talks %} +
+ + {% csrf_token %} +
+
+
+

{{presentation.talk.title}}

+
+ {{ presentation.talk.speaker }} +
+ + +
+
+
+
+
+
+{% endfor %} +
+{% endif %} +{% 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 e9a32ee..846ff9f 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,20 @@ {% endif %} + {% if event.team.is_premium %} + + 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 %} + {{pending_presentations}} proposed talks + {% endif %} + + + {% endif %}
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..3772306 --- /dev/null +++ b/get_together/templates/get_together/speakers/create_talk.html @@ -0,0 +1,33 @@ +{% extends "get_together/base.html" %} +{% load static %} + + +{% block content %} +

New Talk

+ + +
+{% csrf_token %} +{% if event %}{% endif %} + +
+ + {{talk_form}} +
+
+ +
+
+{% endblock %} + + +{% block javascript %} + +{% endblock %} 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 new file mode 100644 index 0000000..386a076 --- /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 events!
+
+{% 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..f5bbb21 --- /dev/null +++ b/get_together/templates/get_together/speakers/edit_speaker.html @@ -0,0 +1,48 @@ +{% extends "get_together/base.html" %} +{% load static %} + + +{% block content %} +

Edit Speaker

+ + +
+{% csrf_token %} +
+ + + {{speaker_form}} +
+
+ +
+
+Delete + +{% 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..fe4dd21 --- /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/speakers/show_speaker.html b/get_together/templates/get_together/speakers/show_speaker.html new file mode 100644 index 0000000..541fdd9 --- /dev/null +++ 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 new file mode 100644 index 0000000..bfe0f66 --- /dev/null +++ b/get_together/templates/get_together/speakers/show_talk.html @@ -0,0 +1,51 @@ +{% 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.user }}, {{ talk.speaker.title }}
Category:{{ talk.category }}
Website:{{ talk.web_url }}
Abstract:{{ talk.abstract|markdown }}
+ +
+ +
+
+
+

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/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..1e883c6 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,10 +35,29 @@ {% if user.weburl %}

Homepage: {{user.weburl}}

{% endif %} +{% if talks %} +

Talks

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

{{talk.title}}

+
+ {{ talk.speaker }} +
+
+
+
+ {% endfor %} +
+
+{% endif %}
{% if teams %} -

Your Teams

+

Teams

    {% for t in teams %}
  • @@ -33,6 +66,14 @@ {% endfor %}
{% endif %} +

Categories

+
    + {% for c in user.categories.all %} +
  • + {{c.name}} +
  • + {% endfor %} +
diff --git a/get_together/urls.py b/get_together/urls.py index 1766586..24da216 100644 --- a/get_together/urls.py +++ b/get_together/urls.py @@ -49,6 +49,17 @@ 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//', views.show_speaker, name='show-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 +81,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 32cf08c..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 @@ -72,6 +73,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 diff --git a/get_together/views/events.py b/get_together/views/events.py index 3a1ec5d..64fb18f 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, + 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, @@ -20,7 +29,7 @@ from events.forms import ( NewPlaceForm, UploadEventPhotoForm, NewCommonEventForm - ) +) from events import location import datetime @@ -56,6 +65,8 @@ 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.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) @@ -220,13 +231,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 +395,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: diff --git a/get_together/views/speakers.py b/get_together/views/speakers.py new file mode 100644 index 0000000..217badd --- /dev/null +++ b/get_together/views/speakers.py @@ -0,0 +1,278 @@ +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 +from django.http import HttpResponse, JsonResponse +from django.core.exceptions import ObjectDoesNotExist + +from events.models.profiles import UserProfile +from events.forms import ( + SpeakerBioForm, + DeleteSpeakerForm, + UserTalkForm, + DeleteTalkForm, + SchedulePresentationForm, +) + +from events.models.events import Event +from events.models.speakers import Speaker, Talk, Presentation, SpeakerRequest +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 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_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('user-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): + 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): + 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() + 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, + } + 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() + 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, + '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('show-talk', talk.id) + 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 not event.team.is_premium: + 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': + 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): + 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) + 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)