Skip to content

Commit

Permalink
Merge pull request #859 from pneff/job-review-improvements
Browse files Browse the repository at this point in the history
Job review improvements
  • Loading branch information
malemburg committed Jan 19, 2016
2 parents a1916ef + e1072a6 commit 2370475
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 79 deletions.
21 changes: 4 additions & 17 deletions jobs/listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django_comments_xtd.utils import send_mail

from .models import Job
from .signals import job_was_approved, job_was_rejected
from .signals import job_was_submitted, job_was_approved, job_was_rejected

### Globals

Expand Down Expand Up @@ -101,29 +101,16 @@ def on_job_was_rejected(sender, job, rejecting_user, **kwargs):
'jobs/email/job_was_rejected.txt')


@receiver(models.signals.post_save, sender=Job, dispatch_uid="job_was_submitted")
def on_job_was_submitted(sender, instance, created=False, **kwargs):
@receiver(job_was_submitted)
def on_job_was_submitted(sender, job, **kwargs):
"""
Notify the jobs board when a new job has been submitted for approval
"""
# Only send emails for newly created Jobs
if not created:
return

# Skip in fixtures
if kwargs.get('raw', False):
return

# Only new Jobs in review status should trigger the email
Job = models.get_model('jobs', 'Job')
if instance.status != Job.STATUS_REVIEW:
return

subject_template = loader.get_template('jobs/email/job_was_submitted_subject.txt')
message_template = loader.get_template('jobs/email/job_was_submitted.txt')

