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 %}
+
+ {% 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
+
+
+
+{% 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
+
+
+
+{% 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}}?
+
+{% 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
+
+
+
+{% 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
+
+
+
+
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 %}
+
+{% 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 %}
+
+{% endfor %}
+
+
+
+
+
+{% for talk in talks %}
+
+{% 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 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 %}
+
+ {% 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
+
+