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

This commit is contained in:
Michael Hall 2018-04-25 10:36:01 -04:00
parent 5f16e176f9
commit 8e7c955928
18 changed files with 251 additions and 106 deletions

View file

@ -10,8 +10,6 @@ from .models.profiles import (
Member, Member,
Category, Category,
Topic, Topic,
Speaker,
Talk,
) )
from .models.search import Searchable from .models.search import Searchable
from .models.events import ( from .models.events import (
@ -22,7 +20,12 @@ from .models.events import (
EventPhoto, EventPhoto,
CommonEvent, CommonEvent,
Attendee, Attendee,
)
from .models.speakers import (
Speaker,
Talk,
Presentation, Presentation,
SpeakerRequest,
) )
admin.site.register(Language) admin.site.register(Language)

View file

@ -6,7 +6,7 @@ from django.utils import timezone
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .models.locale import Country, SPR, City 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 ( from .models.events import (
Event, Event,
EventComment, EventComment,
@ -14,6 +14,10 @@ from .models.events import (
EventSeries, EventSeries,
Place, Place,
EventPhoto, EventPhoto,
)
from .models.speakers import (
Speaker,
Talk,
Presentation, Presentation,
SpeakerRequest, SpeakerRequest,
) )
@ -343,6 +347,9 @@ class SpeakerBioForm(forms.ModelForm):
model = Speaker model = Speaker
fields = ['avatar', 'title', 'bio', 'categories'] fields = ['avatar', 'title', 'bio', 'categories']
class DeleteSpeakerForm(forms.Form):
confirm = forms.BooleanField(label="Yes, delete series", required=True)
class UserTalkForm(forms.ModelForm): class UserTalkForm(forms.ModelForm):
class Meta: class Meta:
model = Talk model = Talk

View file

@ -2,4 +2,4 @@ from .profiles import *
from .locale import * from .locale import *
from .search import * from .search import *
from .events import * from .events import *
from .speakers import *

View file

@ -376,28 +376,4 @@ class EventSeries(models.Model):
def __str__(self): def __str__(self):
return u'%s by %s at %s' % (self.name, self.team.name, self.start_time) 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)

View file

@ -86,10 +86,6 @@ class UserProfile(models.Model):
def moderating(self): def moderating(self):
return [member.team for member in Member.objects.filter(user=self, role__in=(Member.ADMIN, Member.MODERATOR))] 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): def can_create_event(self, team):
try: try:
if self.user.is_superuser: if self.user.is_superuser:
@ -306,62 +302,3 @@ class Topic(models.Model):
self.slug = slugify(self.name) self.slug = slugify(self.name)
super().save(*args, **kwargs) 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

104
events/models/speakers.py Normal file
View file

@ -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"

View file

@ -151,7 +151,7 @@
<td width="120px"><b>Presentations:</b></td> <td width="120px"><b>Presentations:</b></td>
<td> <td>
{% for presentation in presentation_list %} {% for presentation in presentation_list %}
<div><a href="{% url 'show-talk' presentation.talk.id %}">{{presentation.talk.title}}</a> by <a href="{% url 'show-profile' presentation.talk.speaker.user.id %}">{{presentation.talk.speaker}}</a></div> <div><a href="{% url 'show-talk' presentation.talk.id %}">{{presentation.talk.title}}</a> by <a href="{% url 'show-profile' presentation.talk.speaker.user.id %}">{{presentation.talk.speaker.user}}</a>, {{presentation.talk.speaker.title}}</div>
{% endfor %} {% endfor %}
<a class="btn btn-primary btn-sm" href="{% url 'propose-event-talk' event.id %}">Propose a talk</a> <a class="btn btn-primary btn-sm" href="{% url 'propose-event-talk' event.id %}">Propose a talk</a>
</td> </td>

View file

@ -8,6 +8,8 @@
<form action="{% url "add-talk" %}" method="post"> <form action="{% url "add-talk" %}" method="post">
{% csrf_token %} {% csrf_token %}
{% if event %}<input type="hidden" name="event" value="{{event.id}}" />{% endif %}
<div class="form-group"> <div class="form-group">
<table class="table"> <table class="table">
{{talk_form}} {{talk_form}}

View file

@ -0,0 +1,20 @@
{% extends "get_together/base.html" %}
{% block content %}
<h2>Confirm deletion</h2>
{% if speaker.talks.count > 0 %}
<div class="alerts">
<div class="alert alert-danger">This speaker profile has {{ speaker.talks.count }} talks!</div>
</div>
{% endif %}
Are you sure you want to delete <strong>{{speaker.title}}</strong>?
<form action="{% url "delete-speaker" speaker.id %}" method="post">
{% csrf_token %}
<div class="form-group">
{{ delete_form }}
<br />
<button type="submit" class="btn btn-danger">Delete Talk</button>
</div>
</form>
{% endblock %}

View file

@ -4,7 +4,7 @@
<h2>Confirm deletion</h2> <h2>Confirm deletion</h2>
{% if talk.future_presentations.count > 0 %} {% if talk.future_presentations.count > 0 %}
<div class="alerts"> <div class="alerts">
<div class="alert alert-danger">This talk has {{ talk.future_presentations.count }} pending event!</div> <div class="alert alert-danger">This talk has {{ talk.future_presentations.count }} pending events!</div>
</div> </div>
{% endif %} {% endif %}
Are you sure you want to delete <strong>{{talk.title}}</strong>? Are you sure you want to delete <strong>{{talk.title}}</strong>?

View file

@ -17,6 +17,8 @@
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
</div> </div>
</form> </form>
<a href="{% url 'delete-speaker' speaker.id %}" class="btn btn-danger">Delete</a>
{% endblock %} {% endblock %}
{% block javascript %} {% block javascript %}

