Commit e25c779d authored by smilin_desperado's avatar smilin_desperado

Rewrite to rest api with vuejs frontend

parent 2aabf1d1
......@@ -7,11 +7,14 @@ name = "pypi"
[packages]
django = "*"
Django = "*"
django-widget-tweaks = "*"
"psycopg2-binary" = "*"
djangorestframework = "*"
django-cors-headers = "*"
djangorestframework-jwt = "*"
[dev-packages]
django-debug-toolbar = "*"
"flake8" = "*"
{
"_meta": {
"hash": {
"sha256": "47d1394146f3ca4ca58edd6e6eb5fbe0443e0df9529dc6a96d75d9cd5a9575c0"
"sha256": "eb5a1f02484eea5a25be36cb6876eecbb1ba31c7fb26c7eecdb7e8d21c4b3786"
},
"pipfile-spec": 6,
"requires": {},
......@@ -16,11 +16,19 @@
"default": {
"django": {
"hashes": [
"sha256:2d8b9eed8815f172a8e898678ae4289a5e9176bc08295676eff4228dd638ea61",
"sha256:d81a1652963c81488e709729a80b510394050e312f386037f26b54912a3a10d0"
"sha256:3eb25c99df1523446ec2dc1b00e25eb2ecbdf42c9d8b0b8b32a204a8db9011f8",
"sha256:69ff89fa3c3a8337015478a1a0744f52a9fef5d12c1efa01a01f99bcce9bf10c"
],
"index": "pypi",
"version": "==2.0.4"
"version": "==2.0.6"
},
"django-cors-headers": {
"hashes": [
"sha256:0e9532628b3aa8806442d4d0b15e56112e6cfbef3735e13401935c98b842a2b4",
"sha256:c7ec4816ec49416517b84f317499d1519db62125471922ab78d670474ed9b987"
],
"index": "pypi",
"version": "==2.2.0"
},
"django-widget-tweaks": {
"hashes": [
......@@ -30,6 +38,22 @@
"index": "pypi",
"version": "==1.4.2"
},
"djangorestframework": {
"hashes": [
"sha256:b6714c3e4b0f8d524f193c91ecf5f5450092c2145439ac2769711f7eba89a9d9",
"sha256:c375e4f95a3a64fccac412e36fb42ba36881e52313ec021ef410b40f67cddca4"
],
"index": "pypi",
"version": "==3.8.2"
},
"djangorestframework-jwt": {
"hashes": [
"sha256:5efe33032f3a4518a300dc51a51c92145ad95fb6f4b272e5aa24701db67936a7",
"sha256:ab15dfbbe535eede8e2e53adaf52ef0cf018ee27dbfad10cbc4cbec2ab63d38c"
],
"index": "pypi",
"version": "==1.11.0"
},
"psycopg2-binary": {
"hashes": [
"sha256:02eb674e3d5810e19b4d5d00720b17130e182da1ba259dda608aaf33d787347d",
......@@ -63,6 +87,13 @@
"index": "pypi",
"version": "==2.7.4"
},
"pyjwt": {
"hashes": [
"sha256:30b1380ff43b55441283cc2b2676b755cca45693ae3097325dea01f3d110628c",
"sha256:4ee413b357d53fd3fb44704577afac88e72e878716116270d722723d65b42176"
],
"version": "==1.6.4"
},
"pytz": {
"hashes": [
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
......@@ -72,35 +103,34 @@
}
},
"develop": {
"django": {
"flake8": {
"hashes": [
"sha256:2d8b9eed8815f172a8e898678ae4289a5e9176bc08295676eff4228dd638ea61",
"sha256:d81a1652963c81488e709729a80b510394050e312f386037f26b54912a3a10d0"
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
],
"index": "pypi",
"version": "==2.0.4"
"version": "==3.5.0"
},
"django-debug-toolbar": {
"mccabe": {
"hashes": [
"sha256:4af2a4e1e932dadbda197b18585962d4fc20172b4e5a479490bc659fe998864d",
"sha256:d9ea75659f76d8f1e3eb8f390b47fc5bad0908d949c34a8a3c4c87978eb40a0f"
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"index": "pypi",
"version": "==1.9.1"
"version": "==0.6.1"
},
"pytz": {
"pycodestyle": {
"hashes": [
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
],
"version": "==2018.4"
"version": "==2.3.1"
},
"sqlparse": {
"pyflakes": {
"hashes": [
"sha256:ce028444cfab83be538752a2ffdb56bc417b7784ff35bb9a3062413717807dec",
"sha256:d9cf190f51cbb26da0412247dfe4fb5f4098edb73db84e02f9fc21fdca31fed4"
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
],
"version": "==0.2.4"
"version": "==1.6.0"
}
}
}
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'
from django.db import models
# Create your models here.
{% extends 'base.html' %}
{% load widget_tweaks %}
{% block title %}Login{% endblock %}
{% block pagecontent %}
<style>
.login {
max-width: 500px;
}
</style>
<div class="container p-5 login">
{% if form.errors %}
<p class="text-danger font-weight-bold">Your username and password didn't match. Please try again</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p class="text-danger">Your account doesn't have access to this page. To proceed,
please login with an account that has access</p>
{% else %}
<p class="text-danger font-weight-bold">Please login to see this page</p>
{% endif %}
{% endif %}
<h2 class="">Job Application Tracker</h2>
<form method='post' action="{% url 'accounts:login' %}">
{% csrf_token %}
<div class="form-group">
{{ form.username.label_tag }}
{{ form.username|add_class:"form-control" }}
</div>
<div class="form-group">
{{ form.password.label_tag }}
{{ form.password|add_class:"form-control" }}
</div>
<input type="submit" value="Log in" class="btn btn-primary btn-lg btn-block"/>
<input type="hidden" name="next" value="{{ next }}"/>
</form>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block title %}Thanks{% endblock %}
{% block pagecontent %}
<div class="container p-5">
<h2>Thanks for using this job application tracker</h2>
<p>Click <a href="{% url 'accounts:login' %}">here</a> to login again</p>
</div>
{% endblock %}
from django.test import TestCase
from django.test.utils import setup_test_environment
from django.urls import reverse
class LoginTests(TestCase):
def test_unauthenticated_user_redirects_to_login(self):
response = self.client.get('/', follow=True)
html = response.content.decode('utf8')
self.assertRedirects(response, '/accounts/login/?next=/')
self.assertIn('Please login to see this page', html)
self.assertIn('<title>Login | JAT</title>', html)
def test_login_empty_submit(self):
response = self.client.post('/accounts/login/', {})
html = response.content.decode('utf8')
self.assertIn("Your username and password didn't match. Please try again", html)
def test_login_incorrect_details(self):
response = self.client.post('/accounts/login/', {'username': 'abc', 'password': 'badpass'})
html = response.content.decode('utf8')
self.assertIn("Your username and password didn't match. Please try again", html)
from django.urls import path
from django.contrib.auth.views import LoginView, LogoutView
app_name = 'accounts'
urlpatterns = [
path('login/', LoginView.as_view(template_name="accounts/login.html"), name='login'),
path('logout/', LogoutView.as_view(template_name="accounts/logout.html"), name='logout'),
]
from django.shortcuts import render
# Create your views here.
from django.contrib import admin
from .models import Job, Note
from .models import Job
admin.site.register(Job)
admin.site.register(Note)
from django.apps import AppConfig
class TrackerConfig(AppConfig):
name = 'tracker'
class ApiConfig(AppConfig):
name = 'api'
def ready(self):
import tracker.signals
import api.signals
# Generated by Django 2.0.3 on 2018-03-30 06:14
# Generated by Django 2.0.6 on 2018-06-10 09:25
from django.conf import settings
from django.db import migrations, models
......@@ -14,6 +14,14 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='DataPoint',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change_to', models.CharField(choices=[('1', 'Researching'), ('2', 'Applied'), ('3', 'Interviewed For'), ('4', 'Offer Made'), ('5', 'Rejected')], default='1', max_length=50)),
('change_date', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Job',
fields=[
......@@ -30,7 +38,7 @@ class Migration(migrations.Migration):
('date_applied', models.DateField(blank=True, null=True)),
('date_added', models.DateTimeField(auto_now_add=True)),
('date_modified', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='applications', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['status'],
......@@ -43,7 +51,12 @@ class Migration(migrations.Migration):
('text', models.TextField()),
('date_added', models.DateTimeField(auto_now_add=True)),
('date_modified', models.DateTimeField(auto_now=True)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tracker.Job')),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job')),
],
),
migrations.AddField(
model_name='datapoint',
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job'),
),
]
# Generated by Django 2.0.3 on 2018-04-09 11:25
# Generated by Django 2.0.6 on 2018-06-10 09:26
from django.db import migrations
......@@ -6,12 +6,15 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tracker', '0002_datapoint'),
('api', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='datapoint',
name='change_from',
model_name='note',
name='job',
),
migrations.DeleteModel(
name='Note',
),
]
......@@ -9,11 +9,13 @@ APPLICATION_STATUS = (
("5", 'Rejected'),
)
class Job(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="applications")
user = models.ForeignKey(User, on_delete=models.CASCADE,
related_name="applications", null=True)
position_title = models.CharField(max_length=100)
company = models.CharField(max_length=100, blank=True)
salary = models.IntegerField(blank=True, null=True)
salary = models.IntegerField(blank=True, null=True)
url = models.URLField(max_length=1000, blank=True)
contact_person = models.CharField(max_length=100, blank=True)
contact_email = models.EmailField(blank=True)
......@@ -26,21 +28,12 @@ class Job(models.Model):
def __str__(self):
return self.position_title
class Meta:
ordering = ['status',]
class Note(models.Model):
text = models.TextField()
job = models.ForeignKey(Job, on_delete=models.CASCADE)
date_added = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['status', ]
def __str__(self):
return self.text[:10]
class DataPoint(models.Model):
job = models.ForeignKey(Job, on_delete=models.CASCADE)
change_to = models.CharField(max_length=50, choices=APPLICATION_STATUS, default='1')
change_date = models.DateTimeField(auto_now_add=True)
from rest_framework import serializers
from .models import Job
class JobSerialiser(serializers.ModelSerializer):
class Meta:
model = Job
fields = '__all__'
......@@ -2,17 +2,18 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Job, DataPoint
@receiver(post_save, sender=Job)
def job_handler(sender, instance, **kwargs):
try:
dp = DataPoint.objects.filter(job=instance.id).order_by('-change_date')[0]
if dp.change_to != instance.status:
dp = DataPoint()
dp.job = instance
dp.change_to = instance.status
dp.save()
except IndexError:
dp = DataPoint()
dp.job = instance
dp.change_to = instance.status
dp.save()
try:
dp = DataPoint.objects.filter(job=instance.id).order_by('-change_date')[0]
if dp.change_to != instance.status:
dp = DataPoint()
dp.job = instance
dp.change_to = instance.status
dp.save()
except IndexError:
dp = DataPoint()
dp.job = instance
dp.change_to = instance.status
dp.save()
from django.urls import reverse
from django.contrib.auth.models import User
from rest_framework.test import APITestCase
from rest_framework_jwt.settings import api_settings
from rest_framework.status import (
HTTP_200_OK,
HTTP_201_CREATED,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
)
from .serialisers import JobSerialiser
from .models import Job, APPLICATION_STATUS
def get_jwt(user):
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
def deserialiseJob(data, many=False):
serialiser = JobSerialiser(data=data, many=many)
assert serialiser.is_valid()
return serialiser.save()
class TrackerTests(APITestCase):
def login(self, user='test_user'):
user = User.objects.get_or_create(username=user)[0]
self.client.force_authenticate(user)
self.client.credentials(Authorization='JWT ' + get_jwt(user))
return user
def test_add_job_succeeds(self):
"""
Job can be added successfully
"""
self.login()
url = reverse('jobs-list')
data = {'position_title': 'test job'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, HTTP_201_CREATED)
self.assertEqual(Job.objects.count(), 1)
self.assertEqual(Job.objects.get().position_title, 'test job')
def test_get_status_options(self):
"""
Get available job status options
"""
self.login()
url = reverse('jobs-list')
response = self.client.options(url)
self.assertEqual(response.status_code, HTTP_200_OK)
choices = response.data['actions']['POST']['status']['choices']
self.assertEqual(len(choices), len(APPLICATION_STATUS))
def test_fetch_all(self):
"""
Get all jobs
"""
user = self.login()
job1 = Job(position_title='dummy job 1', user=user)
job2 = Job(position_title='dummy job 2', user=user)
job1.save()
job2.save()
url = reverse('jobs-list')
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_200_OK)
entities = deserialiseJob(response.data, many=True)
self.assertEqual(len(entities), 2)
def test_fetch_one_job(self):
"""
Can fetch a single job by id
"""
user = self.login()
job = Job(position_title='another test job', user=user)
job.save()
url = reverse('jobs-detail', kwargs={'pk': job.id})
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_200_OK)
new_job = deserialiseJob(response.data)
self.assertEqual(new_job.position_title, 'another test job')
self.assertEqual(new_job.user, user)
def test_delete_job(self):
"""
Can delete a job by id
"""
user = self.login()
job = Job(position_title='test position', user=user)
job.save()
url = reverse('jobs-detail', kwargs={'pk': job.id})
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_200_OK)
response = self.client.delete(url)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND)
def test_update_job(self):
"""
Update a job
"""
user = self.login()
job = Job(position_title='test position', user=user)
job.save()
url = reverse('jobs-detail', kwargs={'pk': job.id})
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_200_OK)
job = deserialiseJob(response.data)
job.position_title = 'updated position'
serialiser = JobSerialiser(job)
response = self.client.put(url, serialiser.data)
self.assertEqual(response.status_code, HTTP_200_OK)
updated = deserialiseJob(response.data)
self.assertEqual(updated.position_title, 'updated position')
class AuthAndPermTests(APITestCase):
def test_incorrect_auth_fails(self):
"""
Authenticate with incorrect credentials fails
"""
url = reverse('jwt_login')
data = {
'username': 'user',
'password': 'inCorrectPassword1'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
error = response.data['non_field_errors'][0]
self.assertIn('Unable to log in with provided credentials', error)
def test_authenticate(self):
"""
Authenticate with correct credentials
"""
User.objects.create_user(username='test_user',
password='correctPassword1')
url = reverse('jwt_login')
data = {
'username': 'test_user',
'password': 'correctPassword1'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertIsNotNone(response.data['token'])
def test_token_refresh(self):
"""
Token refresh succeeds
"""
correct_user = User.objects.create_user(username='test_user',
password='correctPassword1')
token = get_jwt(correct_user)
url = reverse('jwt_refresh')
data = {
'token': token
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertIsNotNone(response.data['token'])
def test_jobs_filtered_by_user(self):
"""
Job-details method only returns jobs created by user
"""
user1 = User.objects.create_user(username='user1', password='user1Password')
user2 = User.objects.create_user(username='user2', password='user2Password')
job1 = Job(position_title="test job user 1", user=user1)
job2 = Job(position_title="test job user 2", user=user2)
job1.save()
job2.save()
url = reverse('jobs-list')
self.client.force_authenticate(user1)
self.client.credentials(Authorization='JWT ' + get_jwt(user1))
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_200_OK)
jobs = deserialiseJob(response.data, many=True)
self.assertEqual(len(jobs), 1)
self.assertEqual(jobs[0].position_title, 'test job user 1')
self.assertEqual(jobs[0].user, user1)
def test_job_detail_for_wrong_user_404(self):
"""
Attempting to return a job for incorrect user returns 404
"""
user = User.objects.create_user(username='user1', password='user1Password')
wrong_user = User.objects.create_user(username='wrong', password='user2Password')
job = Job(position_title="test job", user=user)
job.save()
url = reverse('jobs-detail', kwargs={'pk': job.id})
self.client.force_authenticate(wrong_user)
self.client.credentials(Authorization='JWT ' + get_jwt(wrong_user))
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND)
from rest_framework.viewsets import ModelViewSet
from rest_framework.authentication import BasicAuthentication
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from .serialisers import JobSerialiser
class JobViewSet(ModelViewSet):
serializer_class = JobSerialiser
permission_classes = (IsAuthenticated,)
authentication_classes = [JSONWebTokenAuthentication, BasicAuthentication]
def get_queryset(self):
return self.request.user.applications.all()
def perform_create(self, serializer):
serializer.save(user=self.request.user)
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"],
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": ["transform-vue-jsx", "istanbul"]
}
}
}
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
/build/
/config/
/dist/
/*.js
/test/unit/coverage/
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}