252697b842
Upgrade site and modules to Django 2.2. Remove and replace obsolete functionality with current equivalents. Update requirements to latest versions where possible. Remove unused dependencies.
413 lines
15 KiB
Python
413 lines
15 KiB
Python
import hashlib
|
|
import random
|
|
import sys
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
|
from django.db.models import Q
|
|
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
|
from django.shortcuts import render, redirect, get_object_or_404
|
|
from django.urls import reverse
|
|
from django.views import static
|
|
|
|
from django.contrib import messages
|
|
from django.contrib.auth import get_user_model
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from symposion.proposals.models import (
|
|
ProposalBase, ProposalSection, ProposalKind
|
|
)
|
|
from symposion.proposals.models import SupportingDocument, AdditionalSpeaker
|
|
from symposion.speakers.models import Speaker
|
|
from symposion.utils.mail import send_email
|
|
|
|
from symposion.proposals.forms import (
|
|
AddSpeakerForm, SupportingDocumentCreateForm
|
|
)
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
def get_form(name):
|
|
dot = name.rindex(".")
|
|
mod_name, form_name = name[:dot], name[dot + 1:]
|
|
__import__(mod_name)
|
|
return getattr(sys.modules[mod_name], form_name)
|
|
|
|
|
|
def proposal_submit(request):
|
|
if not request.user.is_authenticated:
|
|
messages.info(request, _("To submit a proposal, please "
|
|
"<a href='{0}'>log in</a> and create a speaker profile "
|
|
"via the dashboard.".format(settings.LOGIN_URL)))
|
|
return redirect("dashboard") # @@@ unauth'd speaker info page?
|
|
else:
|
|
try:
|
|
request.user.speaker_profile
|
|
except ObjectDoesNotExist:
|
|
url = reverse("speaker_create")
|
|
messages.info(request, _("To submit a proposal, first "
|
|
"<a href='{0}'>create a speaker "
|
|
"profile</a>.".format(url)))
|
|
return redirect("dashboard")
|
|
|
|
kinds = []
|
|
for proposal_section in ProposalSection.available():
|
|
for kind in proposal_section.section.proposal_kinds.all():
|
|
kinds.append(kind)
|
|
|
|
return render(request, "symposion/proposals/proposal_submit.html", {
|
|
"kinds": kinds,
|
|
})
|
|
|
|
@login_required
|
|
def proposal_submit_kind(request, kind_slug):
|
|
|
|
kind = get_object_or_404(ProposalKind, slug=kind_slug)
|
|
|
|
try:
|
|
speaker_profile = request.user.speaker_profile
|
|
except ObjectDoesNotExist:
|
|
return redirect("speaker_create")
|
|
|
|
if not kind.section.proposalsection.is_available():
|
|
return redirect("proposal_submit")
|
|
|
|
form_class = get_form(settings.PROPOSAL_FORMS[kind_slug])
|
|
|
|
if request.method == "POST":
|
|
form = form_class(request.POST)
|
|
if form.is_valid():
|
|
proposal = form.save(commit=False)
|
|
proposal.kind = kind
|
|
proposal.speaker = speaker_profile
|
|
proposal.save()
|
|
form.save_m2m()
|
|
messages.success(request, _("Proposal submitted."))
|
|
if "add-speakers" in request.POST:
|
|
return redirect("proposal_speaker_manage", proposal.pk)
|
|
return redirect("dashboard")
|
|
else:
|
|
form = form_class()
|
|
|
|
return render(request, "symposion/proposals/proposal_submit_kind.html", {
|
|
"kind": kind,
|
|
"proposal_form": form,
|
|
})
|
|
|
|
|
|
@login_required
|
|
def proposal_speaker_manage(request, pk):
|
|
queryset = ProposalBase.objects.select_related("speaker")
|
|
proposal = get_object_or_404(queryset, pk=pk)
|
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
|
|
|
if proposal.speaker != request.user.speaker_profile:
|
|
raise Http404()
|
|
|
|
if request.method == "POST":
|
|
add_speaker_form = AddSpeakerForm(request.POST, proposal=proposal)
|
|
if add_speaker_form.is_valid():
|
|
message_ctx = {
|
|
"proposal": proposal,
|
|
}
|
|
|
|
def create_speaker_token(email_address):
|
|
# create token and look for an existing speaker to prevent
|
|
# duplicate tokens and confusing the pending speaker
|
|
try:
|
|
pending = Speaker.objects.get(
|
|
Q(user=None, invite_email=email_address)
|
|
)
|
|
except Speaker.DoesNotExist:
|
|
salt = hashlib.sha1(str(random.random()).encode('UTF-8')).hexdigest()[:5]
|
|
saltedemail = (salt + email_address).encode('UTF-8')
|
|
token = hashlib.sha1(saltedemail).hexdigest()
|
|
pending = Speaker.objects.create(
|
|
invite_email=email_address,
|
|
invite_token=token,
|
|
)
|
|
else:
|
|
token = pending.invite_token
|
|
return pending, token
|
|
email_address = add_speaker_form.cleaned_data["email"]
|
|
# check if email is on the site now
|
|
try:
|
|
user = User.objects.get(email=email_address)
|
|
except ObjectDoesNotExist:
|
|
user = None
|
|
except MultipleObjectsReturned:
|
|
# FIXME: This is not handled in the previous code, so I'm not
|
|
# going to frett on this now, but should be handled as it is
|
|
# an occourance that really, really shouldn't occour.
|
|
# Previously, code took [0] from the list and continued.
|
|
raise NotImplementedError("Non unique email should not occour")
|
|
if user:
|
|
# should only be one since we enforce unique email
|
|
message_ctx["user"] = user
|
|
# look for speaker profile
|
|
try:
|
|
speaker = user.speaker_profile
|
|
except ObjectDoesNotExist:
|
|
speaker, token = create_speaker_token(email_address)
|
|
message_ctx["token"] = token
|
|
# fire off email to user to create profile
|
|
send_email(
|
|
[email_address], "speaker_no_profile",
|
|
context=message_ctx
|
|
)
|
|
else:
|
|
# fire off email to user letting them they are loved.
|
|
send_email(
|
|
[email_address], "speaker_addition",
|
|
context=message_ctx
|
|
)
|
|
else:
|
|
speaker, token = create_speaker_token(email_address)
|
|
message_ctx["token"] = token
|
|
# fire off email letting user know about site and to create
|
|
# account and speaker profile
|
|
send_email(
|
|
[email_address], "speaker_invite",
|
|
context=message_ctx
|
|
)
|
|
invitation, created = AdditionalSpeaker.objects.get_or_create(
|
|
proposalbase=proposal.proposalbase_ptr, speaker=speaker)
|
|
messages.success(request, "Speaker invited to proposal.")
|
|
return redirect("proposal_speaker_manage", proposal.pk)
|
|
else:
|
|
add_speaker_form = AddSpeakerForm(proposal=proposal)
|
|
ctx = {
|
|
"proposal": proposal,
|
|
"speakers": proposal.speakers(),
|
|
"add_speaker_form": add_speaker_form,
|
|
}
|
|
return render(request, "symposion/proposals/proposal_speaker_manage.html", ctx)
|
|
|
|
|
|
@login_required
|
|
def proposal_edit(request, pk):
|
|
queryset = ProposalBase.objects.select_related("speaker")
|
|
proposal = get_object_or_404(queryset, pk=pk)
|
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
|
|
|
if request.user != proposal.speaker.user:
|
|
raise Http404()
|
|
|
|
if not proposal.can_edit():
|
|
ctx = {
|
|
"title": "Proposal editing closed",
|
|
"body": "Proposal editing is closed for this session type."
|
|
}
|
|
return render(request, "symposion/proposals/proposal_error.html", ctx)
|
|
|
|
form_class = get_form(settings.PROPOSAL_FORMS[proposal.kind.slug])
|
|
|
|
if request.method == "POST":
|
|
form = form_class(request.POST, instance=proposal)
|
|
if form.is_valid():
|
|
form.save()
|
|
if hasattr(proposal, "reviews"):
|
|
# Miniconf updates should only email the admins
|
|
if proposal.kind.slug == 'miniconf':
|
|
users = User.objects.filter(username__in=settings.ADMIN_USERNAMES)
|
|
else:
|
|
users = User.objects.filter(
|
|
Q(review__proposal=proposal) |
|
|
Q(proposalmessage__proposal=proposal)
|
|
)
|
|
users = users.exclude(id=request.user.id).distinct()
|
|
for user in users:
|
|
ctx = {
|
|
"user": request.user,
|
|
"proposal": proposal,
|
|
}
|
|
send_email(
|
|
[user.email], "proposal_updated",
|
|
context=ctx
|
|
)
|
|
messages.success(request, "Proposal updated.")
|
|
return redirect("proposal_detail", proposal.pk)
|
|
else:
|
|
form = form_class(instance=proposal)
|
|
|
|
return render(request, "symposion/proposals/proposal_edit.html", {
|
|
"proposal": proposal,
|
|
"form": form,
|
|
})
|
|
|
|
|
|
@login_required
|
|
def proposal_detail(request, pk):
|
|
queryset = ProposalBase.objects.select_related("speaker", "speaker__user")
|
|
proposal = get_object_or_404(queryset, pk=pk)
|
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
|
|
|
if request.user not in [p.user for p in proposal.speakers()]:
|
|
raise Http404()
|
|
|
|
if "symposion.reviews" in settings.INSTALLED_APPS:
|
|
from symposion.reviews.forms import SpeakerCommentForm
|
|
message_form = SpeakerCommentForm()
|
|
if request.method == "POST":
|
|
message_form = SpeakerCommentForm(request.POST)
|
|
if message_form.is_valid():
|
|
|
|
message = message_form.save(commit=False)
|
|
message.user = request.user
|
|
message.proposal = proposal
|
|
message.save()
|
|
|
|
ProposalMessage = SpeakerCommentForm.Meta.model
|
|
reviewers = User.objects.filter(
|
|
id__in=ProposalMessage.objects.filter(
|
|
proposal=proposal
|
|
).exclude(
|
|
user=request.user
|
|
).distinct().values_list("user", flat=True)
|
|
)
|
|
|
|
for reviewer in reviewers:
|
|
ctx = {
|
|
"proposal": proposal,
|
|
"message": message,
|
|
"reviewer": True,
|
|
}
|
|
send_email(
|
|
[reviewer.email], "proposal_new_message",
|
|
context=ctx
|
|
)
|
|
|
|
return redirect(request.path)
|
|
else:
|
|
message_form = SpeakerCommentForm()
|
|
else:
|
|
message_form = None
|
|
|
|
return render(request, "symposion/proposals/proposal_detail.html", {
|
|
"proposal": proposal,
|
|
"message_form": message_form
|
|
})
|
|
|
|
|
|
@login_required
|
|
def proposal_cancel(request, pk):
|
|
queryset = ProposalBase.objects.select_related("speaker")
|
|
proposal = get_object_or_404(queryset, pk=pk)
|
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
|
|
|
if proposal.speaker.user != request.user:
|
|
return HttpResponseForbidden()
|
|
|
|
if request.method == "POST":
|
|
proposal.cancelled = True
|
|
proposal.save()
|
|
# @@@ fire off email to submitter and other speakers
|
|
messages.success(request, "%s has been cancelled" % proposal.title)
|
|
return redirect("dashboard")
|
|
|
|
return render(request, "symposion/proposals/proposal_cancel.html", {
|
|
"proposal": proposal,
|
|
})
|
|
|
|
|
|
@login_required
|
|
def proposal_leave(request, pk):
|
|
queryset = ProposalBase.objects.select_related("speaker")
|
|
proposal = get_object_or_404(queryset, pk=pk)
|
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
|
|
|
try:
|
|
speaker = proposal.additional_speakers.get(user=request.user)
|
|
except ObjectDoesNotExist:
|
|
return HttpResponseForbidden()
|
|
if request.method == "POST":
|
|
proposal.additional_speakers.remove(speaker)
|
|
# @@@ fire off email to submitter and other speakers
|
|
messages.success(request, "You are no longer speaking on %s" % proposal.title)
|
|
return redirect("dashboard")
|
|
ctx = {
|
|
"proposal": proposal,
|
|
}
|
|
return render(request, "symposion/proposals/proposal_leave.html", ctx)
|
|
|
|
|
|
@login_required
|
|
def proposal_pending_join(request, pk):
|
|
proposal = get_object_or_404(ProposalBase, pk=pk)
|
|
speaking = get_object_or_404(AdditionalSpeaker, speaker=request.user.speaker_profile,
|
|
proposalbase=proposal)
|
|
if speaking.status == AdditionalSpeaker.SPEAKING_STATUS_PENDING:
|
|
speaking.status = AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED
|
|
speaking.save()
|
|
messages.success(request, "You have accepted the invitation to join %s" % proposal.title)
|
|
return redirect("dashboard")
|
|
else:
|
|
return redirect("dashboard")
|
|
|
|
|
|
@login_required
|
|
def proposal_pending_decline(request, pk):
|
|
proposal = get_object_or_404(ProposalBase, pk=pk)
|
|
speaking = get_object_or_404(AdditionalSpeaker, speaker=request.user.speaker_profile,
|
|
proposalbase=proposal)
|
|
if speaking.status == AdditionalSpeaker.SPEAKING_STATUS_PENDING:
|
|
speaking.status = AdditionalSpeaker.SPEAKING_STATUS_DECLINED
|
|
speaking.save()
|
|
messages.success(request, "You have declined to speak on %s" % proposal.title)
|
|
return redirect("dashboard")
|
|
else:
|
|
return redirect("dashboard")
|
|
|
|
|
|
@login_required
|
|
def document_create(request, proposal_pk):
|
|
queryset = ProposalBase.objects.select_related("speaker")
|
|
proposal = get_object_or_404(queryset, pk=proposal_pk)
|
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
|
|
|
if proposal.cancelled:
|
|
return HttpResponseForbidden()
|
|
|
|
if request.method == "POST":
|
|
form = SupportingDocumentCreateForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
document = form.save(commit=False)
|
|
document.proposal = proposal
|
|
document.uploaded_by = request.user
|
|
document.save()
|
|
return redirect("proposal_detail", proposal.pk)
|
|
else:
|
|
form = SupportingDocumentCreateForm()
|
|
|
|
return render(request, "symposion/proposals/document_create.html", {
|
|
"proposal": proposal,
|
|
"form": form,
|
|
})
|
|
|
|
|
|
@login_required
|
|
def document_download(request, pk, *args):
|
|
document = get_object_or_404(SupportingDocument, pk=pk)
|
|
if getattr(settings, "USE_X_ACCEL_REDIRECT", False):
|
|
response = HttpResponse()
|
|
response["X-Accel-Redirect"] = document.file.url
|
|
# delete content-type to allow Gondor to determine the filetype and
|
|
# we definitely don't want Django's crappy default :-)
|
|
del response["content-type"]
|
|
else:
|
|
response = static.serve(request, document.file.name, document_root=settings.MEDIA_ROOT)
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def document_delete(request, pk):
|
|
document = get_object_or_404(SupportingDocument, pk=pk, uploaded_by=request.user)
|
|
proposal_pk = document.proposal.pk
|
|
|
|
if request.method == "POST":
|
|
document.delete()
|
|
|
|
return redirect("proposal_detail", proposal_pk)
|