diff --git a/.gitignore b/.gitignore
index a2274b00d145b376cee7323396849040f0118584..2673ca550fb5d82633cc80cf1c145f3c9b1d157e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 __pycache__
 
 *.json
+!**/fixtures/*.json
 
 *~
 
@@ -14,4 +15,4 @@ SCIPOST_JOURNALS
 UPLOADS
 
 docs/_build
-local_files
\ No newline at end of file
+local_files
diff --git a/commentaries/factories.py b/commentaries/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0bae1a0912e9a18ceb0c6f079918c595534948b
--- /dev/null
+++ b/commentaries/factories.py
@@ -0,0 +1,33 @@
+import factory
+
+from .models import Commentary, COMMENTARY_TYPES
+
+from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS
+from scipost.factories import ContributorFactory
+from journals.models import SCIPOST_JOURNALS_DOMAINS
+
+
+class CommentaryFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = Commentary
+        abstract = True
+
+    requested_by = factory.SubFactory(ContributorFactory)
+    vetted_by = factory.SubFactory(ContributorFactory)
+    type = COMMENTARY_TYPES[0][0]
+    discipline = SCIPOST_DISCIPLINES[0][0]
+    domain = SCIPOST_JOURNALS_DOMAINS[0][0]
+    subject_area = SCIPOST_SUBJECT_AREAS[0][1][0][0]
+    pub_title = factory.Sequence(lambda n: "Commentary %d" % n)
+    pub_DOI = '10.1103/PhysRevB.92.214427'
+    arxiv_identifier = '1610.06911v1'
+    author_list = factory.Faker('name')
+    pub_abstract = factory.Faker('text')
+
+
+class EmptyCommentaryFactory(CommentaryFactory):
+    pub_DOI = None
+    arxiv_identifier = None
+
+class VettedCommentaryFactory(CommentaryFactory):
+    vetted = True
diff --git a/commentaries/forms.py b/commentaries/forms.py
index 14cd5852633ca67586f10bb5cf3e39f7fcefd1ea..f0dcdf3a30c8497a8d19650abc9fda2c291bda45 100644
--- a/commentaries/forms.py
+++ b/commentaries/forms.py
@@ -1,7 +1,9 @@
 from django import forms
+from django.shortcuts import get_object_or_404
 
 from .models import Commentary
 
+from scipost.models import Contributor
 
 COMMENTARY_ACTION_CHOICES = (
     (0, 'modify'),
@@ -17,10 +19,12 @@ COMMENTARY_REFUSAL_CHOICES = (
     )
 commentary_refusal_dict = dict(COMMENTARY_REFUSAL_CHOICES)
 
+
 class DOIToQueryForm(forms.Form):
     doi = forms.CharField(widget=forms.TextInput(
         {'label': 'DOI', 'placeholder': 'ex.: 10.21468/00.000.000000'}))
 
+
 class IdentifierToQueryForm(forms.Form):
     identifier = forms.CharField(widget=forms.TextInput(
         {'label': 'arXiv identifier',
@@ -28,6 +32,8 @@ class IdentifierToQueryForm(forms.Form):
 
 
 class RequestCommentaryForm(forms.ModelForm):
+    existing_commentary = None
+
     class Meta:
         model = Commentary
         fields = ['type', 'discipline', 'domain', 'subject_area',
@@ -38,6 +44,7 @@ class RequestCommentaryForm(forms.ModelForm):
                   'pub_DOI', 'pub_abstract']
 
     def __init__(self, *args, **kwargs):
+        self.user = kwargs.pop('user', None)
         super(RequestCommentaryForm, self).__init__(*args, **kwargs)
         self.fields['metadata'].widget = forms.HiddenInput()
         self.fields['pub_date'].widget.attrs.update({'placeholder': 'Format: YYYY-MM-DD'})
@@ -46,6 +53,44 @@ class RequestCommentaryForm(forms.ModelForm):
         self.fields['pub_DOI'].widget.attrs.update({'placeholder': 'ex.: 10.21468/00.000.000000'})
         self.fields['pub_abstract'].widget.attrs.update({'cols': 100})
 
+    def clean(self, *args, **kwargs):
+        cleaned_data = super(RequestCommentaryForm, self).clean(*args, **kwargs)
+
+        # Either Arxiv-ID or DOI is given
+        if not cleaned_data['arxiv_identifier'] and not cleaned_data['pub_DOI']:
+            msg = ('You must provide either a DOI (for a published paper) '
+                'or an arXiv identifier (for a preprint).')
+            self.add_error('arxiv_identifier', msg)
+            self.add_error('pub_DOI', msg)
+        elif (cleaned_data['arxiv_identifier'] and
+              (Commentary.objects
+               .filter(arxiv_identifier=cleaned_data['arxiv_identifier']).exists())):
+            msg = 'There already exists a Commentary Page on this preprint, see'
+            self.existing_commentary = get_object_or_404(
+                Commentary,
+                arxiv_identifier=cleaned_data['arxiv_identifier'])
+            self.add_error('arxiv_identifier', msg)
+        elif (cleaned_data['pub_DOI'] and
+              Commentary.objects.filter(pub_DOI=cleaned_data['pub_DOI']).exists()):
+            msg = 'There already exists a Commentary Page on this publication, see'
+            self.existing_commentary = get_object_or_404(Commentary, pub_DOI=cleaned_data['pub_DOI'])
+            self.add_error('pub_DOI', msg)
+
+        # Current user is not known
+        if not self.user or not Contributor.objects.filter(user=self.user).exists():
+            self.add_error(None, 'Sorry, current user is not known to SciPost.')
+
+
+    def save(self, *args, **kwargs):
+        """Prefill instance before save"""
+        self.requested_by = Contributor.objects.get(user=self.user)
+        return super(RequestCommentaryForm, self).save(*args, **kwargs)
+
+    def get_existing_commentary(self):
+        """Get Commentary if found after validation"""
+        return self.existing_commentary
+
+
 class VetCommentaryForm(forms.Form):
     action_option = forms.ChoiceField(widget=forms.RadioSelect,
                                       choices=COMMENTARY_ACTION_CHOICES,
@@ -54,7 +99,16 @@ class VetCommentaryForm(forms.Form):
     email_response_field = forms.CharField(widget=forms.Textarea(
         attrs={'rows': 5, 'cols': 40}), label='Justification (optional)', required=False)
 
+
 class CommentarySearchForm(forms.Form):
+    """Search for Commentary specified by user"""
     pub_author = forms.CharField(max_length=100, required=False, label="Author(s)")
-    pub_title_keyword = forms.CharField(max_length=100, label="Title", required=False)
+    pub_title_keyword = forms.CharField(max_length=100, required=False, label="Title")
     pub_abstract_keyword = forms.CharField(max_length=1000, required=False, label="Abstract")
+
+    def search_results(self):
+        """Return all Commentary objects according to search"""
+        return Commentary.objects.vetted(
+            pub_title__icontains=self.cleaned_data['pub_title_keyword'],
+            pub_abstract__icontains=self.cleaned_data['pub_abstract_keyword'],
+            author_list__icontains=self.cleaned_data['pub_author']).order_by('-pub_date')
diff --git a/commentaries/migrations/0013_auto_20161213_2328.py b/commentaries/migrations/0013_auto_20161213_2328.py
new file mode 100644
index 0000000000000000000000000000000000000000..ec27da0367e4f87026cfc64b8131232f62bfb3cc
--- /dev/null
+++ b/commentaries/migrations/0013_auto_20161213_2328.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2016-12-13 22:28
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('commentaries', '0012_remove_commentary_specialization'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='commentary',
+            name='created',
+            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='commentary',
+            name='latest_activity',
+            field=models.DateTimeField(auto_now=True),
+        ),
+    ]
diff --git a/commentaries/models.py b/commentaries/models.py
index c216ce3599e806f4ec35c2925dcb8d49c7b3caf2..a42005b4dae74ac4621960f27e9bfc11dcd4bbb0 100644
--- a/commentaries/models.py
+++ b/commentaries/models.py
@@ -1,70 +1,86 @@
 from django.utils import timezone
 from django.db import models
-from django.contrib.auth.models import User
 from django.contrib.postgres.fields import JSONField
 from django.template import Template, Context
 
 from journals.models import SCIPOST_JOURNALS_DOMAINS, SCIPOST_JOURNALS_SPECIALIZATIONS
-from scipost.models import Contributor
-from scipost.models import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS
-
-
+from scipost.models import TimeStampedModel, Contributor
+from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS
 
 COMMENTARY_TYPES = (
     ('published', 'published paper'),
     ('preprint', 'arXiv preprint'),
     )
 
-class Commentary(models.Model):
+
+class CommentaryManager(models.Manager):
+    def vetted(self, **kwargs):
+        return self.filter(vetted=True, **kwargs)
+
+    def awaiting_vetting(self, **kwargs):
+        return self.filter(vetted=False, **kwargs)
+
+
+class Commentary(TimeStampedModel):
     """
     A Commentary contains all the contents of a SciPost Commentary page for a given publication.
     """
-    requested_by = models.ForeignKey (Contributor, blank=True, null=True,
-                                      on_delete=models.CASCADE, related_name='requested_by')
+    requested_by = models.ForeignKey(
+        Contributor, blank=True, null=True,
+        on_delete=models.CASCADE, related_name='requested_by')
     vetted = models.BooleanField(default=False)
-    vetted_by = models.ForeignKey (Contributor, blank=True, null=True, on_delete=models.CASCADE)
-    type = models.CharField(max_length=9, choices=COMMENTARY_TYPES) # published paper or arxiv preprint
+    vetted_by = models.ForeignKey(Contributor, blank=True, null=True, on_delete=models.CASCADE)
+    type = models.CharField(max_length=9, choices=COMMENTARY_TYPES)
     discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default='physics')
     domain = models.CharField(max_length=3, choices=SCIPOST_JOURNALS_DOMAINS)
-#    specialization = models.CharField(max_length=1, choices=SCIPOST_JOURNALS_SPECIALIZATIONS)
-    subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS, default='Phys:QP')
+    subject_area = models.CharField(
+        max_length=10, choices=SCIPOST_SUBJECT_AREAS,
+        default='Phys:QP')
     open_for_commenting = models.BooleanField(default=True)
     pub_title = models.CharField(max_length=300, verbose_name='title')
-    arxiv_identifier = models.CharField(max_length=100,
-                                        verbose_name="arXiv identifier (including version nr)",
-                                        blank=True, null=True)
+    arxiv_identifier = models.CharField(
+        max_length=100, verbose_name="arXiv identifier (including version nr)",
+        blank=True, null=True)
     arxiv_link = models.URLField(verbose_name='arXiv link (including version nr)', blank=True)
-    pub_DOI = models.CharField(max_length=200, verbose_name='DOI of the original publication',
-                               blank=True, null=True)
-    pub_DOI_link = models.URLField(verbose_name='DOI link to the original publication', blank=True)
+    pub_DOI = models.CharField(
+        max_length=200, verbose_name='DOI of the original publication',
+        blank=True, null=True)
+    pub_DOI_link = models.URLField(
+        verbose_name='DOI link to the original publication',
+        blank=True)
     metadata = JSONField(default={}, blank=True, null=True)
     arxiv_or_DOI_string = models.CharField(
         max_length=100,
         verbose_name='string form of arxiv nr or DOI for commentary url',
         default='')
     author_list = models.CharField(max_length=1000)
+
     # Authors which have been mapped to contributors:
-    authors = models.ManyToManyField (Contributor, blank=True,
-                                      related_name='authors_com')
-    authors_claims = models.ManyToManyField (Contributor, blank=True,
-                                             related_name='authors_com_claims')
-    authors_false_claims = models.ManyToManyField (Contributor, blank=True,
-                                                   related_name='authors_com_false_claims')
+    authors = models.ManyToManyField(
+        Contributor, blank=True,
+        related_name='authors_com')
+    authors_claims = models.ManyToManyField(
+        Contributor, blank=True,
+        related_name='authors_com_claims')
+    authors_false_claims = models.ManyToManyField(
+        Contributor, blank=True,
+        related_name='authors_com_false_claims')
     journal = models.CharField(max_length=300, blank=True, null=True)
     volume = models.CharField(max_length=50, blank=True, null=True)
     pages = models.CharField(max_length=50, blank=True, null=True)
-    pub_date = models.DateField(verbose_name='date of original publication', blank=True, null=True)
+    pub_date = models.DateField(
+        verbose_name='date of original publication',
+        blank=True, null=True)
     pub_abstract = models.TextField(verbose_name='abstract')
-    latest_activity = models.DateTimeField(default=timezone.now)
+
+    objects = CommentaryManager()
 
     class Meta:
         verbose_name_plural = 'Commentaries'
 
-
     def __str__(self):
         return self.pub_title
 
-
     def header_as_table(self):
         # for display in Commentary page itself
         header = ('<table>'
@@ -93,8 +109,8 @@ class Commentary(models.Model):
         header += '</table>'
         template = Template(header)
         context = Context({
-                'pub_title': self.pub_title, 'author_list': self.author_list,
-                })
+            'pub_title': self.pub_title, 'author_list': self.author_list,
+        })
         if self.type == 'published':
             context['journal'] = self.journal
             context['volume'] = self.volume
@@ -105,7 +121,6 @@ class Commentary(models.Model):
             context['arxiv_link'] = self.arxiv_link
         return template.render(context)
 
-
     def header_as_li(self):
         # for display in search lists
         context = Context({'scipost_url': self.scipost_url(), 'pub_title': self.pub_title,
@@ -136,7 +151,6 @@ class Commentary(models.Model):
 
         return template.render(context)
 
-
     def simple_header_as_li(self):
         # for display in Lists
         context = Context({'scipost_url': self.scipost_url(), 'pub_title': self.pub_title,
@@ -158,7 +172,6 @@ class Commentary(models.Model):
         template = Template(header)
         return template.render(context)
 
-
     def parse_links_into_urls(self):
         """ Takes the arXiv nr or DOI and turns it into the urls """
         if self.pub_DOI:
diff --git a/commentaries/test_forms.py b/commentaries/test_forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..694ce19ec56c93503b879a51038f068b06947f7c
--- /dev/null
+++ b/commentaries/test_forms.py
@@ -0,0 +1,42 @@
+import factory
+
+from django.test import TestCase
+
+from scipost.factories import UserFactory
+
+from .factories import VettedCommentaryFactory
+from .forms import RequestCommentaryForm
+from common.helpers import model_form_data
+
+
+class TestRequestCommentaryForm(TestCase):
+    fixtures = ['permissions', 'groups']
+
+    def setUp(self):
+        factory_instance = VettedCommentaryFactory.build()
+        self.user = UserFactory()
+        self.valid_form_data = model_form_data(factory_instance, RequestCommentaryForm)
+
+    def test_valid_data_is_valid_for_arxiv(self):
+        """Test valid form for Arxiv identifier"""
+        form_data = self.valid_form_data
+        form_data['pub_DOI'] = ''
+        form = RequestCommentaryForm(form_data, user=self.user)
+        self.assertTrue(form.is_valid())
+
+    def test_valid_data_is_valid_for_DOI(self):
+        """Test valid form for DOI"""
+        form_data = self.valid_form_data
+        form_data['arxiv_identifier'] = ''
+        form = RequestCommentaryForm(form_data, user=self.user)
+        self.assertTrue(form.is_valid())
+
+    # def test_form_has_no_identifiers(self):
+    #     """Test invalid form has no DOI nor Arxiv ID"""
+    #     form_data = self.valid_form_data
+    #     form_data['pub_DOI'] = ''
+    #     form_data['arxiv_identifier'] = ''
+    #     form = RequestCommentaryForm(form_data, user=self.user)
+    #     form_response = form.is_valid()
+    #     print(form_response)
+    #     self.assertFormError(form_response, form, 'arxiv_identifier', None)
diff --git a/theses/tests.py b/commentaries/test_models.py
similarity index 55%
rename from theses/tests.py
rename to commentaries/test_models.py
index 7ce503c2dd97ba78597f6ff6e4393132753573f6..2e9cb5f6ba351402af656aec1be5d9ac257bc5c0 100644
--- a/theses/tests.py
+++ b/commentaries/test_models.py
@@ -1,3 +1 @@
 from django.test import TestCase
-
-# Create your tests here.
diff --git a/commentaries/test_views.py b/commentaries/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..e6cc591f82f92345fe466d9dad9a23fe0d24063f
--- /dev/null
+++ b/commentaries/test_views.py
@@ -0,0 +1,29 @@
+from django.contrib.auth.models import Group
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+class RequestCommentaryTest(TestCase):
+    """Test cases for `request_commentary` view method"""
+    fixtures = ['permissions', 'groups', 'contributors']
+
+    def setUp(self):
+        self.view_url = reverse('commentaries:request_commentary')
+        self.login_url = reverse('scipost:login')
+        self.redirected_login_url = '%s?next=%s' % (self.login_url, self.view_url)
+
+    def test_get_requests(self):
+        """Test different GET requests on view"""
+        # Anoymous user should redirect to login page
+        request = self.client.get(self.view_url)
+        self.assertRedirects(request, self.redirected_login_url)
+
+        # Registered Contributor should get 200
+        self.client.login(username="Test", password="testpw")
+        request = self.client.get(self.view_url)
+        self.assertEquals(request.status_code, 200)
+
+    def test_post_invalid_forms(self):
+        """Test different kind of invalid RequestCommentaryForm submits"""
+        self.client.login(username="Test", password="testpw")
+        request = self.client.post(self.view_url)
+        self.assertEquals(request.status_code, 200)
diff --git a/commentaries/views.py b/commentaries/views.py
index 88f4d7c69b97e0c58870415078bd703ea2afa578..506fd838fc126d015f7c7144b29e4f73a4340b0f 100644
--- a/commentaries/views.py
+++ b/commentaries/views.py
@@ -6,15 +6,12 @@ import requests
 from django.db.models import Q
 from django.utils import timezone
 from django.shortcuts import get_object_or_404, render
-from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth import login, logout
 from django.contrib.auth.decorators import login_required, permission_required
-from django.contrib.auth.models import User
 from django.core.mail import EmailMessage
 from django.core.urlresolvers import reverse
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse
 from django.shortcuts import redirect
-from django.views.decorators.csrf import csrf_protect
-from django.db.models import Avg
 
 from .models import Commentary
 from .forms import RequestCommentaryForm, DOIToQueryForm, IdentifierToQueryForm
@@ -35,64 +32,29 @@ from scipost.forms import AuthenticationForm
 @login_required
 @permission_required('scipost.can_request_commentary_pages', raise_exception=True)
 def request_commentary(request):
+    form = RequestCommentaryForm(request.POST or None, user=request.user)
     if request.method == 'POST':
-        form = RequestCommentaryForm(request.POST)
         if form.is_valid():
-            errormessage = ''
-            existing_commentary = None
-            if not form.cleaned_data['arxiv_identifier'] and not form.cleaned_data['pub_DOI']:
-                errormessage = ('You must provide either a DOI (for a published paper) '
-                                'or an arXiv identifier (for a preprint).')
-            elif (form.cleaned_data['arxiv_identifier'] and
-                  (Commentary.objects
-                   .filter(arxiv_identifier=form.cleaned_data['arxiv_identifier']).exists())):
-                errormessage = 'There already exists a Commentary Page on this preprint, see'
-                existing_commentary = get_object_or_404(
-                    Commentary,
-                    arxiv_identifier=form.cleaned_data['arxiv_identifier'])
-            elif (form.cleaned_data['pub_DOI'] and
-                  Commentary.objects.filter(pub_DOI=form.cleaned_data['pub_DOI']).exists()):
-                errormessage = 'There already exists a Commentary Page on this publication, see'
-                existing_commentary = get_object_or_404(Commentary, pub_DOI=form.cleaned_data['pub_DOI'])
-            if errormessage:
-                doiform = DOIToQueryForm()
-                identifierform = IdentifierToQueryForm()
-                context = {'form': form, 'doiform': doiform, 'identifierform': identifierform,
-                           'errormessage': errormessage,
-                           'existing_commentary': existing_commentary}
-                return render(request, 'commentaries/request_commentary.html', context)
-            
-            # Otherwise we can create the Commentary
-            contributor = Contributor.objects.get(user=request.user)
-            commentary = Commentary (
-                requested_by = contributor,
-                type = form.cleaned_data['type'],
-                discipline = form.cleaned_data['discipline'],
-                domain = form.cleaned_data['domain'],
-                subject_area = form.cleaned_data['subject_area'],
-                pub_title = form.cleaned_data['pub_title'],
-                arxiv_identifier = form.cleaned_data['arxiv_identifier'],
-                pub_DOI = form.cleaned_data['pub_DOI'],
-                metadata = form.cleaned_data['metadata'],
-                author_list = form.cleaned_data['author_list'],
-                journal = form.cleaned_data['journal'],
-                volume = form.cleaned_data['volume'],
-                pages = form.cleaned_data['pages'],
-                pub_date = form.cleaned_data['pub_date'],
-                pub_abstract = form.cleaned_data['pub_abstract'],
-                latest_activity = timezone.now(),
-                )
+            commentary = form.save(commit=False)
             commentary.parse_links_into_urls()
             commentary.save()
-            
+
             context = {'ack_header': 'Thank you for your request for a Commentary Page',
                        'ack_message': 'Your request will soon be handled by an Editor. ',
                        'followup_message': 'Return to your ',
                        'followup_link': reverse('scipost:personal_page'),
                        'followup_link_label': 'personal page'}
             return render(request, 'scipost/acknowledgement.html', context)
-    else:
-        form = RequestCommentaryForm()
+
+        else:
+            doiform = DOIToQueryForm()
+            existing_commentary = form.get_existing_commentary()
+            identifierform = IdentifierToQueryForm()
+            context = {'form': form, 'doiform': doiform, 'identifierform': identifierform,
+                       'errormessage': form.errors,
+                       'existing_commentary': existing_commentary}
+            return render(request, 'commentaries/request_commentary.html', context)
+
     doiform = DOIToQueryForm()
     identifierform = IdentifierToQueryForm()
     context = {'form': form, 'doiform': doiform, 'identifierform': identifierform}
@@ -120,7 +82,7 @@ def prefill_using_DOI(request):
                            'errormessage': errormessage,
                            'existing_commentary': existing_commentary}
                 return render(request, 'commentaries/request_commentary.html', context)
-            
+
             # Otherwise we query Crossref for the information:
             try:
                 queryurl = 'http://api.crossref.org/works/%s' % doiform.cleaned_data['doi']
@@ -133,12 +95,12 @@ def prefill_using_DOI(request):
                 for author in doiqueryJSON['message']['author'][1:]:
                     authorlist += ', ' + author['given'] + ' ' + author['family']
                 journal = doiqueryJSON['message']['container-title'][0]
-                
+
                 try:
                     volume = doiqueryJSON['message']['volume']
                 except KeyError:
                     volume = ''
-                
+
                 pages = ''
                 try:
                     pages = doiqueryJSON['message']['article-number'] # for Phys Rev
@@ -148,7 +110,7 @@ def prefill_using_DOI(request):
                     pages = doiqueryJSON['message']['page']
                 except KeyError:
                     pass
-                
+
                 pub_date = ''
                 try:
                     pub_date = (str(doiqueryJSON['message']['issued']['date-parts'][0][0]) + '-' +
@@ -209,7 +171,7 @@ def prefill_using_identifier(request):
                 queryurl = ('http://export.arxiv.org/api/query?id_list=%s'
                             % identifierform.cleaned_data['identifier'])
                 arxivquery = feedparser.parse(queryurl)
-                
+
                 # If paper has been published, should comment on published version
                 try:
                     arxiv_journal_ref = arxivquery['entries'][0]['arxiv_journal_ref']
@@ -223,7 +185,7 @@ def prefill_using_identifier(request):
                                     + '. Please comment on the published version.')
                 except (IndexError, KeyError):
                     pass
-                
+
                 if errormessage:
                     form = RequestCommentaryForm()
                     doiform = DOIToQueryForm()
@@ -231,7 +193,7 @@ def prefill_using_identifier(request):
                                'errormessage': errormessage,
                                'existing_commentary': existing_commentary}
                     return render(request, 'commentaries/request_commentary.html', context)
-                
+
                 # otherwise prefill the form:
                 metadata = arxivquery
                 pub_title = arxivquery['entries'][0]['title']
@@ -265,7 +227,7 @@ def prefill_using_identifier(request):
 @permission_required('scipost.can_vet_commentary_requests', raise_exception=True)
 def vet_commentary_requests(request):
     contributor = Contributor.objects.get(user=request.user)
-    commentary_to_vet = Commentary.objects.filter(vetted=False).first() # only handle one at a time
+    commentary_to_vet = Commentary.objects.awaiting_vetting().first() # only handle one at a time
     form = VetCommentaryForm()
     context = {'contributor': contributor, 'commentary_to_vet': commentary_to_vet, 'form': form }
     return render(request, 'commentaries/vet_commentary_requests.html', context)
@@ -348,62 +310,33 @@ def vet_commentary_request_ack(request, commentary_id):
                'followup_link_label': 'Commentary requests page'}
     return render(request, 'scipost/acknowledgement.html', context)
 
-
 def commentaries(request):
-    if request.method == 'POST':
-        form = CommentarySearchForm(request.POST)
-        if form.is_valid() and form.has_changed():
-            commentary_search_list = Commentary.objects.filter(
-                pub_title__icontains=form.cleaned_data['pub_title_keyword'],
-                author_list__icontains=form.cleaned_data['pub_author'],
-                pub_abstract__icontains=form.cleaned_data['pub_abstract_keyword'],
-                vetted=True,
-                )
-            commentary_search_list.order_by('-pub_date')
-        else:
-            commentary_search_list = []
-
+    """List and search all commentaries"""
+    form = CommentarySearchForm(request.POST or None)
+    if form.is_valid() and form.has_changed():
+        commentary_search_list = form.search_results()
     else:
-        form = CommentarySearchForm()
         commentary_search_list = []
 
-    comment_recent_list = (Comment.objects.filter(status='1')
-                           .order_by('-date_submitted')[:10])
-
-    commentary_recent_list = (Commentary.objects.filter(vetted=True)
-                              .order_by('-latest_activity')[:10])
-    context = {'form': form, 'commentary_search_list': commentary_search_list,
-               'comment_recent_list': comment_recent_list,
-               'commentary_recent_list': commentary_recent_list }
+    comment_recent_list = Comment.objects.filter(status='1').order_by('-date_submitted')[:10]
+    commentary_recent_list = Commentary.objects.vetted().order_by('-latest_activity')[:10]
+    context = {
+        'form': form, 'commentary_search_list': commentary_search_list,
+        'comment_recent_list': comment_recent_list,
+        'commentary_recent_list': commentary_recent_list}
     return render(request, 'commentaries/commentaries.html', context)
 
-
 def browse(request, discipline, nrweeksback):
-    if request.method == 'POST':
-        form = CommentarySearchForm(request.POST)
-        if form.is_valid() and form.has_changed():
-            commentary_search_list = Commentary.objects.filter(
-                pub_title__icontains=form.cleaned_data['pub_title_keyword'],
-                author_list__icontains=form.cleaned_data['pub_author'],
-                pub_abstract__icontains=form.cleaned_data['pub_abstract_keyword'],
-                vetted=True,
-                )
-            commentary_search_list.order_by('-pub_date')
-        else:
-            commentary_search_list = []
-        context = {'form': form, 'commentary_search_list': commentary_search_list}
-        return HttpResponseRedirect(request, 'commentaries/commentaries.html', context)
-    else:
-        form = CommentarySearchForm()
-    commentary_browse_list = Commentary.objects.filter(
-        vetted=True, discipline=discipline,
-        latest_activity__gte=timezone.now() + datetime.timedelta(weeks=-int(nrweeksback))
-        )
-    context = {'form': form, 'discipline': discipline, 'nrweeksback': nrweeksback,
-               'commentary_browse_list': commentary_browse_list }
+    """List all commentaries for discipline and period"""
+    commentary_browse_list = Commentary.objects.vetted(
+        discipline=discipline,
+        latest_activity__gte=timezone.now() + datetime.timedelta(weeks=-int(nrweeksback)))
+    context = {
+        'form': CommentarySearchForm(),
+        'discipline': discipline, 'nrweeksback': nrweeksback,
+        'commentary_browse_list': commentary_browse_list}
     return render(request, 'commentaries/commentaries.html', context)
 
-
 def commentary_detail(request, arxiv_or_DOI_string):
     commentary = get_object_or_404(Commentary, arxiv_or_DOI_string=arxiv_or_DOI_string)
     comments = commentary.comment_set.all()
diff --git a/comments/forms.py b/comments/forms.py
index 0e94f326ed13e5e2388230d1c5d4cdf41a19fecf..d87f969a38eb8bb7c03fba8d8fc7bfb632141207 100644
--- a/comments/forms.py
+++ b/comments/forms.py
@@ -62,7 +62,6 @@ class CommentForm(forms.ModelForm):
             )
 
 
-
 class VetCommentForm(forms.Form):
     action_option = forms.ChoiceField(widget=forms.RadioSelect, choices=COMMENT_ACTION_CHOICES,
                                       required=True, label='Action')
diff --git a/comments/models.py b/comments/models.py
index d18801033f5fa367ab1b16bfbf812dd0cb0e25fa..1e6c1c5d1da23a916f201a3e664b24e001ab0d7d 100644
--- a/comments/models.py
+++ b/comments/models.py
@@ -33,15 +33,11 @@ COMMENT_STATUS = (
 )
 comment_status_dict = dict(COMMENT_STATUS)
 
+
 class Comment(models.Model):
     """ A Comment is an unsollicited note, submitted by a Contributor,
     on a particular publication or in reply to an earlier Comment. """
-    # status:
-    # 1: vetted
-    # 0: unvetted
-    # -1: rejected (unclear)
-    # -2: rejected (incorrect)
-    # -3: rejected (not useful)
+
     status = models.SmallIntegerField(default=0)
     vetted_by = models.ForeignKey(Contributor, blank=True, null=True,
                                   on_delete=models.CASCADE,
diff --git a/common/__init__.py b/common/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/common/helpers/__init__.py b/common/helpers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a29cff832e8239e0ffc0cf1db98d51745c4c88c8
--- /dev/null
+++ b/common/helpers/__init__.py
@@ -0,0 +1,30 @@
+def model_form_data(model, form_class):
+    '''
+    Returns a dict that can be used to instantiate a form object.
+    It fills in the model's data, but filters out fields that are not on the form.
+    Example:
+
+    class Car(models.Model):
+        brand = CharField(max_length = 50)
+        fuel_tank_size = FloatField()
+        # more fields
+
+    class CreateCarForm(forms.ModelForm):
+        fields = ['brand']
+
+    my_car = Car(brand='Nissan', fuel_tank_size=60)
+
+    model_form_data(my_car, CreateCarForm)
+    # returns {'brand': 'Nissan'}
+
+    Note that the returned dict does not have a field 'fuel_tank_size', because it is not
+    on the form.
+    '''
+
+    model_data = model.__dict__
+    form_fields = list(form_class().fields.keys())
+    return filter_keys(model_data, form_fields)
+
+
+def filter_keys(dictionary, keys_to_keep):
+    return {key: dictionary[key] for key in keys_to_keep}
diff --git a/journals/models.py b/journals/models.py
index 8581d9d7915b7f7f4ad6acc16d0d5b2097ad3ed7..80464164d6915e51439452e8d41f5739657fa91a 100644
--- a/journals/models.py
+++ b/journals/models.py
@@ -3,8 +3,8 @@ from django.db import models
 from django.template import Template, Context
 from django.utils import timezone
 
-from scipost.models import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict, TITLE_CHOICES
-from scipost.models import ChoiceArrayField, Contributor
+from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict
+from scipost.models import ChoiceArrayField, Contributor, TITLE_CHOICES
 
 
 class UnregisteredAuthor(models.Model):
@@ -22,9 +22,11 @@ SCIPOST_JOURNALS = (
     )
 journals_dict = dict(SCIPOST_JOURNALS)
 
+
 class JournalNameError(Exception):
     def __init__(self, name):
         self.name = name
+
     def __str__(self):
         return self.name
 
diff --git a/requirements.txt b/requirements.txt
index 1dbfb212397ecfd37a21f1fac65f3bafd5975002..b25c7e2fba57bc091dcc0578fac5910b75817117 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,14 +10,18 @@ django-mptt==0.8.6
 django-simple-captcha==0.5.3
 djangorestframework==3.5.3
 docutils==0.12
+factory-boy==2.7.0
+fake-factory==0.7.2
 feedparser==5.2.1
 imagesize==0.7.1
 Jinja2==2.8
 Markdown==2.6.7
 MarkupSafe==0.23
+pep8==1.7.0
 Pillow==3.4.2
 psycopg2==2.6.2
 Pygments==2.1.3
+python-dateutil==2.6.0
 pytz==2016.7
 requests==2.12.1
 six==1.10.0
diff --git a/scipost/constants.py b/scipost/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c7933e18e787769f830f999423730a6af836d82
--- /dev/null
+++ b/scipost/constants.py
@@ -0,0 +1,120 @@
+SCIPOST_DISCIPLINES = (
+    ('physics', 'Physics'),
+    ('astrophysics', 'Astrophysics'),
+    ('mathematics', 'Mathematics'),
+    ('computerscience', 'Computer Science'),
+    )
+disciplines_dict = dict(SCIPOST_DISCIPLINES)
+
+SCIPOST_SUBJECT_AREAS = (
+    ('Physics', (
+        ('Phys:AE', 'Atomic, Molecular and Optical Physics - Experiment'),
+        ('Phys:AT', 'Atomic, Molecular and Optical Physics - Theory'),
+        ('Phys:BI', 'Biophysics'),
+        ('Phys:CE', 'Condensed Matter Physics - Experiment'),
+        ('Phys:CT', 'Condensed Matter Physics - Theory'),
+        ('Phys:FD', 'Fluid Dynamics'),
+        ('Phys:GR', 'Gravitation, Cosmology and Astroparticle Physics'),
+        ('Phys:HE', 'High-Energy Physics - Experiment'),
+        ('Phys:HT', 'High-Energy Physics- Theory'),
+        ('Phys:HP', 'High-Energy Physics - Phenomenology'),
+        ('Phys:MP', 'Mathematical Physics'),
+        ('Phys:NE', 'Nuclear Physics - Experiment'),
+        ('Phys:NT', 'Nuclear Physics - Theory'),
+        ('Phys:QP', 'Quantum Physics'),
+        ('Phys:SM', 'Statistical and Soft Matter Physics'),
+        )
+     ),
+    ('Astrophysics', (
+        ('Astro:GA', 'Astrophysics of Galaxies'),
+        ('Astro:CO', 'Cosmology and Nongalactic Astrophysics'),
+        ('Astro:EP', 'Earth and Planetary Astrophysics'),
+        ('Astro:HE', 'High Energy Astrophysical Phenomena'),
+        ('Astro:IM', 'Instrumentation and Methods for Astrophysics'),
+        ('Astro:SR', 'Solar and Stellar Astrophysics'),
+        )
+     ),
+    ('Mathematics', (
+        ('Math:AG', 'Algebraic Geometry'),
+        ('Math:AT', 'Algebraic Topology'),
+        ('Math:AP', 'Analysis of PDEs'),
+        ('Math:CT', 'Category Theory'),
+        ('Math:CA', 'Classical Analysis and ODEs'),
+        ('Math:CO', 'Combinatorics'),
+        ('Math:AC', 'Commutative Algebra'),
+        ('Math:CV', 'Complex Variables'),
+        ('Math:DG', 'Differential Geometry'),
+        ('Math:DS', 'Dynamical Systems'),
+        ('Math:FA', 'Functional Analysis'),
+        ('Math:GM', 'General Mathematics'),
+        ('Math:GN', 'General Topology'),
+        ('Math:GT', 'Geometric Topology'),
+        ('Math:GR', 'Group Theory'),
+        ('Math:HO', 'History and Overview'),
+        ('Math:IT', 'Information Theory'),
+        ('Math:KT', 'K-Theory and Homology'),
+        ('Math:LO', 'Logic'),
+        ('Math:MP', 'Mathematical Physics'),
+        ('Math:MG', 'Metric Geometry'),
+        ('Math:NT', 'Number Theory'),
+        ('Math:NA', 'Numerical Analysis'),
+        ('Math:OA', 'Operator Algebras'),
+        ('Math:OC', 'Optimization and Control'),
+        ('Math:PR', 'Probability'),
+        ('Math:QA', 'Quantum Algebra'),
+        ('Math:RT', 'Representation Theory'),
+        ('Math:RA', 'Rings and Algebras'),
+        ('Math:SP', 'Spectral Theory'),
+        ('Math:ST', 'Statistics Theory'),
+        ('Math:SG', 'Symplectic Geometry'),
+        )
+     ),
+    ('Computer Science', (
+        ('Comp:AI', 'Artificial Intelligence'),
+        ('Comp:CC', 'Computational Complexity'),
+        ('Comp:CE', 'Computational Engineering, Finance, and Science'),
+        ('Comp:CG', 'Computational Geometry'),
+        ('Comp:GT', 'Computer Science and Game Theory'),
+        ('Comp:CV', 'Computer Vision and Pattern Recognition'),
+        ('Comp:CY', 'Computers and Society'),
+        ('Comp:CR', 'Cryptography and Security'),
+        ('Comp:DS', 'Data Structures and Algorithms'),
+        ('Comp:DB', 'Databases'),
+        ('Comp:DL', 'Digital Libraries'),
+        ('Comp:DM', 'Discrete Mathematics'),
+        ('Comp:DC', 'Distributed, Parallel, and Cluster Computing'),
+        ('Comp:ET', 'Emerging Technologies'),
+        ('Comp:FL', 'Formal Languages and Automata Theory'),
+        ('Comp:GL', 'General Literature'),
+        ('Comp:GR', 'Graphics'),
+        ('Comp:AR', 'Hardware Architecture'),
+        ('Comp:HC', 'Human-Computer Interaction'),
+        ('Comp:IR', 'Information Retrieval'),
+        ('Comp:IT', 'Information Theory'),
+        ('Comp:LG', 'Learning'),
+        ('Comp:LO', 'Logic in Computer Science'),
+        ('Comp:MS', 'Mathematical Software'),
+        ('Comp:MA', 'Multiagent Systems'),
+        ('Comp:MM', 'Multimedia'),
+        ('Comp:NI', 'Networking and Internet Architecture'),
+        ('Comp:NE', 'Neural and Evolutionary Computing'),
+        ('Comp:NA', 'Numerical Analysis'),
+        ('Comp:OS', 'Operating Systems'),
+        ('Comp:OH', 'Other Computer Science'),
+        ('Comp:PF', 'Performance'),
+        ('Comp:PL', 'Programming Languages'),
+        ('Comp:RO', 'Robotics'),
+        ('Comp:SI', 'Social and Information Networks'),
+        ('Comp:SE', 'Software Engineering'),
+        ('Comp:SD', 'Sound'),
+        ('Comp:SC', 'Symbolic Computation'),
+        ('Comp:SY', 'Systems and Control'),
+        )
+     ),
+)
+subject_areas_raw_dict = dict(SCIPOST_SUBJECT_AREAS)
+
+# Make dict of the form {'Phys:AT': 'Atomic...', ...}
+subject_areas_dict = {}
+for k in subject_areas_raw_dict.keys():
+    subject_areas_dict.update(dict(subject_areas_raw_dict[k]))
diff --git a/scipost/factories.py b/scipost/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..c69b975f60db3f7f1b5952012384a719ecc7c9bb
--- /dev/null
+++ b/scipost/factories.py
@@ -0,0 +1,41 @@
+import factory
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Group, User
+
+from .models import Contributor
+
+
+class ContributorFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = Contributor
+
+    title = "MR"
+    user = factory.SubFactory('scipost.factories.UserFactory', contributor=None)
+    status = 1
+    vetted_by = factory.SubFactory('scipost.factories.ContributorFactory', vetted_by=None)
+
+
+class UserFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = get_user_model()
+
+    username = factory.Faker('user_name')
+    password = factory.Faker('password')
+    email = factory.Faker('safe_email')
+    first_name = factory.Faker('first_name')
+    last_name = factory.Faker('last_name')
+    # When user object is created, associate new Contributor object to it.
+    contributor = factory.RelatedFactory(ContributorFactory, 'user')
+
+    @factory.post_generation
+    def groups(self, create, extracted, **kwargs):
+        # If the object is not saved, we cannot use many-to-many relationship.
+        if not create:
+            return
+        # If group objects were passed in, use those.
+        if extracted:
+            for group in extracted:
+                self.groups.add(group)
+        else:
+            self.groups.add(Group.objects.get(name="Registered Contributors"))
diff --git a/scipost/fixtures/contributors.json b/scipost/fixtures/contributors.json
new file mode 100644
index 0000000000000000000000000000000000000000..a4863604278b1460829d963d108b4db098579aef
--- /dev/null
+++ b/scipost/fixtures/contributors.json
@@ -0,0 +1,32 @@
+[
+  {
+    "model": "auth.user",
+    "pk": 1,
+    "fields": {
+      "password": "pbkdf2_sha256$30000$iqtXX60Ahqcx$IKfNZNSMbSca/agzPXHTdEej3dXhQi1sK/MCrBTnuW4=",
+      "last_login": null,
+      "is_superuser": false,
+      "username": "Test",
+      "first_name": "Firstname",
+      "last_name": "Testuser",
+      "email": "testuser@test.com",
+      "is_staff": false,
+      "is_active": true,
+      "date_joined": "2016-12-14T20:41:31.282Z",
+      "groups": [
+        6
+      ],
+      "user_permissions": []
+    }
+  },
+  {
+    "model": "scipost.contributor",
+    "pk": 2,
+    "fields": {
+      "user": 1,
+      "status": 1,
+      "title": "MR",
+      "vetted_by": 2
+    }
+  }
+]
diff --git a/scipost/fixtures/groups.json b/scipost/fixtures/groups.json
new file mode 100644
index 0000000000000000000000000000000000000000..82fafdc4738001941947ba92ecf8d9a5749ad11d
--- /dev/null
+++ b/scipost/fixtures/groups.json
@@ -0,0 +1,116 @@
+[
+{
+    "model": "auth.group",
+    "pk": 1,
+    "fields": {
+        "name": "SciPost Administrators",
+        "permissions": [
+            143,
+            130,
+            131,
+            148,
+            128,
+            147,
+            139,
+            137,
+            140,
+            126,
+            138,
+            142
+        ]
+    }
+},
+{
+    "model": "auth.group",
+    "pk": 2,
+    "fields": {
+        "name": "Advisory Board",
+        "permissions": [
+            128
+        ]
+    }
+},
+{
+    "model": "auth.group",
+    "pk": 3,
+    "fields": {
+        "name": "Editorial Administrators",
+        "permissions": [
+            143,
+            148,
+            147,
+            149,
+            142
+        ]
+    }
+},
+{
+    "model": "auth.group",
+    "pk": 4,
+    "fields": {
+        "name": "Editorial College",
+        "permissions": [
+            144,
+            145,
+            142,
+            132
+        ]
+    }
+},
+{
+    "model": "auth.group",
+    "pk": 5,
+    "fields": {
+        "name": "Vetting Editors",
+        "permissions": [
+            139,
+            137,
+            140,
+            138
+        ]
+    }
+},
+{
+    "model": "auth.group",
+    "pk": 6,
+    "fields": {
+        "name": "Registered Contributors",
+        "permissions": [
+            134,
+            146,
+            135,
+            136,
+            133,
+            141
+        ]
+    }
+},
+{
+    "model": "auth.group",
+    "pk": 7,
+    "fields": {
+        "name": "Testers",
+        "permissions": []
+    }
+},
+{
+    "model": "auth.group",
+    "pk": 8,
+    "fields": {
+        "name": "Ambassadors",
+        "permissions": [
+            128
+        ]
+    }
+},
+{
+    "model": "auth.group",
+    "pk": 9,
+    "fields": {
+        "name": "Junior Ambassadors",
+        "permissions": [
+            127
+        ]
+    }
+}
+]
diff --git a/scipost/fixtures/permissions.json b/scipost/fixtures/permissions.json
new file mode 100644
index 0000000000000000000000000000000000000000..a718c3476c978d97c20a15d8b81f01ac0500596f
--- /dev/null
+++ b/scipost/fixtures/permissions.json
@@ -0,0 +1,1343 @@
+[
+{
+    "model": "auth.permission",
+    "pk": 1,
+    "fields": {
+        "name": "Can add log entry",
+        "content_type": 1,
+        "codename": "add_logentry"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 2,
+    "fields": {
+        "name": "Can change log entry",
+        "content_type": 1,
+        "codename": "change_logentry"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 3,
+    "fields": {
+        "name": "Can delete log entry",
+        "content_type": 1,
+        "codename": "delete_logentry"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 4,
+    "fields": {
+        "name": "Can add user",
+        "content_type": 2,
+        "codename": "add_user"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 5,
+    "fields": {
+        "name": "Can change user",
+        "content_type": 2,
+        "codename": "change_user"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 6,
+    "fields": {
+        "name": "Can delete user",
+        "content_type": 2,
+        "codename": "delete_user"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 7,
+    "fields": {
+        "name": "Can add group",
+        "content_type": 3,
+        "codename": "add_group"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 8,
+    "fields": {
+        "name": "Can change group",
+        "content_type": 3,
+        "codename": "change_group"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 9,
+    "fields": {
+        "name": "Can delete group",
+        "content_type": 3,
+        "codename": "delete_group"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 10,
+    "fields": {
+        "name": "Can add permission",
+        "content_type": 4,
+        "codename": "add_permission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 11,
+    "fields": {
+        "name": "Can change permission",
+        "content_type": 4,
+        "codename": "change_permission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 12,
+    "fields": {
+        "name": "Can delete permission",
+        "content_type": 4,
+        "codename": "delete_permission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 13,
+    "fields": {
+        "name": "Can add content type",
+        "content_type": 5,
+        "codename": "add_contenttype"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 14,
+    "fields": {
+        "name": "Can change content type",
+        "content_type": 5,
+        "codename": "change_contenttype"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 15,
+    "fields": {
+        "name": "Can delete content type",
+        "content_type": 5,
+        "codename": "delete_contenttype"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 16,
+    "fields": {
+        "name": "Can add session",
+        "content_type": 6,
+        "codename": "add_session"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 17,
+    "fields": {
+        "name": "Can change session",
+        "content_type": 6,
+        "codename": "change_session"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 18,
+    "fields": {
+        "name": "Can delete session",
+        "content_type": 6,
+        "codename": "delete_session"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 19,
+    "fields": {
+        "name": "Can add captcha store",
+        "content_type": 7,
+        "codename": "add_captchastore"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 20,
+    "fields": {
+        "name": "Can change captcha store",
+        "content_type": 7,
+        "codename": "change_captchastore"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 21,
+    "fields": {
+        "name": "Can delete captcha store",
+        "content_type": 7,
+        "codename": "delete_captchastore"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 22,
+    "fields": {
+        "name": "Can add user object permission",
+        "content_type": 8,
+        "codename": "add_userobjectpermission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 23,
+    "fields": {
+        "name": "Can change user object permission",
+        "content_type": 8,
+        "codename": "change_userobjectpermission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 24,
+    "fields": {
+        "name": "Can delete user object permission",
+        "content_type": 8,
+        "codename": "delete_userobjectpermission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 25,
+    "fields": {
+        "name": "Can add group object permission",
+        "content_type": 9,
+        "codename": "add_groupobjectpermission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 26,
+    "fields": {
+        "name": "Can change group object permission",
+        "content_type": 9,
+        "codename": "change_groupobjectpermission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 27,
+    "fields": {
+        "name": "Can delete group object permission",
+        "content_type": 9,
+        "codename": "delete_groupobjectpermission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 28,
+    "fields": {
+        "name": "Can add commentary",
+        "content_type": 10,
+        "codename": "add_commentary"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 29,
+    "fields": {
+        "name": "Can change commentary",
+        "content_type": 10,
+        "codename": "change_commentary"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 30,
+    "fields": {
+        "name": "Can delete commentary",
+        "content_type": 10,
+        "codename": "delete_commentary"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 31,
+    "fields": {
+        "name": "Can add comment",
+        "content_type": 11,
+        "codename": "add_comment"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 32,
+    "fields": {
+        "name": "Can change comment",
+        "content_type": 11,
+        "codename": "change_comment"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 33,
+    "fields": {
+        "name": "Can delete comment",
+        "content_type": 11,
+        "codename": "delete_comment"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 34,
+    "fields": {
+        "name": "Can add issue",
+        "content_type": 12,
+        "codename": "add_issue"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 35,
+    "fields": {
+        "name": "Can change issue",
+        "content_type": 12,
+        "codename": "change_issue"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 36,
+    "fields": {
+        "name": "Can delete issue",
+        "content_type": 12,
+        "codename": "delete_issue"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 37,
+    "fields": {
+        "name": "Can add journal",
+        "content_type": 13,
+        "codename": "add_journal"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 38,
+    "fields": {
+        "name": "Can change journal",
+        "content_type": 13,
+        "codename": "change_journal"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 39,
+    "fields": {
+        "name": "Can delete journal",
+        "content_type": 13,
+        "codename": "delete_journal"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 40,
+    "fields": {
+        "name": "Can add unregistered author",
+        "content_type": 14,
+        "codename": "add_unregisteredauthor"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 41,
+    "fields": {
+        "name": "Can change unregistered author",
+        "content_type": 14,
+        "codename": "change_unregisteredauthor"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 42,
+    "fields": {
+        "name": "Can delete unregistered author",
+        "content_type": 14,
+        "codename": "delete_unregisteredauthor"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 43,
+    "fields": {
+        "name": "Can add deposit",
+        "content_type": 15,
+        "codename": "add_deposit"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 44,
+    "fields": {
+        "name": "Can change deposit",
+        "content_type": 15,
+        "codename": "change_deposit"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 45,
+    "fields": {
+        "name": "Can delete deposit",
+        "content_type": 15,
+        "codename": "delete_deposit"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 46,
+    "fields": {
+        "name": "Can add publication",
+        "content_type": 16,
+        "codename": "add_publication"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 47,
+    "fields": {
+        "name": "Can change publication",
+        "content_type": 16,
+        "codename": "change_publication"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 48,
+    "fields": {
+        "name": "Can delete publication",
+        "content_type": 16,
+        "codename": "delete_publication"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 49,
+    "fields": {
+        "name": "Can add volume",
+        "content_type": 17,
+        "codename": "add_volume"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 50,
+    "fields": {
+        "name": "Can change volume",
+        "content_type": 17,
+        "codename": "change_volume"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 51,
+    "fields": {
+        "name": "Can delete volume",
+        "content_type": 17,
+        "codename": "delete_volume"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 52,
+    "fields": {
+        "name": "Can add news item",
+        "content_type": 18,
+        "codename": "add_newsitem"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 53,
+    "fields": {
+        "name": "Can change news item",
+        "content_type": 18,
+        "codename": "change_newsitem"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 54,
+    "fields": {
+        "name": "Can delete news item",
+        "content_type": 18,
+        "codename": "delete_newsitem"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 55,
+    "fields": {
+        "name": "Can add precooked email",
+        "content_type": 19,
+        "codename": "add_precookedemail"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 56,
+    "fields": {
+        "name": "Can change precooked email",
+        "content_type": 19,
+        "codename": "change_precookedemail"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 57,
+    "fields": {
+        "name": "Can delete precooked email",
+        "content_type": 19,
+        "codename": "delete_precookedemail"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 58,
+    "fields": {
+        "name": "Can add affiliation object",
+        "content_type": 20,
+        "codename": "add_affiliationobject"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 59,
+    "fields": {
+        "name": "Can change affiliation object",
+        "content_type": 20,
+        "codename": "change_affiliationobject"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 60,
+    "fields": {
+        "name": "Can delete affiliation object",
+        "content_type": 20,
+        "codename": "delete_affiliationobject"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 61,
+    "fields": {
+        "name": "Can add registration invitation",
+        "content_type": 21,
+        "codename": "add_registrationinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 62,
+    "fields": {
+        "name": "Can change registration invitation",
+        "content_type": 21,
+        "codename": "change_registrationinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 63,
+    "fields": {
+        "name": "Can delete registration invitation",
+        "content_type": 21,
+        "codename": "delete_registrationinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 64,
+    "fields": {
+        "name": "Can add spb membership agreement",
+        "content_type": 22,
+        "codename": "add_spbmembershipagreement"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 65,
+    "fields": {
+        "name": "Can change spb membership agreement",
+        "content_type": 22,
+        "codename": "change_spbmembershipagreement"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 66,
+    "fields": {
+        "name": "Can delete spb membership agreement",
+        "content_type": 22,
+        "codename": "delete_spbmembershipagreement"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 67,
+    "fields": {
+        "name": "Can add supporting partner",
+        "content_type": 23,
+        "codename": "add_supportingpartner"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 68,
+    "fields": {
+        "name": "Can change supporting partner",
+        "content_type": 23,
+        "codename": "change_supportingpartner"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 69,
+    "fields": {
+        "name": "Can delete supporting partner",
+        "content_type": 23,
+        "codename": "delete_supportingpartner"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 70,
+    "fields": {
+        "name": "Can add arc",
+        "content_type": 24,
+        "codename": "add_arc"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 71,
+    "fields": {
+        "name": "Can change arc",
+        "content_type": 24,
+        "codename": "change_arc"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 72,
+    "fields": {
+        "name": "Can delete arc",
+        "content_type": 24,
+        "codename": "delete_arc"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 73,
+    "fields": {
+        "name": "Can add graph",
+        "content_type": 25,
+        "codename": "add_graph"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 74,
+    "fields": {
+        "name": "Can view graph",
+        "content_type": 25,
+        "codename": "view_graph"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 75,
+    "fields": {
+        "name": "Can change graph",
+        "content_type": 25,
+        "codename": "change_graph"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 76,
+    "fields": {
+        "name": "Can delete graph",
+        "content_type": 25,
+        "codename": "delete_graph"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 77,
+    "fields": {
+        "name": "Can add contributor",
+        "content_type": 26,
+        "codename": "add_contributor"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 78,
+    "fields": {
+        "name": "Can change contributor",
+        "content_type": 26,
+        "codename": "change_contributor"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 79,
+    "fields": {
+        "name": "Can delete contributor",
+        "content_type": 26,
+        "codename": "delete_contributor"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 80,
+    "fields": {
+        "name": "Can add authorship claim",
+        "content_type": 27,
+        "codename": "add_authorshipclaim"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 81,
+    "fields": {
+        "name": "Can change authorship claim",
+        "content_type": 27,
+        "codename": "change_authorshipclaim"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 82,
+    "fields": {
+        "name": "Can delete authorship claim",
+        "content_type": 27,
+        "codename": "delete_authorshipclaim"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 83,
+    "fields": {
+        "name": "Can add unavailability period",
+        "content_type": 28,
+        "codename": "add_unavailabilityperiod"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 84,
+    "fields": {
+        "name": "Can change unavailability period",
+        "content_type": 28,
+        "codename": "change_unavailabilityperiod"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 85,
+    "fields": {
+        "name": "Can delete unavailability period",
+        "content_type": 28,
+        "codename": "delete_unavailabilityperiod"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 86,
+    "fields": {
+        "name": "Can add list",
+        "content_type": 29,
+        "codename": "add_list"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 87,
+    "fields": {
+        "name": "Can view list",
+        "content_type": 29,
+        "codename": "view_list"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 88,
+    "fields": {
+        "name": "Can change list",
+        "content_type": 29,
+        "codename": "change_list"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 89,
+    "fields": {
+        "name": "Can delete list",
+        "content_type": 29,
+        "codename": "delete_list"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 90,
+    "fields": {
+        "name": "Can add node",
+        "content_type": 30,
+        "codename": "add_node"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 91,
+    "fields": {
+        "name": "Can view node",
+        "content_type": 30,
+        "codename": "view_node"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 92,
+    "fields": {
+        "name": "Can change node",
+        "content_type": 30,
+        "codename": "change_node"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 93,
+    "fields": {
+        "name": "Can delete node",
+        "content_type": 30,
+        "codename": "delete_node"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 94,
+    "fields": {
+        "name": "Can add draft invitation",
+        "content_type": 31,
+        "codename": "add_draftinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 95,
+    "fields": {
+        "name": "Can change draft invitation",
+        "content_type": 31,
+        "codename": "change_draftinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 96,
+    "fields": {
+        "name": "Can delete draft invitation",
+        "content_type": 31,
+        "codename": "delete_draftinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 97,
+    "fields": {
+        "name": "Can add team",
+        "content_type": 32,
+        "codename": "add_team"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 98,
+    "fields": {
+        "name": "Can view team",
+        "content_type": 32,
+        "codename": "view_team"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 99,
+    "fields": {
+        "name": "Can change team",
+        "content_type": 32,
+        "codename": "change_team"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 100,
+    "fields": {
+        "name": "Can delete team",
+        "content_type": 32,
+        "codename": "delete_team"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 101,
+    "fields": {
+        "name": "Can add remark",
+        "content_type": 33,
+        "codename": "add_remark"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 102,
+    "fields": {
+        "name": "Can change remark",
+        "content_type": 33,
+        "codename": "change_remark"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 103,
+    "fields": {
+        "name": "Can delete remark",
+        "content_type": 33,
+        "codename": "delete_remark"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 104,
+    "fields": {
+        "name": "Can add editorial communication",
+        "content_type": 34,
+        "codename": "add_editorialcommunication"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 105,
+    "fields": {
+        "name": "Can change editorial communication",
+        "content_type": 34,
+        "codename": "change_editorialcommunication"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 106,
+    "fields": {
+        "name": "Can delete editorial communication",
+        "content_type": 34,
+        "codename": "delete_editorialcommunication"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 107,
+    "fields": {
+        "name": "Can add referee invitation",
+        "content_type": 35,
+        "codename": "add_refereeinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 108,
+    "fields": {
+        "name": "Can change referee invitation",
+        "content_type": 35,
+        "codename": "change_refereeinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 109,
+    "fields": {
+        "name": "Can delete referee invitation",
+        "content_type": 35,
+        "codename": "delete_refereeinvitation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 110,
+    "fields": {
+        "name": "Can add submission",
+        "content_type": 36,
+        "codename": "add_submission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 111,
+    "fields": {
+        "name": "Can change submission",
+        "content_type": 36,
+        "codename": "change_submission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 112,
+    "fields": {
+        "name": "Can delete submission",
+        "content_type": 36,
+        "codename": "delete_submission"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 113,
+    "fields": {
+        "name": "Can take editorial actions",
+        "content_type": 36,
+        "codename": "can_take_editorial_actions"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 114,
+    "fields": {
+        "name": "Can add report",
+        "content_type": 37,
+        "codename": "add_report"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 115,
+    "fields": {
+        "name": "Can change report",
+        "content_type": 37,
+        "codename": "change_report"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 116,
+    "fields": {
+        "name": "Can delete report",
+        "content_type": 37,
+        "codename": "delete_report"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 117,
+    "fields": {
+        "name": "Can add eic recommendation",
+        "content_type": 38,
+        "codename": "add_eicrecommendation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 118,
+    "fields": {
+        "name": "Can change eic recommendation",
+        "content_type": 38,
+        "codename": "change_eicrecommendation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 119,
+    "fields": {
+        "name": "Can delete eic recommendation",
+        "content_type": 38,
+        "codename": "delete_eicrecommendation"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 120,
+    "fields": {
+        "name": "Can add editorial assignment",
+        "content_type": 39,
+        "codename": "add_editorialassignment"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 121,
+    "fields": {
+        "name": "Can change editorial assignment",
+        "content_type": 39,
+        "codename": "change_editorialassignment"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 122,
+    "fields": {
+        "name": "Can delete editorial assignment",
+        "content_type": 39,
+        "codename": "delete_editorialassignment"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 123,
+    "fields": {
+        "name": "Can add thesis link",
+        "content_type": 40,
+        "codename": "add_thesislink"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 124,
+    "fields": {
+        "name": "Can change thesis link",
+        "content_type": 40,
+        "codename": "change_thesislink"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 125,
+    "fields": {
+        "name": "Can delete thesis link",
+        "content_type": 40,
+        "codename": "delete_thesislink"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 126,
+    "fields": {
+        "name": "Can vet registration requests",
+        "content_type": 26,
+        "codename": "can_vet_registration_requests"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 127,
+    "fields": {
+        "name": "Can draft registration invitations",
+        "content_type": 26,
+        "codename": "can_draft_registration_invitations"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 128,
+    "fields": {
+        "name": "Can manage registration invitations",
+        "content_type": 26,
+        "codename": "can_manage_registration_invitations"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 129,
+    "fields": {
+        "name": "Can invite Fellows",
+        "content_type": 26,
+        "codename": "can_invite_Fellows"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 130,
+    "fields": {
+        "name": "Can email group members",
+        "content_type": 26,
+        "codename": "can_email_group_members"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 131,
+    "fields": {
+        "name": "Can email particulars",
+        "content_type": 26,
+        "codename": "can_email_particulars"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 132,
+    "fields": {
+        "name": "Can view By-laws of Editorial College",
+        "content_type": 26,
+        "codename": "view_bylaws"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 133,
+    "fields": {
+        "name": "Can submit Comments",
+        "content_type": 26,
+        "codename": "can_submit_comments"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 134,
+    "fields": {
+        "name": "Can express opinion on Comments",
+        "content_type": 26,
+        "codename": "can_express_opinion_on_comments"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 135,
+    "fields": {
+        "name": "Can request opening of Commentara Pages",
+        "content_type": 26,
+        "codename": "can_request_commentary_pages"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 136,
+    "fields": {
+        "name": "Can request Thesis Links",
+        "content_type": 26,
+        "codename": "can_request_thesislinks"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 137,
+    "fields": {
+        "name": "Can vet Commentary page requests",
+        "content_type": 26,
+        "codename": "can_vet_commentary_requests"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 138,
+    "fields": {
+        "name": "Can vet Thesis Link requests",
+        "content_type": 26,
+        "codename": "can_vet_thesislink_requests"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 139,
+    "fields": {
+        "name": "Can vet Authorship claims",
+        "content_type": 26,
+        "codename": "can_vet_authorship_claims"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 140,
+    "fields": {
+        "name": "Can vet submitted Comments",
+        "content_type": 26,
+        "codename": "can_vet_comments"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 141,
+    "fields": {
+        "name": "Can submit manuscript",
+        "content_type": 26,
+        "codename": "can_submit_manuscript"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 142,
+    "fields": {
+        "name": "Can view Submissions Pool",
+        "content_type": 26,
+        "codename": "can_view_pool"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 143,
+    "fields": {
+        "name": "Can assign incoming Submissions to potential Editor-in-charge",
+        "content_type": 26,
+        "codename": "can_assign_submissions"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 144,
+    "fields": {
+        "name": "Can take charge (become Editor-in-charge) of submissions",
+        "content_type": 26,
+        "codename": "can_take_charge_of_submissions"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 145,
+    "fields": {
+        "name": "Can vet submitted Reports",
+        "content_type": 26,
+        "codename": "can_vet_submitted_reports"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 146,
+    "fields": {
+        "name": "Can act as a referee and submit reports on Submissions",
+        "content_type": 26,
+        "codename": "can_referee"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 147,
+    "fields": {
+        "name": "Can prepare recommendations for voting",
+        "content_type": 26,
+        "codename": "can_prepare_recommendations_for_voting"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 148,
+    "fields": {
+        "name": "Can fix the College voting decision",
+        "content_type": 26,
+        "codename": "can_fix_College_decision"
+    }
+},
+{
+    "model": "auth.permission",
+    "pk": 149,
+    "fields": {
+        "name": "Can publish accepted submission",
+        "content_type": 26,
+        "codename": "can_publish_accepted_submission"
+    }
+}
+]
diff --git a/scipost/forms.py b/scipost/forms.py
index 3716ad8d83f4800b3c1e0eed0756279746f16bda..f8d0629bd7684d0ae9d2ee559d420167aadaf496 100644
--- a/scipost/forms.py
+++ b/scipost/forms.py
@@ -11,6 +11,7 @@ from crispy_forms.helper import FormHelper
 from crispy_forms.layout import Layout, Div, Field, Fieldset, HTML, Submit
 
 from .models import *
+from .constants import SCIPOST_DISCIPLINES
 
 from journals.models import Publication
 from submissions.models import SUBMISSION_STATUS_PUBLICLY_UNLISTED
diff --git a/scipost/models.py b/scipost/models.py
index 2962da77cc4716fcf50d66db6668033d42deb937..033c08ec50202e4cc88e200a4db4f8ee523a5be2 100644
--- a/scipost/models.py
+++ b/scipost/models.py
@@ -10,129 +10,10 @@ from django.utils.safestring import mark_safe
 
 from django_countries.fields import CountryField
 
-from scipost.models import *
-
+from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\
+    disciplines_dict, subject_areas_dict
 
-SCIPOST_DISCIPLINES = (
-    ('physics', 'Physics'),
-    ('astrophysics', 'Astrophysics'),
-    ('mathematics', 'Mathematics'),
-    ('computerscience', 'Computer Science'),
-    )
-disciplines_dict = dict(SCIPOST_DISCIPLINES)
-
-SCIPOST_SUBJECT_AREAS = (
-    ('Physics', (
-        ('Phys:AE', 'Atomic, Molecular and Optical Physics - Experiment'),
-        ('Phys:AT', 'Atomic, Molecular and Optical Physics - Theory'),
-        ('Phys:BI', 'Biophysics'),
-        ('Phys:CE', 'Condensed Matter Physics - Experiment'),
-        ('Phys:CT', 'Condensed Matter Physics - Theory'),
-        ('Phys:FD', 'Fluid Dynamics'),
-        ('Phys:GR', 'Gravitation, Cosmology and Astroparticle Physics'),
-        ('Phys:HE', 'High-Energy Physics - Experiment'),
-        ('Phys:HT', 'High-Energy Physics- Theory'),
-        ('Phys:HP', 'High-Energy Physics - Phenomenology'),
-        ('Phys:MP', 'Mathematical Physics'),
-        ('Phys:NE', 'Nuclear Physics - Experiment'),
-        ('Phys:NT', 'Nuclear Physics - Theory'),
-        ('Phys:QP', 'Quantum Physics'),
-        ('Phys:SM', 'Statistical and Soft Matter Physics'),
-        )
-     ),
-    ('Astrophysics', (
-        ('Astro:GA', 'Astrophysics of Galaxies'),
-        ('Astro:CO', 'Cosmology and Nongalactic Astrophysics'),
-        ('Astro:EP', 'Earth and Planetary Astrophysics'),
-        ('Astro:HE', 'High Energy Astrophysical Phenomena'),
-        ('Astro:IM', 'Instrumentation and Methods for Astrophysics'),
-        ('Astro:SR', 'Solar and Stellar Astrophysics'),
-        )
-     ),
-    ('Mathematics', (
-        ('Math:AG', 'Algebraic Geometry'),
-        ('Math:AT', 'Algebraic Topology'),
-        ('Math:AP', 'Analysis of PDEs'),
-        ('Math:CT', 'Category Theory'),
-        ('Math:CA', 'Classical Analysis and ODEs'),
-        ('Math:CO', 'Combinatorics'),
-        ('Math:AC', 'Commutative Algebra'),
-        ('Math:CV', 'Complex Variables'),
-        ('Math:DG', 'Differential Geometry'),
-        ('Math:DS', 'Dynamical Systems'),
-        ('Math:FA', 'Functional Analysis'),
-        ('Math:GM', 'General Mathematics'),
-        ('Math:GN', 'General Topology'),
-        ('Math:GT', 'Geometric Topology'),
-        ('Math:GR', 'Group Theory'),
-        ('Math:HO', 'History and Overview'),
-        ('Math:IT', 'Information Theory'),
-        ('Math:KT', 'K-Theory and Homology'),
-        ('Math:LO', 'Logic'),
-        ('Math:MP', 'Mathematical Physics'),
-        ('Math:MG', 'Metric Geometry'),
-        ('Math:NT', 'Number Theory'),
-        ('Math:NA', 'Numerical Analysis'),
-        ('Math:OA', 'Operator Algebras'),
-        ('Math:OC', 'Optimization and Control'),
-        ('Math:PR', 'Probability'),
-        ('Math:QA', 'Quantum Algebra'),
-        ('Math:RT', 'Representation Theory'),
-        ('Math:RA', 'Rings and Algebras'),
-        ('Math:SP', 'Spectral Theory'),
-        ('Math:ST', 'Statistics Theory'),
-        ('Math:SG', 'Symplectic Geometry'),
-        )
-     ),
-    ('Computer Science', (
-        ('Comp:AI', 'Artificial Intelligence'),
-        ('Comp:CC', 'Computational Complexity'),
-        ('Comp:CE', 'Computational Engineering, Finance, and Science'),
-        ('Comp:CG', 'Computational Geometry'),
-        ('Comp:GT', 'Computer Science and Game Theory'),
-        ('Comp:CV', 'Computer Vision and Pattern Recognition'),
-        ('Comp:CY', 'Computers and Society'),
-        ('Comp:CR', 'Cryptography and Security'),
-        ('Comp:DS', 'Data Structures and Algorithms'),
-        ('Comp:DB', 'Databases'),
-        ('Comp:DL', 'Digital Libraries'),
-        ('Comp:DM', 'Discrete Mathematics'),
-        ('Comp:DC', 'Distributed, Parallel, and Cluster Computing'),
-        ('Comp:ET', 'Emerging Technologies'),
-        ('Comp:FL', 'Formal Languages and Automata Theory'),
-        ('Comp:GL', 'General Literature'),
-        ('Comp:GR', 'Graphics'),
-        ('Comp:AR', 'Hardware Architecture'),
-        ('Comp:HC', 'Human-Computer Interaction'),
-        ('Comp:IR', 'Information Retrieval'),
-        ('Comp:IT', 'Information Theory'),
-        ('Comp:LG', 'Learning'),
-        ('Comp:LO', 'Logic in Computer Science'),
-        ('Comp:MS', 'Mathematical Software'),
-        ('Comp:MA', 'Multiagent Systems'),
-        ('Comp:MM', 'Multimedia'),
-        ('Comp:NI', 'Networking and Internet Architecture'),
-        ('Comp:NE', 'Neural and Evolutionary Computing'),
-        ('Comp:NA', 'Numerical Analysis'),
-        ('Comp:OS', 'Operating Systems'),
-        ('Comp:OH', 'Other Computer Science'),
-        ('Comp:PF', 'Performance'),
-        ('Comp:PL', 'Programming Languages'),
-        ('Comp:RO', 'Robotics'),
-        ('Comp:SI', 'Social and Information Networks'),
-        ('Comp:SE', 'Software Engineering'),
-        ('Comp:SD', 'Sound'),
-        ('Comp:SC', 'Symbolic Computation'),
-        ('Comp:SY', 'Systems and Control'),
-        )
-     ),
-)
-subject_areas_raw_dict = dict(SCIPOST_SUBJECT_AREAS)
-
-# Make dict of the form {'Phys:AT': 'Atomic...', ...}
-subject_areas_dict = {}
-for k in subject_areas_raw_dict.keys():
-    subject_areas_dict.update(dict(subject_areas_raw_dict[k]))
+from scipost.models import *
 
 
 class ChoiceArrayField(ArrayField):
@@ -179,6 +60,19 @@ TITLE_CHOICES = (
 title_dict = dict(TITLE_CHOICES)
 
 
+class TimeStampedModel(models.Model):
+    """
+    All objects should inherit from this abstract model.
+    This will ensure the creation of created and modified
+    timestamps in the objects.
+    """
+    created = models.DateTimeField(auto_now_add=True)
+    latest_activity = models.DateTimeField(auto_now=True)
+
+    class Meta:
+        abstract = True
+
+
 class Contributor(models.Model):
     """
     All users of SciPost are Contributors.
@@ -212,7 +106,6 @@ class Contributor(models.Model):
         default=True,
         verbose_name="I accept to receive SciPost emails")
 
-
     def __str__(self):
         return '%s, %s' % (self.user.last_name, self.user.first_name)
 
diff --git a/submissions/models.py b/submissions/models.py
index 68a3d1e867b2720f97fd109dee8cd1d8a18d681a..2ea5a6c80cecdb9cdbc796f30b55e1ec87b17d8a 100644
--- a/submissions/models.py
+++ b/submissions/models.py
@@ -8,8 +8,8 @@ from django.template import Template, Context
 from .models import *
 
 from scipost.models import ChoiceArrayField, Contributor, title_dict, Remark
-from scipost.models import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS
-from scipost.models import subject_areas_dict, TITLE_CHOICES
+from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict
+from scipost.models import TITLE_CHOICES
 from journals.models import SCIPOST_JOURNALS_SUBMIT, SCIPOST_JOURNALS_DOMAINS
 from journals.models import SCIPOST_JOURNALS_SPECIALIZATIONS
 from journals.models import journals_submit_dict, journals_domains_dict, journals_spec_dict
diff --git a/theses/factories.py b/theses/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a12cd026941ad06ef7e4ad82c798b6947813a93
--- /dev/null
+++ b/theses/factories.py
@@ -0,0 +1,19 @@
+import factory
+from .models import ThesisLink
+from scipost.factories import ContributorFactory
+
+
+class ThesisLinkFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = ThesisLink
+
+    requested_by = factory.SubFactory(ContributorFactory)
+    type = ThesisLink.MASTER_THESIS
+    title = factory.Sequence(lambda n: "thesis {0}".format(n))
+    pub_link = factory.Faker('uri')
+    author = factory.Faker('name')
+    supervisor = factory.Faker('name')
+    institution = factory.Faker('company')
+    defense_date = factory.Faker('date_time_this_century')
+    abstract = factory.Faker('text')
+    domain = 'ET'
diff --git a/theses/forms.py b/theses/forms.py
index 95849d3b3d8bd0bb00ca8de031ee2326950a7651..13b4a1793a47b46f2e9c993e8c55c2d86ebf0834 100644
--- a/theses/forms.py
+++ b/theses/forms.py
@@ -1,6 +1,7 @@
 from django import forms
 
 from .models import *
+from .helpers import past_years
 
 THESIS_ACTION_CHOICES = (
     (0, 'modify'),
@@ -14,18 +15,18 @@ THESIS_REFUSAL_CHOICES = (
     (-2, 'the external link to this thesis does not work'),
     )
 
+
 class RequestThesisLinkForm(forms.ModelForm):
     class Meta:
         model = ThesisLink
         fields = ['type', 'discipline', 'domain', 'subject_area',
                   'title', 'author', 'supervisor', 'institution',
                   'defense_date', 'pub_link', 'abstract']
+        widgets = {
+            'defense_date': forms.SelectDateWidget(years=past_years(50)),
+            'pub_link': forms.TextInput(attrs={'placeholder': 'Full URL'})
+        }
 
-    def __init__(self, *args, **kwargs):
-        super(RequestThesisLinkForm, self).__init__(*args, **kwargs)
-        self.fields['defense_date'].widget.attrs.update({'placeholder': 'Format: YYYY-MM-DD'})
-        self.fields['pub_link'].widget.attrs.update({'placeholder': 'Full URL'})
-        self.fields['abstract'].widget.attrs.update({'cols': 100})
 
 class VetThesisLinkForm(forms.Form):
     action_option = forms.ChoiceField(widget=forms.RadioSelect,
@@ -35,6 +36,7 @@ class VetThesisLinkForm(forms.Form):
     email_response_field = forms.CharField(widget=forms.Textarea(
         attrs={'rows': 5, 'cols': 40}), label='Justification (optional)', required=False)
 
+
 class ThesisLinkSearchForm(forms.Form):
     author = forms.CharField(max_length=100, required=False, label="Author")
     title_keyword = forms.CharField(max_length=100, label="Title", required=False)
diff --git a/theses/helpers.py b/theses/helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b2961e45306298fd6544c8e515f7a8d22675579
--- /dev/null
+++ b/theses/helpers.py
@@ -0,0 +1,9 @@
+import datetime
+
+
+def past_years(n):
+    '''
+    Gives back list of integers representing a range of n years, counting down from current year.
+    '''
+    this_year = datetime.datetime.now().year
+    return range(this_year, this_year - n, -1)
diff --git a/theses/migrations/0006_auto_20161219_2012.py b/theses/migrations/0006_auto_20161219_2012.py
new file mode 100644
index 0000000000000000000000000000000000000000..935f906aacc44850699e1d60558ba9c3fd845f31
--- /dev/null
+++ b/theses/migrations/0006_auto_20161219_2012.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2016-12-19 19:12
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('theses', '0005_remove_thesislink_specialization'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='thesislink',
+            name='domain',
+            field=models.CharField(choices=[('E', 'Experimental'), ('T', 'Theoretical'), ('C', 'Computational'), ('ET', 'Exp. & Theor.'), ('EC', 'Exp. & Comp.'), ('TC', 'Theor. & Comp.'), ('ETC', 'Exp., Theor. & Comp.')], max_length=3),
+        ),
+    ]
diff --git a/theses/models.py b/theses/models.py
index 37fcb52f07fd7f09fc90c3a53982bd18c9d162e8..50ec6b1f47548e9f43d6f53d31d091883f4ed091 100644
--- a/theses/models.py
+++ b/theses/models.py
@@ -6,17 +6,21 @@ from django.template import Template, Context
 from .models import *
 
 from journals.models import *
+from scipost.constants import SCIPOST_DISCIPLINES, subject_areas_dict, disciplines_dict
 from scipost.models import *
 
-THESIS_TYPES = (
-    ('MA', 'Master\'s'),
-    ('PhD', 'Ph.D.'),
-    ('Hab', 'Habilitation'),
-    )
-thesis_type_dict = dict(THESIS_TYPES)
-
 
 class ThesisLink(models.Model):
+    MASTER_THESIS = 'MA'
+    PHD_THESIS = 'PhD'
+    HABILITATION_THESIS = 'Hab'
+    THESIS_TYPES = (
+        (MASTER_THESIS, 'Master\'s'),
+        (PHD_THESIS, 'Ph.D.'),
+        (HABILITATION_THESIS, 'Habilitation'),
+    )
+    THESIS_TYPES_DICT = dict(THESIS_TYPES)
+
     """ An URL pointing to a thesis """
     requested_by = models.ForeignKey(
         Contributor, blank=True, null=True,
@@ -26,13 +30,13 @@ class ThesisLink(models.Model):
     vetted_by = models.ForeignKey(
         Contributor, blank=True, null=True,
         on_delete=models.CASCADE)
-    type = models.CharField(max_length=3, choices=THESIS_TYPES)
+    type = models.CharField(choices=THESIS_TYPES, max_length=3)
     discipline = models.CharField(
         max_length=20, choices=SCIPOST_DISCIPLINES,
         default='physics')
     domain = models.CharField(
         max_length=3, choices=SCIPOST_JOURNALS_DOMAINS,
-        blank=True)
+        blank=False)
     subject_area = models.CharField(
         max_length=10,
         choices=SCIPOST_SUBJECT_AREAS,
@@ -85,7 +89,7 @@ class ThesisLink(models.Model):
             header += '<td>(not claimed)</td>'
         header += (
             '</tr>'
-            '<tr><td>Type: </td><td></td><td>' + thesis_type_dict[self.type] +
+            '<tr><td>Type: </td><td></td><td>' + self.THESIS_TYPES_DICT[self.type] +
             '</td></tr>'
             '<tr><td>Discipline: </td><td></td><td>' +
             disciplines_dict[self.discipline] + '</td></tr>'
@@ -110,11 +114,13 @@ class ThesisLink(models.Model):
             'pub_link': self.pub_link, 'institution': self.institution,
             'supervisor': self.supervisor, 'defense_date': self.defense_date,
             'latest_activity': self.latest_activity.strftime('%Y-%m-%d %H:%M')})
+        print(subject_areas_dict)
+        print(self.subject_area in subject_areas_dict)
         header = (
             '<li><div class="flex-container">'
             '<div class="flex-whitebox0"><p><a href="/thesis/{{ id }}" '
             'class="pubtitleli">{{ title }}</a></p>'
-            '<p>' + thesis_type_dict[self.type] + ' thesis by {{ author }} '
+            '<p>' + self.THESIS_TYPES_DICT[self.type] + ' thesis by {{ author }} '
             '(supervisor(s): {{ supervisor }}) in ' +
             disciplines_dict[self.discipline] + ', ' +
             journals_domains_dict[self.domain] + ' ' +
@@ -133,7 +139,7 @@ class ThesisLink(models.Model):
             '<li><div class="flex-container">'
             '<div class="flex-whitebox0"><p><a href="/thesis/{{ id }}" '
             'class="pubtitleli">{{ title }}</a></p>'
-            '<p>' + thesis_type_dict[self.type] +
+            '<p>' + self.THESIS_TYPES_DICT[self.type] +
             ' thesis by {{ author }} </div></div></li>')
         template = Template(header)
         return template.render(context)
diff --git a/theses/test_forms.py b/theses/test_forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce07aa6214b9811d4b0ca54e09c7b5b304b93901
--- /dev/null
+++ b/theses/test_forms.py
@@ -0,0 +1,26 @@
+import factory
+
+from django.test import TestCase
+
+from .factories import ThesisLinkFactory
+from .forms import RequestThesisLinkForm
+from common.helpers import model_form_data
+
+
+class TestRequestThesisLink(TestCase):
+    fixtures = ['permissions', 'groups']
+
+    def setUp(self):
+        self.valid_form_data = model_form_data(ThesisLinkFactory(), RequestThesisLinkForm)
+
+    def test_valid_data_is_valid(self):
+        form_data = self.valid_form_data
+        form = RequestThesisLinkForm(self.valid_form_data)
+        self.assertTrue(form.is_valid())
+
+    def test_empty_domain_is_invalid(self):
+        form_data = self.valid_form_data
+        form_data['domain'] = ''
+        form = RequestThesisLinkForm(form_data)
+        form.is_valid()
+        self.assertEqual(form.errors['domain'], ['This field is required.'])
diff --git a/theses/test_models.py b/theses/test_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..4308ab25f1d22c56e33cb5047cb6f571fe080bf7
--- /dev/null
+++ b/theses/test_models.py
@@ -0,0 +1,15 @@
+import re
+
+from django.test import TestCase
+from django.core.exceptions import ValidationError
+
+from .models import ThesisLink
+from .factories import ThesisLinkFactory
+
+
+class ThesisLinkTestCase(TestCase):
+    def test_domain_cannot_be_blank(self):
+        thesis_link = ThesisLinkFactory()
+        thesis_link.domain = ""
+        self.assertRaisesRegexp(ValidationError, re.compile(r'domain'),
+                                thesis_link.full_clean)
diff --git a/theses/test_views.py b/theses/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..7e70a6dd41170d221dfd8a4d586a4a8b0ff0c38c
--- /dev/null
+++ b/theses/test_views.py
@@ -0,0 +1,42 @@
+from django.test import TestCase, RequestFactory
+from django.test.client import Client
+from django.contrib.auth.models import AnonymousUser
+from django.urls import reverse
+
+from .views import RequestThesisLink
+from scipost.factories import UserFactory
+from .factories import ThesisLinkFactory
+from .models import ThesisLink
+
+
+class TestThesisDetail(TestCase):
+    fixtures = ['groups', 'permissions']
+
+    def test_visits_valid_thesis_detail(self):
+        thesis_link = ThesisLinkFactory()
+        client = Client()
+        target = reverse('theses:thesis', kwargs={'thesislink_id': thesis_link.id})
+        response = client.post(target)
+        self.assertEqual(response.status_code, 200)
+
+
+class TestRequestThesisLink(TestCase):
+    fixtures = ['groups', 'permissions']
+
+    def setUp(self):
+        self.client = Client()
+
+    def test_response_when_not_logged_in(self):
+        '''A visitor that is not logged in cannot view this page.'''
+        response = self.client.get(reverse('theses:request_thesislink'))
+        self.assertEqual(response.status_code, 403)
+
+    def test_response_when_logged_in(self):
+        request = RequestFactory().get(reverse('theses:request_thesislink'))
+        request.user = UserFactory()
+        response = RequestThesisLink.as_view()(request)
+        self.assertEqual(response.status_code, 200)
+
+    def test_redirects_to_acknowledgement_page(self):
+        response = self.client.post(reverse('theses:request_thesislink'), {}, follow=True)
+        self.assertRedirects(response, reverse('scipost:acknowledgement'))
diff --git a/theses/urls.py b/theses/urls.py
index 59758c05e57add2fa5e09e8fdcca125810b36e36..05839aa7550bedf319b58d82bb3e7aa59703cacf 100644
--- a/theses/urls.py
+++ b/theses/urls.py
@@ -7,9 +7,10 @@ urlpatterns = [
     # Thesis Links
     url(r'^$', views.theses, name='theses'),
     url(r'^browse/(?P<discipline>[a-z]+)/(?P<nrweeksback>[0-9]+)/$', views.browse, name='browse'),
-    #url(r'^thesis/(?P<thesislink_id>[0-9]+)/$', views.thesis_detail, name='thesis'),
     url(r'^(?P<thesislink_id>[0-9]+)/$', views.thesis_detail, name='thesis'),
-    url(r'^request_thesislink$', views.request_thesislink, name='request_thesislink'),
-    url(r'^vet_thesislink_requests$', views.vet_thesislink_requests, name='vet_thesislink_requests'),
-    url(r'^vet_thesislink_request_ack/(?P<thesislink_id>[0-9]+)$', views.vet_thesislink_request_ack, name='vet_thesislink_request_ack'),
+    url(r'^request_thesislink$', views.RequestThesisLink.as_view(), name='request_thesislink'),
+    url(r'^vet_thesislink_requests$', views.vet_thesislink_requests,
+        name='vet_thesislink_requests'),
+    url(r'^vet_thesislink_request_ack/(?P<thesislink_id>[0-9]+)$',
+        views.vet_thesislink_request_ack, name='vet_thesislink_request_ack'),
 ]
diff --git a/theses/views.py b/theses/views.py
index 882c2f192f88353debfe4235bbcf6154ab763b27..7e738e39d719d57235c783350d1a1d2b4c6cb814 100644
--- a/theses/views.py
+++ b/theses/views.py
@@ -1,4 +1,5 @@
 import datetime
+
 from django.utils import timezone
 from django.shortcuts import get_object_or_404, render
 from django.contrib.auth import authenticate, login, logout
@@ -9,6 +10,8 @@ from django.core.urlresolvers import reverse
 from django.http import HttpResponse, HttpResponseRedirect
 from django.views.decorators.csrf import csrf_protect
 from django.db.models import Avg
+from django.views.generic.edit import CreateView
+from django.utils.decorators import method_decorator
 
 from .models import *
 from .forms import *
@@ -17,55 +20,40 @@ from comments.models import Comment
 from comments.forms import CommentForm
 from scipost.forms import TITLE_CHOICES, AuthenticationForm
 
-title_dict = dict(TITLE_CHOICES) # Convert titles for use in emails
+
+title_dict = dict(TITLE_CHOICES)  # Convert titles for use in emails
 
 ################
 # Theses
 ################
 
 
-@permission_required('scipost.can_request_thesislinks', raise_exception=True)
-def request_thesislink(request):
-    if request.method == 'POST':
-        form = RequestThesisLinkForm(request.POST)
-        if form.is_valid():
-            contributor = Contributor.objects.get(user=request.user)
-            thesislink = ThesisLink (
-                requested_by = contributor,
-                type = form.cleaned_data['type'],
-                discipline = form.cleaned_data['discipline'],
-                domain = form.cleaned_data['domain'],
-                subject_area = form.cleaned_data['subject_area'],
-                title = form.cleaned_data['title'],
-                author = form.cleaned_data['author'],
-                supervisor = form.cleaned_data['supervisor'],
-                institution = form.cleaned_data['institution'],
-                defense_date = form.cleaned_data['defense_date'],
-                pub_link = form.cleaned_data['pub_link'],
-                abstract = form.cleaned_data['abstract'],
-                latest_activity = timezone.now(),
-                )
-            thesislink.save()
-            #return HttpResponseRedirect('request_thesislink_ack')
-            context = {'ack_header': 'Thank you for your request for a Thesis Link',
-                       'ack_message': 'Your request will soon be handled by an Editor. ',
-                       'followup_message': 'Return to your ',
-                       'followup_link': reverse('scipost:personal_page'),
-                       'followup_link_label': 'personal page'}
-            return render(request, 'scipost/acknowledgement.html', context)
-    else:
-        form = RequestThesisLinkForm()
-    return render(request, 'theses/request_thesislink.html', {'form': form})
+@method_decorator(permission_required(
+    'scipost.can_request_thesislinks', raise_exception=True), name='dispatch')
+class RequestThesisLink(CreateView):
+    form_class = RequestThesisLinkForm
+    template_name = 'theses/request_thesislink.html'
+    success_url = ''
+
+    def form_valid(self, form):
+        context = {'ack_header': 'Thank you for your request for a Thesis Link',
+                   'ack_message': 'Your request will soon be handled by an Editor. ',
+                   'followup_message': 'Return to your ',
+                   'followup_link': reverse('scipost:personal_page'),
+                   'followup_link_label': 'personal page'}
+        return render(self.request, 'scipost/acknowledgement.html', context)
 
 
 @permission_required('scipost.can_vet_thesislink_requests', raise_exception=True)
 def vet_thesislink_requests(request):
     contributor = Contributor.objects.get(user=request.user)
-    thesislink_to_vet = ThesisLink.objects.filter(vetted=False).first() # only handle one at a time
+    thesislink_to_vet = ThesisLink.objects.filter(
+        vetted=False).first()  # only handle one at a time
     form = VetThesisLinkForm()
-    context = {'contributor': contributor, 'thesislink_to_vet': thesislink_to_vet, 'form': form }
+    context = {'contributor': contributor, 'thesislink_to_vet': thesislink_to_vet, 'form': form}
     return render(request, 'theses/vet_thesislink_requests.html', context)
 
+
 @permission_required('scipost.can_vet_thesislink_requests', raise_exception=True)
 def vet_thesislink_request_ack(request, thesislink_id):
     if request.method == 'POST':
@@ -112,8 +100,8 @@ def vet_thesislink_request_ack(request, thesislink_id):
                                             ['theses@scipost.org'],
                                             reply_to=['theses@scipost.org'])
                 # Don't send email yet... only when option 1 has succeeded!
-                #emailmessage.send(fail_silently=False)
-                context = {'form': form2 }
+                # emailmessage.send(fail_silently=False)
+                context = {'form': form2}
                 return render(request, 'theses/request_thesislink.html', context)
             elif form.cleaned_data['action_option'] == '2':
                 email_text = ('Dear ' + title_dict[thesislink.requested_by.title] + ' '
@@ -124,7 +112,8 @@ def vet_thesislink_request_ack(request, thesislink_id):
                               + form.cleaned_data['refusal_reason']
                               + '.\n\nThank you for your interest, \nThe SciPost Team.')
                 if form.cleaned_data['email_response_field']:
-                    email_text += '\n\nFurther explanations: ' + form.cleaned_data['email_response_field']
+                    email_text += '\n\nFurther explanations: ' + \
+                        form.cleaned_data['email_response_field']
                 emailmessage = EmailMessage('SciPost Thesis Link', email_text,
                                             'SciPost Theses <theses@scipost.org>',
                                             [thesislink.requested_by.user.email],
@@ -133,8 +122,6 @@ def vet_thesislink_request_ack(request, thesislink_id):
                 emailmessage.send(fail_silently=False)
                 thesislink.delete()
 
-    #context = {'thesislink_id': thesislink_id }
-    #return render(request, 'theses/vet_thesislink_request_ack.html', context)
     context = {'ack_header': 'Thesis Link request vetted.',
                'followup_message': 'Return to the ',
                'followup_link': reverse('theses:vet_thesislink_requests'),
@@ -152,7 +139,7 @@ def theses(request):
                 abstract__icontains=form.cleaned_data['abstract_keyword'],
                 supervisor__icontains=form.cleaned_data['supervisor'],
                 vetted=True,
-                )
+            )
             thesislink_search_list.order_by('-pub_date')
         else:
             thesislink_search_list = []
@@ -165,7 +152,7 @@ def theses(request):
                               .filter(vetted=True,
                                       latest_activity__gte=timezone.now() + datetime.timedelta(days=-7)))
     context = {'form': form, 'thesislink_search_list': thesislink_search_list,
-               'thesislink_recent_list': thesislink_recent_list }
+               'thesislink_recent_list': thesislink_recent_list}
     return render(request, 'theses/theses.html', context)
 
 
@@ -179,11 +166,11 @@ def browse(request, discipline, nrweeksback):
                 abstract__icontains=form.cleaned_data['abstract_keyword'],
                 supervisor__icontains=form.cleaned_data['supervisor'],
                 vetted=True,
-                )
+            )
             thesislink_search_list.order_by('-pub_date')
         else:
             thesislink_search_list = []
-        context = {'form': form, 'thesislink_search_list': thesislink_search_list }
+        context = {'form': form, 'thesislink_search_list': thesislink_search_list}
         return HttpResponseRedirect(request, 'theses/theses.html', context)
     else:
         form = ThesisLinkSearchForm()
@@ -192,7 +179,7 @@ def browse(request, discipline, nrweeksback):
         latest_activity__gte=timezone.now() + datetime.timedelta(weeks=-int(nrweeksback))))
     context = {'form': form, 'discipline': discipline,
                'nrweeksback': nrweeksback,
-               'thesislink_browse_list': thesislink_browse_list }
+               'thesislink_browse_list': thesislink_browse_list}
     return render(request, 'theses/theses.html', context)
 
 
@@ -203,26 +190,27 @@ def thesis_detail(request, thesislink_id):
         form = CommentForm(request.POST)
         if form.is_valid():
             author = Contributor.objects.get(user=request.user)
-            newcomment = Comment (
-                thesislink = thesislink,
-                author = author,
-                is_rem = form.cleaned_data['is_rem'],
-                is_que = form.cleaned_data['is_que'],
-                is_ans = form.cleaned_data['is_ans'],
-                is_obj = form.cleaned_data['is_obj'],
-                is_rep = form.cleaned_data['is_rep'],
-                is_val = form.cleaned_data['is_val'],
-                is_lit = form.cleaned_data['is_lit'],
-                is_sug = form.cleaned_data['is_sug'],
-                comment_text = form.cleaned_data['comment_text'],
-                remarks_for_editors = form.cleaned_data['remarks_for_editors'],
-                date_submitted = timezone.now(),
-                )
-            newcomment.save()
+            new_comment = Comment(
+                thesislink=thesislink,
+                author=author,
+                is_rem=form.cleaned_data['is_rem'],
+                is_que=form.cleaned_data['is_que'],
+                is_ans=form.cleaned_data['is_ans'],
+                is_obj=form.cleaned_data['is_obj'],
+                is_rep=form.cleaned_data['is_rep'],
+                is_val=form.cleaned_data['is_val'],
+                is_lit=form.cleaned_data['is_lit'],
+                is_sug=form.cleaned_data['is_sug'],
+                comment_text=form.cleaned_data['comment_text'],
+                remarks_for_editors=form.cleaned_data['remarks_for_editors'],
+                date_submitted=timezone.now(),
+            )
+            new_comment.save()
             author.nr_comments = Comment.objects.filter(author=author).count()
             author.save()
             request.session['thesislink_id'] = thesislink_id
-            return HttpResponseRedirect(reverse('comments:comment_submission_ack'))
+            context = {}
+            return render(request, 'scipost/acknowledgement.html', context)
     else:
         form = CommentForm()