From adb172bf472ddeaf5b288152940199a45b0f9eb8 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 30 Apr 2010 19:11:03 +0100 Subject: [PATCH] HR milestone 2, application audit log and tracking. This milestone brings in application auditing, allowing for people to track who modify what applications and provide acception/rejection reasons as part of the messages sent out to the users. --- hr/admin.py | 10 ++- hr/app_defines.py | 37 +++++++++- hr/forms.py | 11 ++- hr/models.py | 36 +++++++++- hr/urls.py | 3 + hr/views.py | 95 ++++++++++++++++++------- templates/hr/applications/accept.html | 13 ++++ templates/hr/applications/add_note.html | 12 ++++ templates/hr/applications/reject.html | 13 ++++ templates/hr/applications/view.html | 15 ++++ templates/hr/emails/rejected.txt | 4 ++ templates/sso/profile.html | 2 +- 12 files changed, 220 insertions(+), 31 deletions(-) create mode 100644 templates/hr/applications/accept.html create mode 100644 templates/hr/applications/add_note.html create mode 100644 templates/hr/applications/reject.html diff --git a/hr/admin.py b/hr/admin.py index 18dad44..0832a2b 100644 --- a/hr/admin.py +++ b/hr/admin.py @@ -1,12 +1,15 @@ from django.contrib import admin from django.contrib.auth.models import User from django.contrib.auth.admin import UserAdmin -from hr.models import Application, Recommendation +from hr.models import Application, Recommendation, Audit class ApplicationAdmin(admin.ModelAdmin): list_display = ('user', 'character', 'status') search_fields = ['user', 'character', 'status'] + def save_model(self, request, obj, form, change): + obj.save(user=request.user) + admin.site.register(Application, ApplicationAdmin) class RecommendationAdmin(admin.ModelAdmin): @@ -15,3 +18,8 @@ class RecommendationAdmin(admin.ModelAdmin): admin.site.register(Recommendation, RecommendationAdmin) +class AuditAdmin(admin.ModelAdmin): + list_display = ('application', 'event', 'date') + +admin.site.register(Audit, AuditAdmin) + diff --git a/hr/app_defines.py b/hr/app_defines.py index f3c52b2..6fe4328 100644 --- a/hr/app_defines.py +++ b/hr/app_defines.py @@ -15,7 +15,42 @@ APPLICATION_STATUS_CHOICES = ( (APPLICATION_STATUS_COMPLETED, 'Completed'), ) +APPLICATION_STATUS_CHOICES_ADMIN = ( + (APPLICATION_STATUS_NOTSUBMITTED, 'Not Submitted'), + (APPLICATION_STATUS_AWAITINGREVIEW, 'Submitted'), + (APPLICATION_STATUS_QUERY, 'In Query'), + (APPLICATION_STATUS_COMPLETED, 'Completed'), +) + APPLICATION_STATUS_CHOICES_USER = ( (APPLICATION_STATUS_NOTSUBMITTED, 'Not Submitted'), - (APPLICATION_STATUS_AWAITINGREVIEW, 'Awaiting Review'), + (APPLICATION_STATUS_AWAITINGREVIEW, 'Submitted'), ) + +APPLICATION_STATUS_LOOKUP = { + APPLICATION_STATUS_NOTSUBMITTED: 'Not Submitted', + APPLICATION_STATUS_AWAITINGREVIEW: 'Submitted', + APPLICATION_STATUS_REJECTED: 'Rejected', + APPLICATION_STATUS_ACCEPTED: 'Accepted', + APPLICATION_STATUS_QUERY: 'In Query', + APPLICATION_STATUS_COMPLETED: 'Completed', +} + +AUDIT_EVENT_STATUSCHANGE = 0 +AUDIT_EVENT_NOTE = 1 +AUDIT_EVENT_REJECTION = 2 +AUDIT_EVENT_ACCEPTED = 3 + +AUDIT_EVENT_CHOICES = ( + (AUDIT_EVENT_STATUSCHANGE, 'Status Change'), + (AUDIT_EVENT_NOTE, 'Note'), + (AUDIT_EVENT_REJECTION, 'Rejection Reason'), + (AUDIT_EVENT_ACCEPTED, 'Accepted'), +) + +AUDIT_EVENT_LOOKUP = { + AUDIT_EVENT_STATUSCHANGE: 'Status Change', + AUDIT_EVENT_NOTE: 'Note', + AUDIT_EVENT_REJECTION: 'Rejection Reason', + AUDIT_EVENT_ACCEPTED: 'Accepted', +} diff --git a/hr/forms.py b/hr/forms.py index 3c7fc00..d067622 100644 --- a/hr/forms.py +++ b/hr/forms.py @@ -2,7 +2,7 @@ from django import forms import settings from hr.app_defines import * -from hr.models import Application +from hr.models import Application, Audit from eve_api.models import EVEPlayerCharacter, EVEPlayerCorporation def CreateRecommendationForm(user): @@ -40,7 +40,7 @@ def CreateApplicationForm(user): def CreateApplicationStatusForm(admin): if admin: - form_choices = APPLICATION_STATUS_CHOICES + form_choices = APPLICATION_STATUS_CHOICES_ADMIN else: form_choices = APPLICATION_STATUS_CHOICES_USER @@ -54,3 +54,10 @@ def CreateApplicationStatusForm(admin): exclude = ('application') return ApplicationStatusForm + + +class NoteForm(forms.ModelForm): + + class Meta: + model = Audit + exclude = ('application', 'user', 'event') diff --git a/hr/models.py b/hr/models.py index 66ae9a6..e92777b 100644 --- a/hr/models.py +++ b/hr/models.py @@ -19,9 +19,27 @@ class Application(models.Model): @property def status_description(self): for choice in APPLICATION_STATUS_CHOICES: - if choice[0] == self.status: + if choice[0] == int(self.status): return choice[1] + def save(self, *args, **kwargs): + try: + old_instance = Application.objects.get(id=self.id) + if not (old_instance.status == int(self.status)): + event = Audit(application=self) + if 'user' in kwargs: + event.user = kwargs['user'] + event.event = AUDIT_EVENT_STATUSCHANGE + + event.text = "Status changed from %s to %s" % (old_instance.status_description, self.status_description) + event.save() + except: + pass + + if 'user' in kwargs: + del kwargs['user'] + super(Application, self).save(*args, **kwargs) + def __unicode__(self): return self.character.name @@ -37,4 +55,18 @@ class Recommendation(models.Model): return self.user_character.name def __str__(self): - return self.__unicode__() + return self.__unicode__() + +class Audit(models.Model): + application = models.ForeignKey(Application, blank=False, verbose_name="Application") + user = models.ForeignKey(User, blank=True, verbose_name="User") + event = models.IntegerField(choices=AUDIT_EVENT_CHOICES, + verbose_name="Event Type", + help_text="Type of audit event") + text = models.TextField(blank=False, verbose_name="Event Text", + help_text="Detailed event text") + + date = models.DateTimeField(auto_now_add=True, verbose_name="Event Date") + + def event_description(self): + return AUDIT_EVENT_LOOKUP[self.event] diff --git a/hr/urls.py b/hr/urls.py index 35c6b21..7f9517b 100644 --- a/hr/urls.py +++ b/hr/urls.py @@ -9,6 +9,9 @@ urlpatterns = patterns('', (r'^applications/$', views.view_applications), (r'^applications/(?P\d+)/$', views.view_application), (r'^applications/(?P\d+)/update/$', views.update_application), + (r'^applications/(?P\d+)/note/$', views.add_note), + (r'^applications/(?P\d+)/reject/$', views.reject_application), + (r'^applications/(?P\d+)/accept/$', views.accept_application), (r'^add/application/$', views.add_application), (r'^add/recommendation/$', views.add_recommendation), diff --git a/hr/views.py b/hr/views.py index 583c639..f4f1897 100644 --- a/hr/views.py +++ b/hr/views.py @@ -13,11 +13,30 @@ import settings from eve_api.models import EVEAccount, EVEPlayerCorporation from reddit.models import RedditAccount -from hr.forms import CreateRecommendationForm, CreateApplicationForm, CreateApplicationStatusForm -from hr.models import Recommendation, Application +from hr.forms import CreateRecommendationForm, CreateApplicationForm, CreateApplicationStatusForm, NoteForm +from hr.models import Recommendation, Application, Audit from app_defines import * +### Shared Functions + +def send_message(application, message_type, note=None): + from django.core.mail import send_mail + subject = render_to_string('hr/emails/%s_subject.txt' % message_type, { 'app': application }) + subject = ''.join(subject.splitlines()) + message = render_to_string('hr/emails/%s.txt' % message_type, { 'app': application, 'note': note }) + try: + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [application.user.email]) + except: + pass + + if len(application.user.redditaccount_set.all()) > 0: + from reddit.api import Inbox + ib = Inbox(settings.REDDIT_USER, settings.REDDIT_PASSWD) + ib.send(application.user.redditaccount_set.all()[0].username, subject, message) + +### General Views + def index(request): if request.user.is_staff or Group.objects.get(name=settings.HR_STAFF_GROUP) in request.user.groups.all(): hrstaff = True @@ -43,16 +62,18 @@ def view_application(request, applicationid): if request.user.is_staff or Group.objects.get(name=settings.HR_STAFF_GROUP) in request.user.groups.all(): hrstaff = True + audit = app.audit_set.all() else: hrstaff = False + audit = app.audit_set.filter(event__in=[AUDIT_EVENT_STATUSCHANGE, AUDIT_EVENT_REJECTION_REASON]) if hrstaff or app.status < 1: appform = CreateApplicationStatusForm(hrstaff) form = appform(initial={'application': app.id, 'new_status': app.status}) - eveacc = EVEAccount.objects.filter(user=app.user) - redditacc = RedditAccount.objects.filter(user=app.user) - recs = Recommendation.objects.filter(application=app) + eveacc = app.user.eveaccount_set.all() + redditacc = app.user.redditaccount_set.all() + recs = app.recommendation_set.all() posts = [] for acc in redditacc: @@ -152,28 +173,54 @@ def update_application(request, applicationid): return HttpResponseRedirect(reverse('hr.views.index')) if not app.status == form.cleaned_data['new_status']: - + app.status = form.cleaned_data['new_status'] - app.save() - - def send_message(application, message_type): - from django.core.mail import send_mail - subject = render_to_string('hr/emails/%s_subject.txt' % message_type, { 'app': app }) - subject = ''.join(subject.splitlines()) - message = render_to_string('hr/emails/%s.txt' % message_type, { 'app': app }) - try: - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [application.user.email]) - except: - pass - - if len(application.user.redditaccount_set.all()) > 0: - from reddit.api import Inbox - ib = Inbox(settings.REDDIT_USER, settings.REDDIT_PASSWD) - ib.send(application.user.redditaccount_set.all()[0].username, subject, message) + app.save(user=request.user) if int(app.status) == APPLICATION_STATUS_ACCEPTED: send_message(app, 'accepted') - elif int(app.status) == APPLICATION_STATUS_REJECTED: - send_message(app, 'rejected') return HttpResponseRedirect(reverse('hr.views.view_application', args=[applicationid])) + +@login_required +def add_note(request, applicationid): + if request.method == 'POST': + obj = Audit(application=Application.objects.get(id=applicationid), user=request.user, event=AUDIT_EVENT_NOTE) + form = NoteForm(request.POST, instance=obj) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('hr.views.view_application', args=[applicationid])) + + form = NoteForm() + return render_to_response('hr/applications/add_note.html', locals(), context_instance=RequestContext(request)) + +@login_required +def reject_application(request, applicationid): + if request.method == 'POST': + obj = Audit(application=Application.objects.get(id=applicationid), user=request.user, event=AUDIT_EVENT_REJECTION) + form = NoteForm(request.POST, instance=obj) + if form.is_valid(): + obj = form.save() + obj.application.status = APPLICATION_STATUS_REJECTED + obj.application.save(user=request.user) + send_message(obj.application, 'rejected', note=obj.text) + return HttpResponseRedirect(reverse('hr.views.view_application', args=[applicationid])) + + form = NoteForm() + return render_to_response('hr/applications/reject.html', locals(), context_instance=RequestContext(request)) + +@login_required +def accept_application(request, applicationid): + if request.method == 'POST': + obj = Audit(application=Application.objects.get(id=applicationid), user=request.user, event=AUDIT_EVENT_ACCEPTED) + form = NoteForm(request.POST, instance=obj) + if form.is_valid(): + obj = form.save() + obj.application.status = APPLICATION_STATUS_ACCEPTED + obj.application.save(user=request.user) + send_message(obj.application, 'accepted', note=obj.text) + return HttpResponseRedirect(reverse('hr.views.view_application', args=[applicationid])) + + form = NoteForm() + return render_to_response('hr/applications/accept.html', locals(), context_instance=RequestContext(request)) + diff --git a/templates/hr/applications/accept.html b/templates/hr/applications/accept.html new file mode 100644 index 0000000..fcb1878 --- /dev/null +++ b/templates/hr/applications/accept.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Accept Application{% endblock %} + +{% block content %} +

Fill in a note you want to send to the user.

+
+ +{{ form.as_table }} +
+ +
+{% endblock %} diff --git a/templates/hr/applications/add_note.html b/templates/hr/applications/add_note.html new file mode 100644 index 0000000..7543b6b --- /dev/null +++ b/templates/hr/applications/add_note.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block title %}Add Note to Application{% endblock %} + +{% block content %} +
+ +{{ form.as_table }} +
+ +
+{% endblock %} diff --git a/templates/hr/applications/reject.html b/templates/hr/applications/reject.html new file mode 100644 index 0000000..08e2a55 --- /dev/null +++ b/templates/hr/applications/reject.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Reject Application{% endblock %} + +{% block content %} +