message_context = Context({'content_object': instance,
message_context = Context({'content_object': job,
'site': Site.objects.get_current()})
subject = subject_template.render(message_context)
message = message_template.render(message_context)
Expand Down
12 changes: 11 additions & 1 deletion jobs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from fastly.utils import purge_url

from .managers import JobQuerySet, JobTypeQuerySet, JobCategoryQuerySet
from .signals import job_was_approved, job_was_rejected
from .signals import job_was_submitted, job_was_approved, job_was_rejected


DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext')
Expand Down Expand Up @@ -169,6 +169,16 @@ def save(self, **kwargs):

return super().save(**kwargs)

def review(self):
"""Updates job status to Job.STATUS_REVIEW after preview was done by
user.
"""
old_status = self.status
self.status = Job.STATUS_REVIEW
self.save()
if old_status != self.status:
job_was_submitted.send(sender=self.__class__, job=self)

def approve(self, approving_user):
"""Updates job status to Job.STATUS_APPROVED after approval was issued
by approving_user.
Expand Down
2 changes: 2 additions & 0 deletions jobs/signals.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.dispatch import Signal

# Sent after job offer was submitted for review
job_was_submitted = Signal(providing_args=['job'])
# Sent after job offer was approved
job_was_approved = Signal(providing_args=['approving_user', 'job'])
# Sent after job offer was rejected
Expand Down
24 changes: 19 additions & 5 deletions jobs/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,25 +230,39 @@ def test_job_create(self):
User = get_user_model()
creator = User.objects.create_user(username, email, password)
self.client.login(username=creator.username, password='secret')
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
response = self.client.post(url, post_data, follow=True)

# Job was saved in draft mode
jobs = Job.objects.filter(company_name='Other Studio')
self.assertEqual(len(jobs), 1)

job = jobs[0]

preview_url = reverse('jobs:job_preview', kwargs={'pk': job.pk})
self.assertRedirects(response, preview_url)

self.assertNotEqual(job.created, None)
self.assertNotEqual(job.updated, None)
self.assertEqual(job.creator, creator)
self.assertEqual(job.status, 'draft')

self.assertEqual(len(mail.outbox), 0)

# Submit again to save
response = self.client.post(preview_url, {'action': 'review'})

# Job was now moved to review status
job = Job.objects.get(pk=job.pk)
self.assertEqual(job.status, 'review')

# One email was sent
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(
mail.outbox[0].subject,
"Job Submitted for Approval: {}".format(job.display_name)
)

del mail.outbox[:]

def test_job_create_prepopulate_email(self):
create_url = reverse('jobs:job_create')

Expand Down
1 change: 1 addition & 0 deletions jobs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
url(r'^(?P<pk>\d+)/edit/$', views.JobEdit.as_view(), name='job_edit'),
url(r'^(?P<pk>\d+)/publish/$', views.JobPublish.as_view(), name='job_publish'),
url(r'^(?P<pk>\d+)/review/$', views.JobDetailReview.as_view(), name='job_detail_review'),
url(r'^(?P<pk>\d+)/preview/$', views.JobPreview.as_view(), name='job_preview'),
url(r'^(?P<pk>\d+)/$', views.JobDetail.as_view(), name='job_detail'),
]
126 changes: 122 additions & 4 deletions jobs/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from braces.views import LoginRequiredMixin, GroupRequiredMixin
from django.contrib import messages
from django.contrib import comments, messages
from django.contrib.comments import signals
from django.contrib.comments.views.comments import CommentPostBadRequest
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import Http404, HttpResponse
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.utils.html import escape
from django.views.generic import ListView, DetailView, CreateView, UpdateView, TemplateView, View

from .forms import JobForm
Expand Down Expand Up @@ -176,6 +179,11 @@ def post(self, request):
except (KeyError, Job.DoesNotExist):
return redirect('jobs:job_review')

if request.POST.get('comment', '').strip():
ret = self._save_comment(job)
if ret is not True:
return ret

if action == 'approve':
job.approve(request.user)
messages.add_message(self.request, messages.SUCCESS, "'%s' approved." % job)
Expand All @@ -196,6 +204,49 @@ def post(self, request):

return redirect('jobs:job_review')

def _save_comment(self, job):
data = self.request.POST.copy()
if self.request.user.is_authenticated():
if not data.get('name', ''):
data['name'] = self.request.user.get_full_name() or self.request.user.get_username()
if not data.get('email', ''):
data['email'] = self.request.user.email

form = comments.get_form()(job, data=data)
if form.security_errors():
return CommentPostBadRequest(
"The comment form failed security verification: %s" % \
escape(str(form.security_errors())))
if form.errors:
return CommentPostBadRequest(
"Validation error in comment: %s" % \
escape(str(form.errors)))

comment = form.get_comment_object()
comment.ip_address = self.request.META.get("REMOTE_ADDR", None)
comment.user = self.request.user

# Signal that the comment is about to be saved
responses = signals.comment_will_be_posted.send(
sender=comment.__class__,
comment=comment,
request=self.request
)

for (receiver, response) in responses:
if response == False:
return CommentPostBadRequest(
"comment_will_be_posted receiver %r killed the comment" % receiver.__name__)

# Save the comment and signal that it was saved
comment.save()
signals.comment_was_posted.send(
sender=comment.__class__,
comment=comment,
request=self.request
)
return True


class JobDetail(JobMixin, DetailView):
model = Job
Expand Down Expand Up @@ -241,6 +292,55 @@ def get_context_data(self, **kwargs):
return ctx


class JobPreview(LoginRequiredMixin, JobDetail, UpdateView):
template_name = 'jobs/job_detail.html'
form_class = JobForm

def get_success_url(self):
return reverse('jobs:job_thanks')

def post(self, request, *args, **kwargs):
"""
Handles POST requests, instantiating a form instance with the passed
POST variables and then checked for validity.
"""
self.object = self.get_object()
if self.request.POST.get('action') == 'review':
self.object.review()
return HttpResponseRedirect(self.get_success_url())
else:
return self.get(request)

def get_object(self, queryset=None):
""" Show only approved jobs to the public, staff can see all jobs """
# 404 if job doesn't exist
try:
job = Job.objects.select_related().get(pk=self.kwargs['pk'])
except Job.DoesNotExist:
raise Http404("No Job with PK#{} found.".format(self.kwargs['pk']))

# Only allow creator to preview and only while in draft status
if job.creator == self.request.user and job.editable:
return job

if self.request.user.is_staff:
return job

return None

def get_context_data(self, **kwargs):
ctx = super().get_context_data(
user_can_edit=(
self.object.creator == self.request.user
or self.request.user.is_staff
),
under_preview=True,
form=self.get_form(self.form_class),
)
ctx.update(kwargs)
return ctx


class JobDetailReview(LoginRequiredMixin, JobBoardAdminRequiredMixin, JobDetail):

def get_queryset(self):
Expand All @@ -267,7 +367,7 @@ class JobCreate(JobMixin, CreateView):
form_class = JobForm

def get_success_url(self):
return reverse('jobs:job_thanks')
return reverse('jobs:job_preview', kwargs={'pk': self.object.id})

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
Expand All @@ -276,6 +376,12 @@ def get_form_kwargs(self):
kwargs['initial'] = {'email': self.request.user.email}
return kwargs

def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx.update(kwargs)
ctx['needs_preview'] = not self.request.user.is_staff
return ctx

def form_valid(self, form):
""" set the creator to the current user """

Expand All @@ -285,7 +391,8 @@ def form_valid(self, form):

# Associate Job to user
form.instance.creator = self.request.user
return super().form_valid(form)
form.instance.status = 'draft'
return super().form_valid(form)


class JobEdit(JobMixin, UpdateView):
Expand All @@ -309,8 +416,19 @@ def get_context_data(self, **kwargs):
form_action='update',
)
ctx.update(kwargs)
ctx['next'] = self.request.GET.get('next') or self.request.POST.get('next')
ctx['needs_preview'] = not self.request.user.is_staff
return ctx

def get_success_url(self):
next_url = self.request.POST.get('next')
if next_url:
return next_url
elif self.object.pk:
return reverse('jobs:job_preview', kwargs={'pk': self.object.id})
else:
return super().get_success_url()


class JobChangeStatus(LoginRequiredMixin, JobMixin, View):
"""
Expand Down
8 changes: 5 additions & 3 deletions templates/comments/form.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% load comments i18n %}
<form action="{% comment_form_target %}" method="post">{% csrf_token %}
<form action="{% block comment_action %}{% comment_form_target %}{% endblock %}" method="post">{% csrf_token %}
{% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
{% for field in form %}
{% if field.is_hidden %}
Expand All @@ -20,7 +20,9 @@
{% endif %}
{% endfor %}
<p class="submit">
<input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" />
<input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}" />
{% block buttons %}
<input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" />
<input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}" />
{% endblock %}
</p>
</form>
30 changes: 30 additions & 0 deletions templates/comments/jobs/form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% extends "comments/form.html" %}
{% load comments i18n %}
{% block comment_action %}
{% comment %}Special comment target for reviewers{% endcomment %}
{% if request.user == object.creator %}{% comment_form_target %}{% else %}{% url "jobs:job_review" %}{% endif %}
{% endblock %}
{% block buttons %}
{% if request.user == object.creator %}
<input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" />
{% else %}
{% comment %}Special comment options for reviewer{% endcomment %}
<strong>Insert Canned response:</strong>
<select id="canned_response">
<option value="empty">---</option>
<option value="not_python">No or not enough emphasis on Python</option>
<option value="formatting">Formatting broken</option>
<option value="duplicate">Duplicate posting</option>
<option value="voluntary">Voluntary, non paid job</option>
</select>
<h3>Review action</h3>
<input type="hidden" name="job_id" value="{{ job.id }}">

<div>
<a class="review_button button" href="{% url 'jobs:job_edit' pk=object.pk %}?next={{request.get_full_path}}">Edit</a>
<button class="review_button" name="action" value="approve">Approve</button>
<button class="review_button" name="action" value="reject">Reject</button>
</div>
{% endif %}

{% endblock %}
2 changes: 2 additions & 0 deletions templates/jobs/email/job_was_rejected.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Unfortunately, your listing does not comply to our Job Board listing
guidelines (see https://www.python.org/community/jobs/howto/). If possible,
please update your listing and resubmit the request.

You can view and edit your listing at https://{{ site.domain }}{{ content_object.get_absolute_url }}.

If you have questions, please contact the Job Board Team at jobs@python.org.

Many thanks,
Expand Down

0 comments on commit 2370475

Please sign in to comment.