~fincham/change-controller

f0ee23e7da27c4263741fd370584b413f3bfb203 — Michael Fincham 8 years ago 8db6df1 master
Get it off my PC
30 files changed, 882 insertions(+), 0 deletions(-)

A change_controller/change_controller/__init__.py
A change_controller/change_controller/settings.py
A change_controller/change_controller/urls.py
A change_controller/change_controller/wsgi.py
A change_controller/change_requests/__init__.py
A change_controller/change_requests/admin.py
A change_controller/change_requests/context_processors.py
A change_controller/change_requests/forms.py
A change_controller/change_requests/migrations/0001_initial.py
A change_controller/change_requests/migrations/0002_revision_created.py
A change_controller/change_requests/migrations/0003_auto_20150603_1454.py
A change_controller/change_requests/migrations/0004_comment_text.py
A change_controller/change_requests/migrations/0005_auto_20150603_1548.py
A change_controller/change_requests/migrations/0006_auto_20150603_1633.py
A change_controller/change_requests/migrations/__init__.py
A change_controller/change_requests/models.py
A change_controller/change_requests/templates/change_requests/base.html
A change_controller/change_requests/templates/change_requests/bootstrap.html
A change_controller/change_requests/templates/change_requests/create.html
A change_controller/change_requests/templates/change_requests/request_detail.html
A change_controller/change_requests/templates/change_requests/request_form.html
A change_controller/change_requests/templates/change_requests/request_list.html
A change_controller/change_requests/templates/change_requests/request_update_form.html
A change_controller/change_requests/templates/change_requests/revision_form.html
A change_controller/change_requests/templates/registration/login.html
A change_controller/change_requests/tests.py
A change_controller/change_requests/views.py
A change_controller/db.sqlite3
A change_controller/manage.py
A requirements.txt
A change_controller/change_controller/__init__.py => change_controller/change_controller/__init__.py +0 -0
A change_controller/change_controller/settings.py => change_controller/change_controller/settings.py +108 -0
@@ 0,0 1,108 @@
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import sys

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Include BOOTSTRAP3_FOLDER in path
BOOTSTRAP3_FOLDER = os.path.abspath(os.path.join(BASE_DIR, '..', 'bootstrap3'))
if BOOTSTRAP3_FOLDER not in sys.path:
    sys.path.insert(0, BOOTSTRAP3_FOLDER)


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '3eci^12sf*+08*1-6p)iy*rcpy3beege1u7b=gu(!&2&1a-wxy'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'extra_views',
    'bootstrap3',
    'change_requests',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.security.SecurityMiddleware',
)

ROOT_URLCONF = 'change_controller.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'change_requests.context_processors.templates', 
            ],
        },
    },
]

WSGI_APPLICATION = 'change_controller.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/

LANGUAGE_CODE = 'en-nz'

TIME_ZONE = 'Pacific/Auckland'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/

STATIC_URL = '/static/'

# Settings for django-bootstrap3
BOOTSTRAP3 = {
'set_required': False,
'error_css_class': 'bootstrap3-error',
'required_css_class': 'bootstrap3-required',
'javascript_in_head': True,
}

A change_controller/change_controller/urls.py => change_controller/change_controller/urls.py +17 -0
@@ 0,0 1,17 @@
from __future__ import unicode_literals

from django.contrib.auth.decorators import login_required
from django.conf.urls import patterns, url, include
from change_requests.views import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^$', RequestList.as_view(), name='list'),
    url(r'^(?P<pk>\d+)/$', RequestDetail.as_view(), name='detail'),
    url(r'^(?P<pk>\d+)/(?P<version>\d+)$', RequestDetail.as_view(), name='detail-version'),
    #url(r'^create/(?P<template>\d+)$', RequestCreate.as_view(), name='create'),
    url(r'^accounts/', include('django.contrib.auth.urls')),
    url(r'^admin/', include(admin.site.urls)),
)

A change_controller/change_controller/wsgi.py => change_controller/change_controller/wsgi.py +5 -0
@@ 0,0 1,5 @@
import os

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "change_controller.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

A change_controller/change_requests/__init__.py => change_controller/change_requests/__init__.py +0 -0
A change_controller/change_requests/admin.py => change_controller/change_requests/admin.py +10 -0
@@ 0,0 1,10 @@
from django.contrib import admin

