diff --git a/symposion/speakers/__init__.py b/symposion/speakers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/symposion/speakers/admin.py b/symposion/speakers/admin.py
new file mode 100644
index 00000000..9c1174b1
--- /dev/null
+++ b/symposion/speakers/admin.py
@@ -0,0 +1,9 @@
+from django.contrib import admin
+
+from symposion.speakers.models import Speaker
+
+
+admin.site.register(Speaker,
+ list_display = ["name", "email", "twitter_username", "sessions_preference", "created"],
+ search_fields = ["name"],
+)
\ No newline at end of file
diff --git a/symposion/speakers/fixture_gen.py b/symposion/speakers/fixture_gen.py
new file mode 100644
index 00000000..26993fcb
--- /dev/null
+++ b/symposion/speakers/fixture_gen.py
@@ -0,0 +1,31 @@
+from django.contrib.auth.models import User
+
+from fixture_generator import fixture_generator
+
+from symposion.speakers.models import Speaker
+
+
+@fixture_generator(Speaker, User)
+def speakers():
+ guido = User.objects.create_user("guido", "guido@python.org", "pythonisawesome")
+ matz = User.objects.create_user("matz", "matz@ruby.org", "pythonsucks")
+ larry = User.objects.create_user("larryw", "larry@perl.org", "linenoisehere")
+
+ Speaker.objects.create(
+ user=guido,
+ name="Guido van Rossum",
+ biography="I wrote Python, and named it after Monty Python",
+ twitter_username="gvanrossum",
+ )
+ Speaker.objects.create(
+ user=matz,
+ name="Yukihiro Matsumoto",
+ biography="I wrote Ruby, and named it after the rare gem Ruby, a pun "
+ "on Perl/pearl.",
+ twitter_username="yukihiro_matz"
+ )
+ Speaker.objects.create(
+ user=larry,
+ name="Larry Wall",
+ biography="I wrote Perl, and named it after the Parable of the Pearl",
+ )
diff --git a/symposion/speakers/forms.py b/symposion/speakers/forms.py
new file mode 100644
index 00000000..c0dd1257
--- /dev/null
+++ b/symposion/speakers/forms.py
@@ -0,0 +1,63 @@
+from django import forms
+
+from django.contrib import messages
+
+from markitup.widgets import MarkItUpWidget
+
+from symposion.speakers.models import Speaker
+
+
+class SpeakerForm(forms.ModelForm):
+
+ sessions_preference = forms.ChoiceField(
+ widget=forms.RadioSelect(),
+ choices=Speaker.SESSION_COUNT_CHOICES,
+ required=False,
+ help_text="If you've submitted multiple proposals, please let us know if you only want to give one or if you'd like to give two talks."
+ )
+
+ class Meta:
+ model = Speaker
+ fields = [
+ "name",
+ "biography",
+ "photo",
+ "twitter_username",
+ "sessions_preference"
+ ]
+ widgets = {
+ "biography": MarkItUpWidget(),
+ }
+
+ def clean_twitter_username(self):
+ value = self.cleaned_data["twitter_username"]
+ if value.startswith("@"):
+ value = value[1:]
+ return value
+
+ def clean_sessions_preference(self):
+ value = self.cleaned_data["sessions_preference"]
+ if not value:
+ return None
+ return int(value)
+
+
+# class SignupForm(PinaxSignupForm):
+
+# def save(self, speaker, request=None):
+# # don't assume a username is available. it is a common removal if
+# # site developer wants to use email authentication.
+# username = self.cleaned_data.get("username")
+# email = self.cleaned_data["email"]
+# new_user = self.create_user(username)
+# if speaker.invite_email == new_user.email:
+# # already verified so can just create
+# EmailAddress(user=new_user, email=email, verified=True, primary=True).save()
+# else:
+# if request:
+# messages.info(request, u"Confirmation email sent to %(email)s" % {"email": email})
+# EmailAddress.objects.add_email(new_user, email)
+# new_user.is_active = False
+# new_user.save()
+# self.after_signup(new_user)
+# return new_user
diff --git a/symposion/speakers/management/__init__.py b/symposion/speakers/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/symposion/speakers/management/commands/__init__.py b/symposion/speakers/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/symposion/speakers/management/commands/export_speaker_data.py b/symposion/speakers/management/commands/export_speaker_data.py
new file mode 100644
index 00000000..5f9f565d
--- /dev/null
+++ b/symposion/speakers/management/commands/export_speaker_data.py
@@ -0,0 +1,19 @@
+import csv
+import os
+
+from django.core.management.base import BaseCommand, CommandError
+
+from symposion.speakers.models import Speaker
+
+
+class Command(BaseCommand):
+
+ def handle(self, *args, **options):
+ csv_file = csv.writer(open(os.path.join(os.getcwd(), "speakers.csv"), "wb"))
+ csv_file.writerow(["Name", "Bio"])
+
+ for speaker in Speaker.objects.all():
+ csv_file.writerow([
+ speaker.name.encode("utf-8"),
+ speaker.biography.encode("utf-8"),
+ ])
diff --git a/symposion/speakers/models.py b/symposion/speakers/models.py
new file mode 100644
index 00000000..8cc4d16d
--- /dev/null
+++ b/symposion/speakers/models.py
@@ -0,0 +1,55 @@
+import datetime
+
+from django.db import models
+from django.core.urlresolvers import reverse
+
+from django.contrib.auth.models import User
+
+from markitup.fields import MarkupField
+
+
+class Speaker(models.Model):
+
+ SESSION_COUNT_CHOICES = [
+ (1, "One"),
+ (2, "Two")
+ ]
+
+ user = models.OneToOneField(User, null=True, related_name="speaker_profile")
+ name = models.CharField(max_length=100, help_text="As you would like it to appear in the conference program.")
+ biography = MarkupField(help_text="A little bit about you. Edit using Markdown.")
+ photo = models.ImageField(upload_to="speaker_photos", blank=True)
+ twitter_username = models.CharField(
+ max_length = 15,
+ blank = True,
+ help_text = "Your Twitter account"
+ )
+ annotation = models.TextField() # staff only
+ invite_email = models.CharField(max_length=200, unique=True, null=True, db_index=True)
+ invite_token = models.CharField(max_length=40, db_index=True)
+ created = models.DateTimeField(
+ default = datetime.datetime.now,
+ editable = False
+ )
+ sessions_preference = models.IntegerField(
+ choices=SESSION_COUNT_CHOICES,
+ null=True,
+ blank=True,
+ help_text="If you've submitted multiple proposals, please let us know if you only want to give one or if you'd like to give two talks. You may submit more than two proposals."
+ )
+
+ def __unicode__(self):
+ if self.user:
+ return self.name
+ else:
+ return "?"
+
+ def get_absolute_url(self):
+ return reverse("speaker_edit")
+
+ @property
+ def email(self):
+ if self.user is not None:
+ return self.user.email
+ else:
+ return self.invite_email
diff --git a/symposion/speakers/urls.py b/symposion/speakers/urls.py
new file mode 100644
index 00000000..2dc083a8
--- /dev/null
+++ b/symposion/speakers/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns("symposion.speakers.views",
+ url(r"^create/$", "speaker_create", name="speaker_create"),
+ url(r"^create/(\w+)/$", "speaker_create_token", name="speaker_create_token"),
+ url(r"^edit/(?:(?P\d+)/)?$", "speaker_edit", name="speaker_edit"),
+ url(r"^profile/(?P\d+)/$", "speaker_profile", name="speaker_profile"),
+)
diff --git a/symposion/speakers/views.py b/symposion/speakers/views.py
new file mode 100644
index 00000000..54716e88
--- /dev/null
+++ b/symposion/speakers/views.py
@@ -0,0 +1,121 @@
+from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Q
+from django.http import Http404, HttpResponse
+from django.shortcuts import render, redirect, get_object_or_404
+from django.template import RequestContext
+
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+
+from symposion.proposals.models import ProposalBase
+from symposion.speakers.forms import SpeakerForm #, SignupForm
+from symposion.speakers.models import Speaker
+
+
+@login_required
+def speaker_create(request):
+ try:
+ return redirect(request.user.speaker_profile)
+ except ObjectDoesNotExist:
+ pass
+
+ if request.method == "POST":
+ try:
+ speaker = Speaker.objects.get(invite_email=request.user.email)
+ found = True
+ except Speaker.DoesNotExist:
+ speaker = None
+ found = False
+ form = SpeakerForm(request.POST, request.FILES, instance=speaker)
+
+ if form.is_valid():
+ speaker = form.save(commit=False)
+ speaker.user = request.user
+ if not found:
+ speaker.invite_email = None
+ speaker.save()
+ messages.success(request, "Speaker profile created.")
+ return redirect("dashboard")
+ else:
+ form = SpeakerForm(initial = {"name": request.user.get_full_name()})
+
+ return render(request, "speakers/speaker_create.html", {
+ "form": form,
+ })
+
+
+def speaker_create_token(request, token):
+ speaker = get_object_or_404(Speaker, invite_token=token)
+ request.session["pending-token"] = token
+ if request.user.is_authenticated():
+ # check for speaker profile
+ try:
+ existing_speaker = request.user.speaker_profile
+ except ObjectDoesNotExist:
+ pass
+ else:
+ del request.session["pending-token"]
+ additional_speakers = ProposalBase.additional_speakers.through
+ additional_speakers._default_manager.filter(
+ speaker = speaker
+ ).update(
+ speaker = existing_speaker
+ )
+ messages.info(request, "You have been associated with all pending "
+ "talk proposals")
+ return redirect("dashboard")
+ else:
+ if not request.user.is_authenticated():
+ return redirect("account_login")
+ return redirect("speaker_create")
+
+
+@login_required
+def speaker_edit(request, pk=None):
+ if pk is None:
+ try:
+ speaker = request.user.speaker_profile
+ except Speaker.DoesNotExist:
+ return redirect("speaker_create")
+ else:
+ if request.user.groups.filter(name="organizer").exists(): # @@@
+ speaker = get_object_or_404(Speaker, pk=pk)
+ else:
+ raise Http404()
+
+ if request.method == "POST":
+ form = SpeakerForm(request.POST, request.FILES, instance=speaker)
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Speaker profile updated.")
+ return redirect("dashboard")
+ else:
+ form = SpeakerForm(instance=speaker)
+
+ return render(request, "speakers/speaker_edit.html", {
+ "form": form,
+ })
+
+
+def speaker_profile(request, pk, template_name="speakers/speaker_profile.html", extra_context=None):
+
+ if extra_context is None:
+ extra_context = {}
+
+ speaker = get_object_or_404(Speaker, pk=pk)
+
+ # schedule may not be installed so we need to check for sessions
+ if hasattr(speaker, "sessions"):
+ sessions = speaker.sessions.exclude(slot=None).order_by("slot__start")
+ else:
+ sessions = []
+
+ if not sessions:
+ raise Http404()
+
+ return render_to_response(template_name, dict({
+ "speaker": speaker,
+ "sessions": sessions,
+ "timezone": settings.SCHEDULE_TIMEZONE,
+ }, **extra_context), context_instance=RequestContext(request))