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}}
+
{{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)