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/scipost/factories.py b/scipost/factories.py
index 80a4c045804aa2cf133823d882323c7d20013927..c69b975f60db3f7f1b5952012384a719ecc7c9bb 100644
--- a/scipost/factories.py
+++ b/scipost/factories.py
@@ -11,9 +11,9 @@ class ContributorFactory(factory.django.DjangoModelFactory):
         model = Contributor
 
     title = "MR"
-    user = factory.SubFactory(UserFactory, contributor=None)
+    user = factory.SubFactory('scipost.factories.UserFactory', contributor=None)
     status = 1
-    vetted_by = factory.SubFactory(ContributorFactory)
+    vetted_by = factory.SubFactory('scipost.factories.ContributorFactory', vetted_by=None)
 
 
 class UserFactory(factory.django.DjangoModelFactory):
diff --git a/theses/factories.py b/theses/factories.py
index 19489c3c15babf86f0939a1a261c234fb7285560..1a12cd026941ad06ef7e4ad82c798b6947813a93 100644
--- a/theses/factories.py
+++ b/theses/factories.py
@@ -16,3 +16,4 @@ class ThesisLinkFactory(factory.django.DjangoModelFactory):
     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 2193a767c32084fbfdd4f5e7b34143408a68e58d..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'),
@@ -21,12 +22,10 @@ class RequestThesisLinkForm(forms.ModelForm):
         fields = ['type', 'discipline', 'domain', 'subject_area',
                   'title', 'author', 'supervisor', 'institution',
                   'defense_date', 'pub_link', 'abstract']
-
-    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})
+        widgets = {
+            'defense_date': forms.SelectDateWidget(years=past_years(50)),
+            'pub_link': forms.TextInput(attrs={'placeholder': 'Full URL'})
+        }
 
 
 class VetThesisLinkForm(forms.Form):
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/test_forms.py b/theses/test_forms.py
index 2e9cb5f6ba351402af656aec1be5d9ac257bc5c0..ce07aa6214b9811d4b0ca54e09c7b5b304b93901 100644
--- a/theses/test_forms.py
+++ b/theses/test_forms.py
@@ -1 +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_views.py b/theses/test_views.py
index e1c9c7982e1cd36b83d213a82f3794ec860a05f5..7e70a6dd41170d221dfd8a4d586a4a8b0ff0c38c 100644
--- a/theses/test_views.py
+++ b/theses/test_views.py
@@ -1,10 +1,42 @@
-from django.test import TestCase
+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_acknowledges_after_submitting_comment(self):
+    def test_visits_valid_thesis_detail(self):
+        thesis_link = ThesisLinkFactory()
         client = Client()
-        response = client.post('/theses/1')
-        self.assertEqual(response.get('location'), 'bladiebla')
+        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 88bd434545a5636f9b09e832bbd7e24b91bf6d12..05839aa7550bedf319b58d82bb3e7aa59703cacf 100644
--- a/theses/urls.py
+++ b/theses/urls.py
@@ -8,7 +8,7 @@ urlpatterns = [
     url(r'^$', views.theses, name='theses'),
     url(r'^browse/(?P<discipline>[a-z]+)/(?P<nrweeksback>[0-9]+)/$', views.browse, name='browse'),
     url(r'^(?P<thesislink_id>[0-9]+)/$', views.thesis_detail, name='thesis'),
-    url(r'^request_thesislink$', views.request_thesislink, name='request_thesislink'),
+    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]+)$',
diff --git a/theses/views.py b/theses/views.py
index 7a2a2309d363eae341a18e0a62c4470c6f5b1cdf..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 *
@@ -25,38 +28,20 @@ title_dict = dict(TITLE_CHOICES)  # Convert titles for use in emails
 ################
 
 
-@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)