from .models import *

admin.site.register(Template)
admin.site.register(Question)
admin.site.register(Request)
admin.site.register(Revision)
admin.site.register(Answer)
admin.site.register(Comment)

A change_controller/change_requests/context_processors.py => change_controller/change_requests/context_processors.py +6 -0
@@ 0,0 1,6 @@
from models import *

def templates(request):
    return {
        'templates': Template.objects.all()
    }
\ No newline at end of file

A change_controller/change_requests/forms.py => change_controller/change_requests/forms.py +63 -0
@@ 0,0 1,63 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django import forms
from django.forms.formsets import BaseFormSet, formset_factory


from bootstrap3.tests import TestForm

RADIO_CHOICES = (
    ('1', 'Radio 1'),
    ('2', 'Radio 2'),
)

MEDIA_CHOICES = (
    ('Audio', (
        ('vinyl', 'Vinyl'),
        ('cd', 'CD'),
    )
    ),
    ('Video', (
        ('vhs', 'VHS Tape'),
        ('dvd', 'DVD'),
    )
    ),
    ('unknown', 'Unknown'),
)


class ContactForm(TestForm):
    pass


class ContactBaseFormSet(BaseFormSet):
    def add_fields(self, form, index):
        super(ContactBaseFormSet, self).add_fields(form, index)

    def clean(self):
        super(ContactBaseFormSet, self).clean()
        raise forms.ValidationError("This error was added to show the non form errors styling")

ContactFormSet = formset_factory(TestForm, formset=ContactBaseFormSet,
                                 extra=2,
                                 max_num=4,
                                 validate_max=True)


class FilesForm(forms.Form):
    text1 = forms.CharField()
    file1 = forms.FileField()
    file2 = forms.FileField(required=False)
    file3 = forms.FileField(widget=forms.ClearableFileInput)
    file4 = forms.FileField(required=False, widget=forms.ClearableFileInput)


class ArticleForm(forms.Form):
    title = forms.CharField()
    pub_date = forms.DateField()

    def clean(self):
        cleaned_data = super(ArticleForm, self).clean()
        raise forms.ValidationError("This error was added to show the non field errors styling.")
        return cleaned_data

A change_controller/change_requests/migrations/0001_initial.py => change_controller/change_requests/migrations/0001_initial.py +71 -0
@@ 0,0 1,71 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Answer',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('text', models.TextField()),
            ],
        ),
        migrations.CreateModel(
            name='Question',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('name', models.CharField(max_length=200)),
            ],
        ),
        migrations.CreateModel(
            name='Request',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('name', models.CharField(max_length=200)),
                ('created', models.DateTimeField(auto_now_add=True)),
                ('modified', models.DateTimeField(auto_now=True)),
            ],
        ),
        migrations.CreateModel(
            name='Revision',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('number', models.IntegerField()),
                ('request', models.ForeignKey(to='change_requests.Request')),
            ],
        ),
        migrations.CreateModel(
            name='Template',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('name', models.CharField(max_length=200)),
            ],
        ),
        migrations.AddField(
            model_name='request',
            name='template',
            field=models.ForeignKey(to='change_requests.Template'),
        ),
        migrations.AddField(
            model_name='question',
            name='template',
            field=models.ForeignKey(to='change_requests.Template'),
        ),
        migrations.AddField(
            model_name='answer',
            name='question',
            field=models.ForeignKey(to='change_requests.Question'),
        ),
        migrations.AddField(
            model_name='answer',
            name='revision',
            field=models.ForeignKey(to='change_requests.Revision'),
        ),
    ]

A change_controller/change_requests/migrations/0002_revision_created.py => change_controller/change_requests/migrations/0002_revision_created.py +22 -0
@@ 0,0 1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import datetime
from django.utils.timezone import utc


class Migration(migrations.Migration):

    dependencies = [
        ('change_requests', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='revision',
            name='created',
            field=models.DateTimeField(default=datetime.datetime(2015, 6, 3, 2, 50, 44, 138950, tzinfo=utc), auto_now_add=True),
            preserve_default=False,
        ),
    ]

A change_controller/change_requests/migrations/0003_auto_20150603_1454.py => change_controller/change_requests/migrations/0003_auto_20150603_1454.py +54 -0
@@ 0,0 1,54 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
from django.conf import settings