View file

@ -55,7 +55,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<a href="{{event.get_absolute_url}}" class="btn btn-secondary">Cancel</a> <a href="{{event.get_absolute_url}}" class="btn btn-secondary">Cancel</a>
<a href="{% url 'add-user-talk' %}" class="btn btn-success">New Talk</a> <a href="{% url 'add-talk' %}?event={{event.id}}" class="btn btn-success">New Talk</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,50 @@
{% extends "get_together/base.html" %}
{% load markup static %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-9">
<h2>{{ talk.title }}
{% if talk.speaker.user == request.user.profile %}
<a href="{% url 'edit-talk' talk.id %}" class="btn btn-secondary btn-sm">Edit Talk</a>
{% endif %}
</h2>
<table class="table">
<tr>
<td><b>Speaker:</b></td><td>{{ talk.speaker }}</td>
</tr>
<tr>
<td><b>Category:</b></td><td>{{ talk.category }}</td>
</tr>
{% if talk.web_url %}
<tr>
<td><b>Website:</b></td><td><a href="{{ talk.web_url }}" target="_blank">{{ talk.web_url }}</a></td>
</tr>
{% endif %}
<tr>
<td><b>Abstract:</b></td><td>{{ talk.abstract|markdown }}</td>
</tr>
</table>
</div>
<div class="col-md-3">
<div class="container">
<div class="row">
<div class="col"><h4>Events ({{presentations.count}})</h4><hr/></div>
</div>
{% for presentation in presentations %}
<div class="row mb-3">
<div class="col">
<h6 class="mt-2 mb-0"><a href="{{presentation.event.get_absolute_url}}">{{presentation.event.name}}</a></h6>
<small class="text-muted">{{ presentation.event.local_start_time }}</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -35,11 +35,11 @@
{% if user.weburl %} {% if user.weburl %}
<p>Homepage: {{user.weburl}}</p> <p>Homepage: {{user.weburl}}</p>
{% endif %} {% endif %}
{% if user.talks %} {% if talks %}
<h3>Talks</h3> <h3>Talks</h3>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
{% for talk in user.talks %} {% for talk in talks %}
<div class="mr-3 mb-3 col-md-5"> <div class="mr-3 mb-3 col-md-5">
<div class="card box-shadow" > <div class="card box-shadow" >
<div class="card-body"> <div class="card-body">

View file

@ -14,10 +14,10 @@ from events.models.events import (
EventPhoto, EventPhoto,
Place, Place,
Attendee, Attendee,
Presentation,
update_event_searchable, update_event_searchable,
delete_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.models.profiles import Team, Organization, UserProfile, Member
from events.forms import ( from events.forms import (
TeamEventForm, TeamEventForm,

View file

@ -6,16 +6,17 @@ from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from events.models.profiles import UserProfile, Speaker, Talk from events.models.profiles import UserProfile
from events.forms import ( from events.forms import (
SpeakerBioForm, SpeakerBioForm,
DeleteSpeakerForm,
UserTalkForm, UserTalkForm,
DeleteTalkForm, DeleteTalkForm,
SchedulePresentationForm, 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 datetime
import simplejson import simplejson
@ -46,7 +47,7 @@ def add_speaker(request):
speaker_form = SpeakerBioForm(request.POST, request.FILES, instance=new_speaker) speaker_form = SpeakerBioForm(request.POST, request.FILES, instance=new_speaker)
if speaker_form.is_valid(): if speaker_form.is_valid():
new_speaker = speaker_form.save() new_speaker = speaker_form.save()
return redirect('show-talks') return redirect('user-talks')
else: else:
context = { context = {
'speaker': new_speaker, 'speaker': new_speaker,
@ -78,10 +79,41 @@ def edit_speaker(request, speaker_id):
return redirect('home') return redirect('home')
def delete_speaker(request, speaker_id): 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): 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): def add_talk(request):
new_talk = Talk() new_talk = Talk()
@ -92,13 +124,24 @@ def add_talk(request):
'talk': new_talk, 'talk': new_talk,
'talk_form': talk_form, '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) return render(request, 'get_together/speakers/create_talk.html', context)
elif request.method == 'POST': elif request.method == 'POST':
talk_form = UserTalkForm(request.POST, instance=new_talk) talk_form = UserTalkForm(request.POST, instance=new_talk)
talk_form.fields['speaker'].queryset = request.user.profile.speaker_set talk_form.fields['speaker'].queryset = request.user.profile.speaker_set
if talk_form.is_valid(): if talk_form.is_valid():
new_talk = talk_form.save() 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: else:
context = { context = {
'talk': new_talk, 'talk': new_talk,
@ -126,7 +169,7 @@ def edit_talk(request, talk_id):
talk_form.fields['speaker'].queryset = request.user.profile.speaker_set talk_form.fields['speaker'].queryset = request.user.profile.speaker_set
if talk_form.is_valid(): if talk_form.is_valid():
talk = talk_form.save() talk = talk_form.save()
return redirect('user-talks') return redirect('show-talk', talk.id)
else: else:
context = { context = {
'talk': talk, 'talk': talk,

View file

@ -33,10 +33,11 @@ def show_profile(request, user_id):
user = get_object_or_404(UserProfile, id=user_id) user = get_object_or_404(UserProfile, id=user_id)
teams = user.memberships.all() teams = user.memberships.all()
talks = Talk.objects.filter(speaker__user=user)
context = { context = {
'user': user, 'user': user,
'teams': teams, 'teams': teams,
'talks': talks,
} }
return render(request, 'get_together/users/show_profile.html', context) return render(request, 'get_together/users/show_profile.html', context)