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,
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)

View file

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

View file

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

View file

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

View file

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

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>
{% 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 %}
<a class="btn btn-primary btn-sm" href="{% url 'propose-event-talk' event.id %}">Propose a talk</a>
</td>

View file

@ -8,6 +8,8 @@
<form action="{% url "add-talk" %}" method="post">
{% csrf_token %}
{% if event %}<input type="hidden" name="event" value="{{event.id}}" />{% endif %}
<div class="form-group">
<table class="table">
{{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>
{% if talk.future_presentations.count > 0 %}
<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>
{% endif %}
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>
</div>
</form>
<a href="{% url 'delete-speaker' speaker.id %}" class="btn btn-danger">Delete</a>
{% endblock %}
{% block javascript %}

View file

@ -55,7 +55,7 @@
<div class="row">
<div class="col">
<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>

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 %}
<p>Homepage: {{user.weburl}}</p>
{% endif %}
{% if user.talks %}
{% if talks %}
<h3>Talks</h3>
<div class="container">
<div class="row">
{% for talk in user.talks %}
{% for talk in talks %}
<div class="mr-3 mb-3 col-md-5">
<div class="card box-shadow" >
<div class="card-body">

View file

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

View file

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

View file

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