from django.utils import timezone

class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('change_requests', '0002_revision_created'),
    ]

    operations = [
        migrations.CreateModel(
            name='Comment',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('created', models.DateTimeField(auto_now_add=True)),
                ('approves', models.BooleanField()),
                ('rejects', models.BooleanField()),
            ],
        ),
        migrations.AddField(
            model_name='request',
            name='user',
            field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='revision',
            name='implemented',
            field=models.DateTimeField(default=timezone.now()),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='revision',
            name='user',
            field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='comment',
            name='revision',
            field=models.ForeignKey(to='change_requests.Revision'),
        ),
        migrations.AddField(
            model_name='comment',
            name='user',
            field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
        ),
    ]

A change_controller/change_requests/migrations/0004_comment_text.py => change_controller/change_requests/migrations/0004_comment_text.py +20 -0
@@ 0,0 1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

    dependencies = [
        ('change_requests', '0003_auto_20150603_1454'),
    ]

    operations = [
        migrations.AddField(
            model_name='comment',
            name='text',
            field=models.TextField(default='No comment'),
            preserve_default=False,
        ),
    ]

A change_controller/change_requests/migrations/0005_auto_20150603_1548.py => change_controller/change_requests/migrations/0005_auto_20150603_1548.py +24 -0
@@ 0,0 1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

    dependencies = [
        ('change_requests', '0004_comment_text'),
    ]

    operations = [
        migrations.AlterField(
            model_name='comment',
            name='approves',
            field=models.NullBooleanField(),
        ),
        migrations.AlterField(
            model_name='comment',
            name='rejects',
            field=models.NullBooleanField(),
        ),
    ]

A change_controller/change_requests/migrations/0006_auto_20150603_1633.py => change_controller/change_requests/migrations/0006_auto_20150603_1633.py +26 -0
@@ 0,0 1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

    dependencies = [
        ('change_requests', '0005_auto_20150603_1548'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='comment',
            options={'ordering': ['-created']},
        ),
        migrations.AlterModelOptions(
            name='revision',
            options={'ordering': ['-created']},
        ),
        migrations.RemoveField(
            model_name='revision',
            name='number',
        ),
    ]

A change_controller/change_requests/migrations/__init__.py => change_controller/change_requests/migrations/__init__.py +0 -0
A change_controller/change_requests/models.py => change_controller/change_requests/models.py +108 -0
@@ 0,0 1,108 @@
from django.db import models
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.utils import timezone

class Template(models.Model):
    name = models.CharField(max_length=200)

    def __unicode__(self):
        return self.name

class Question(models.Model):
    template = models.ForeignKey(Template)
    name = models.CharField(max_length=200)

    def __unicode__(self):
        return self.name

class Request(models.Model):
    name = models.CharField(max_length=200)
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    template = models.ForeignKey(Template)
    user = models.ForeignKey(User)

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('detail', kwargs={'pk': self.id})

    def status(self):
        if not self.latest_revision():
            return {'text': "Pending approval", 'sort': 0}

        if self.latest_revision().rejected():
            return {'text': "Rejected", 'sort': 100}

        if any([revision.implemented < timezone.now() for revision in self.revision_set.all()]):
            return {'text': "Implemented", 'sort': 90}

        if self.latest_revision().approved():
            return {'text': "Pending implementation", 'sort': 10}
        else:
            return {'text': "Pending approval", 'sort': 0}

    def latest_revision(self):
        return self.revision_set.first()        

class Revision(models.Model):
    created = models.DateTimeField(auto_now_add=True)   
    request = models.ForeignKey(Request)
    implemented = models.DateTimeField()
    user = models.ForeignKey(User)

    class Meta:
        ordering = ['-created']

    def __unicode__(self):
        return "Revision %s of %s" % (str(self.created), self.request.name)

    def approved(self):
        for comment in self.comment_set.all():
            if comment.approves:
                return comment
            elif comment.rejects:
                return False

        return False

    def rejected(self):
        for comment in self.comment_set.all():
            if comment.approves:
                return False
            elif comment.rejects:
                return comment

        return False

    def reason(self):
        if self.rejected():
            return self.rejected()
        if self.approved():
            return self.approved()

        return ""