Fill in the rejection reason below, please note, this will be sent out to the user.

+
+ +{{ form.as_table }} +
+ +
+{% endblock %} diff --git a/templates/hr/applications/view.html b/templates/hr/applications/view.html index 1182000..b068df2 100644 --- a/templates/hr/applications/view.html +++ b/templates/hr/applications/view.html @@ -13,6 +13,7 @@
  • Application Status: {{ app.status_description }}
  • +

    Actions

    {% if form %}
    @@ -22,6 +23,20 @@ {% endif %} +{% if hrstaff %} +

    Add Note, Reject Application, Accept Application

    +{% endif %} + +{% if audit %} +

    Event Log

    +
    + +{% for a in audit %} + +{% endfor %} +
    Event TypeChanged ByChanged DateEvent Details
    {{ a.event_description }}{{ a.user }}{{ a.date }}{{ a.text }}
    +{% endif %} + {% if recs %}

    Recommendations

      diff --git a/templates/hr/emails/rejected.txt b/templates/hr/emails/rejected.txt index eab12f4..46c225f 100644 --- a/templates/hr/emails/rejected.txt +++ b/templates/hr/emails/rejected.txt @@ -2,7 +2,11 @@ Hi {{ app.character }}, Your application to {{ app.corporation }} has been rejected. +{% if note %} +{{ note }} +{% else %} One of our Personnel people will contact you in a seperate method to explain why you have been rejected. +{% endif %} If you have any further questions regarding your application, please contact {{ app.corporation }} via the normal channels. diff --git a/templates/sso/profile.html b/templates/sso/profile.html index 15d96d5..a454bdd 100644 --- a/templates/sso/profile.html +++ b/templates/sso/profile.html @@ -92,7 +92,7 @@ setup.

      {% for acc in redditaccounts %} {{ acc.username }} {{ acc.date_created }} - {% if acc.validated %}Yes{% else %}No (Validate){% endif %} + {% if acc.validated %}Yes{% else %}No (Validate){% endif %} {% endfor %}