class Comment(models.Model):
    created = models.DateTimeField(auto_now_add=True)   
    revision = models.ForeignKey(Revision)
    user = models.ForeignKey(User)
    approves = models.NullBooleanField(null=True)
    rejects = models.NullBooleanField(null=True)
    text = models.TextField()

    class Meta:
        ordering = ['-created']

    def __unicode__(self):
        return "%s: %s" % (self.user, self.text)

class Answer(models.Model):
    revision = models.ForeignKey(Revision)
    question = models.ForeignKey(Question)
    text = models.TextField()

    def __unicode__(self):
        return self.question.name
\ No newline at end of file

A change_controller/change_requests/templates/change_requests/base.html => change_controller/change_requests/templates/change_requests/base.html +39 -0
@@ 0,0 1,39 @@
{% extends 'change_requests/bootstrap.html' %}

{% load bootstrap3 %}

{% block bootstrap3_content %}
    <nav class="navbar navbar-inverse navbar-fixed-top">
      <div class="container">
        <div id="navbar" class="collapse navbar-collapse">
          <a class="navbar-brand" href="/">Change controls</a>
          <ul class="nav navbar-nav">
            <li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Filter requests<span class="caret"></span></a>
                <ul class="dropdown-menu" role="menu">
                    {% for template in templates %} 
                    <li><a href="#">{{ template.name }}</a></li>
                    {% endfor %}
                </ul>
            </li>

            <li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">New request <span class="caret"></span></a>
                <ul class="dropdown-menu" role="menu">
                    {% for template in templates %} 
                    <li><a href="/create/{{ template.id }}">{{ template.name }}</a></li>
                    {% endfor %}
                </ul>
            </li>
        </ul>
    </div>
    </div>
    </nav>

    <div class="container" style="margin-top: 50px;">
        {% bootstrap_messages %}
        <br>
        {% block content %}(no content){% endblock %}
    </div>

{% endblock %}

A change_controller/change_requests/templates/change_requests/bootstrap.html => change_controller/change_requests/templates/change_requests/bootstrap.html +3 -0
@@ 0,0 1,3 @@
{% extends 'bootstrap3/bootstrap3.html' %}

{% block bootstrap3_title %}{% block title %}{% endblock %}{% endblock %}

A change_controller/change_requests/templates/change_requests/create.html => change_controller/change_requests/templates/change_requests/create.html +6 -0
@@ 0,0 1,6 @@
{% extends 'change_requests/base.html' %}

{% block title %}create{% endblock %}

{% block content %}
{% endblock %}

A change_controller/change_requests/templates/change_requests/request_detail.html => change_controller/change_requests/templates/change_requests/request_detail.html +112 -0
@@ 0,0 1,112 @@
{% extends 'change_requests/base.html' %}
{% load bootstrap3 %}

{% block title %}Request detail{% endblock %}

{% block content %}
    <div class="row">

        <div class="col-md-8">

            <dl class="dl-horizontal">
                <dt>&nbsp;</dt><dd>
                    <h1>{{ object.name }}</h1><br>
                    {% if revision.get_previous_by_created %}
                        <a href="{{ revision.get_previous_by_created.id }}">← Older revision</a>
                    {% elif revision.get_next_by_created %}
                        Oldest revision
                    {% endif %}

                    {% if revision.get_next_by_created %}
                        <a href="{{ revision.get_next_by_created.id }}">Newer revision →</a>
                    {% elif revision.get_previous_by_created %}
                        Newest revision
                    {% endif %}                    
                </dd>
            </dl>
            
            <dl class="dl-horizontal">   
                <dt>Originally requested</dt><dd>{{ object.created }} by {{ object.user }}</dd>
                <dt>Status</dt><dd>{{ object.status.text }}</dd>

            </dl>

            <dl class="dl-horizontal">
                <dt>&nbsp;</dt><dd><strong>Revision {{ revision.created }} by {{ revision.user }}</strong></dd>
            </dl>

            <dl class="dl-horizontal">
                    <dt>Implementation date</dt><dd>{{ revision.implemented }}</dd>
            </dl>

            <dl class="dl-horizontal">   
                {% for answer in revision.answer_set.all %}
                    <dt>{{ answer.question.name }}</dt><dd>{{ answer.text }}</dd>
                {% endfor %}
            </dl>   

        </div>
        <div class="col-md-4" style="padding-top: 90px;">
            {% if not revision.get_next_by_created %}

                <form class="form-inline" method="POST" >
                    {% csrf_token %}
                    <div class="form-group">
                        <dl class="dl-horizontal">
                            <dt style="width: 80px;">Comment</dt>
                            <dd style="margin-left: 100px; padding-bottom: 10px;"><textarea style="width: 260px;" id="text" name="text" class="form-control" rows="6" ></textarea></dd>

                            <dt style="width: 80px;">Resolve</dt>
                            <dd style="margin-left: 100px;">
                                <button type="submit" style="margin-top: 5px;" class="pull-right btn btn-default">Add comment</button>
                                <div class="radio">
                                <label>
                                <input name="resolve" id="nothing" value="nothing"  checked=""  type="radio">
                                Comment only
                                </label>
                                </div>
                                <br>

                                <div class="radio" style="padding-right: 5px;">
                                <label>
                                <input name="resolve" id="approve" value="approve" type="radio">
                                Approve
                                </label>
                                </div>
                                <div class="radio">
                                <label>
                                <input name="resolve" id="reject" value="reject" type="radio">
                                Reject
                                </label>
                                </div>
                            </dd>
                        </dl>
                    </div>

                </form>
            {% endif %}
            {% for comment in revision.comment_set.all %}
            {% if comment.approves %}
                <div class="alert alert-success">
            {% elif comment.rejects %}
                <div class="alert alert-danger">
            {% else %}
                <div class="alert alert-info">
            {% endif %}

                    <strong>{{ comment.user }}</strong>
                    {% if comment.approves %}
                        <em>Approved</em>
                    {% elif comment.rejects %}
                        <em>Rejected</em>
                    {% endif %}                    
                    <span class="pull-right">
                        {{ comment.created }}
                    </span>
                    <br class="clearfix"><br>
                    {{ comment.text }}
                </div>
            {% endfor %}            
        </div>
    </div>         
{% endblock %}

A change_controller/change_requests/templates/change_requests/request_form.html => change_controller/change_requests/templates/change_requests/request_form.html +15 -0
@@ 0,0 1,15 @@
{% extends 'change_requests/base.html' %}
{% load bootstrap3 %}

{% block content %}
    <form role="form" class="form-horizontal" method="post">
    {{ formset.management_form }}
    {% for form in formset %}

           {% bootstrap_form form layout="horizontal" %}
    {% endfor %}

        {% buttons submit='Save' layout='horizontal' %}{% endbuttons %}
    </form>

{% endblock %}

A change_controller/change_requests/templates/change_requests/request_list.html => change_controller/change_requests/templates/change_requests/request_list.html +34 -0
@@ 0,0 1,34 @@
{% extends 'change_requests/base.html' %}

{% block title %}List change requests{% endblock %}

{% block content %}
    {% regroup object_list|dictsort:"status.sort" by status as status_list %}

    <table class="table">
    {% for status in status_list %}
                <thead>

            <tr><th style="border: 0" colspan="*"><h1>{{ status.grouper.text }}</h1></th></tr>
                            <tr>
                    <th>Name</th><th>System</th><th>Requested by</th><th>Reason</th><th>Implementation date</th>
                </tr>
            </thead>

            <tbody>

            {% for request in status.list %}
                <tr>
                    <td><a href="{{ request.get_absolute_url }}">{{ request.name }}</a></td>               
                    <td>{{ request.template }}</td>
                    <td>{{ request.user }}</td>
                    <td>{{ request.latest_revision.reason }}</td>
                    <td>{{ request.latest_revision.implemented }}</td>
                </tr>
            {% endfor %}
            </tbody>

    {% endfor %}
        </table>

{% endblock %}

A change_controller/change_requests/templates/change_requests/request_update_form.html => change_controller/change_requests/templates/change_requests/request_update_form.html +12 -0
@@ 0,0 1,12 @@
{% extends 'change_requests/base.html' %}
{% load bootstrap3 %}

{% block title %}Request{% endblock %}

{% block content %}
    <form role="form" class="form-horizontal" method="post">
        {% csrf_token %}
        {% bootstrap_form form layout="horizontal" %}
        {% buttons submit='Save' layout='horizontal' %}{% endbuttons %}
    </form>
{% endblock %}

A change_controller/change_requests/templates/change_requests/revision_form.html => change_controller/change_requests/templates/change_requests/revision_form.html +13 -0
@@ 0,0 1,13 @@
{% extends 'change_requests/base.html' %}
{% load bootstrap3 %}

{% block content %}
    <form role="form" class="form-horizontal" method="post">


           {% bootstrap_form form layout="horizontal" %}

        {% buttons submit='Save' layout='horizontal' %}{% endbuttons %}
    </form>

{% endblock %}

A change_controller/change_requests/templates/registration/login.html => change_controller/change_requests/templates/registration/login.html +31 -0
@@ 0,0 1,31 @@
{% extends 'change_requests/base.html' %}
{% load bootstrap3 %}

{% block title %}Login{% endblock %}

{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

{% if next %}
    {% if user.is_authenticated %}
    <p>Your account doesn't have access to this page. To proceed,
    please login with an account that has access.</p>
    {% else %}
    <p>Please login to see this page.</p>
    {% endif %}
{% endif %}

<form role="form" class="form-horizontal" method="post" action="{% url 'django.contrib.auth.views.login' %}">
    {% csrf_token %}
    <input type="hidden" name="next" value="{{ next }}" />
    {% bootstrap_form form layout="horizontal" %}
    {% buttons submit='Login' layout='horizontal' %}{% endbuttons %}
</form>

{# Assumes you setup the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}
\ No newline at end of file

A change_controller/change_requests/tests.py => change_controller/change_requests/tests.py +3 -0
@@ 0,0 1,3 @@
from django.test import TestCase

# Create your tests here.

A change_controller/change_requests/views.py => change_controller/change_requests/views.py +68 -0
@@ 0,0 1,68 @@
from __future__ import unicode_literals
from django.core.files.storage import default_storage

from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.views.generic import ListView, DetailView
from django.views.generic.edit import UpdateView, CreateView
from django.views.generic.base import TemplateView
from django.shortcuts import get_object_or_404
from django.shortcuts import render

from django import forms
from django.forms.models import inlineformset_factory

from django.contrib import messages

from .models import *

from extra_views import CreateWithInlinesView, UpdateWithInlinesView, InlineFormSet
from extra_views.generic import GenericInlineFormSet


class CommentForm(forms.Form):
    RESOLUTIONS = (
        ('approve', 'Approve'),
        ('reject', 'Reject'),
        ('nothing', 'Comment only'),        
    )
    text = forms.CharField()
    resolve = forms.ChoiceField(choices=RESOLUTIONS)

class AnswerInline(InlineFormSet):
    model = Answer
    fields = ["text"]

class RequestList(ListView):
    model = Request

class RevisionCreate(CreateWithInlinesView):
    model = Revision
    fields = ['implemented']
    inlines = [AnswerInline]

class RequestDetail(DetailView):
    model = Request

    def get_context_data(self, **kwargs):
        context = super(RequestDetail, self).get_context_data(**kwargs)
        try:
            context['revision'] = Revision.objects.get(id=self.kwargs.get('version', context['object'].revision_set.first().id))
        except:
            pass
        return context    

    def post(self, request, *args, **kwargs):
        form = CommentForm(request.POST)
        if form.is_valid():
            if form.cleaned_data['resolve'] == 'approve':
                resolution_dict = {'approves': True}
            elif form.cleaned_data['resolve'] == 'reject':
                resolution_dict = {'rejects': True}
            else:
                resolution_dict = {}

            comment = Comment(user=request.user, text=form.cleaned_data['text'], revision=Revision.objects.get(id=self.kwargs.get('version', self.get_object().latest_revision().id)), **resolution_dict)
            comment.save()
        else:
            messages.add_message(request, messages.INFO, 'Bad form.')
        return self.get(request, *args, **kwargs)

A change_controller/db.sqlite3 => change_controller/db.sqlite3 +0 -0
A change_controller/manage.py => change_controller/manage.py +10 -0
@@ 0,0 1,10 @@
#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "change_controller.settings")

    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)

A requirements.txt => requirements.txt +2 -0
@@ 0,0 1,2 @@
django
django-bootstrap3