Compare commits
No commits in common. "master" and "django" have entirely different histories.
4
.coveragerc
Normal file
4
.coveragerc
Normal file
@ -0,0 +1,4 @@
|
||||
[report]
|
||||
omit =
|
||||
venv/*
|
||||
duckbook/wsgi.py
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,3 +1,10 @@
|
||||
*~
|
||||
*.pyc
|
||||
/db.sqlite3
|
||||
/venv/
|
||||
.env
|
||||
**/*.pyc
|
||||
/static/
|
||||
/.coverage
|
||||
/htmlcov/
|
||||
/test_failures.txt
|
||||
/test_rerun.txt
|
||||
/pylint/
|
||||
|
8
.pylintrc
Normal file
8
.pylintrc
Normal file
@ -0,0 +1,8 @@
|
||||
[MASTER]
|
||||
|
||||
load-plugins=pylint_django
|
||||
|
||||
[REPORTS]
|
||||
|
||||
files-output=yes
|
||||
|
@ -1,23 +0,0 @@
|
||||
# Swagger Codegen Ignore
|
||||
# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Gergely Polonkai
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
20
Makefile
Normal file
20
Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
APPENDCOV = $(shell test -s test_rerun.txt && echo "-a")
|
||||
MODULES = duckbook booking api accounts
|
||||
|
||||
test:
|
||||
coverage run $(APPENDCOV) \
|
||||
--source `echo $(MODULES) | sed 's/ /,/g'` \
|
||||
manage.py test `cat test_rerun.txt`
|
||||
coverage html
|
||||
coverage report
|
||||
@echo "Coverage data is available in HTML format under the htmlcov directory"
|
||||
|
||||
lint:
|
||||
rm -rf pylint/* || true
|
||||
pylint --rcfile=.pylintrc $(MODULES) || true
|
||||
mkdir pylint || true
|
||||
sh -c 'for file in pylint_*; do \
|
||||
o="$${file#pylint_}"; \
|
||||
mv "$$file" pylint/$$o; \
|
||||
done'
|
||||
@echo "lint data is available in TXT format under the pylint directory"
|
2
Procfile
2
Procfile
@ -1 +1 @@
|
||||
web: gunicorn app:app --log-file -
|
||||
web: gunicorn duckbook.wsgi --log-file=-
|
@ -1,7 +1,4 @@
|
||||
# Rubber Duck Booking Tool
|
||||
|
||||
Rubber Duck Booking Tool for the masses
|
||||
|
||||
This tool allows you to manage your rubber duck repository, helping
|
||||
your fellow developers to get the proper duck
|
||||
for [debugging](http://en.wikipedia.org/wiki/Rubber_duck_debugging).
|
||||
This tool allows you to manage your rubber duck repository, helping your fellow developers’ to get the proper duck for [debugging](http://en.wikipedia.org/wiki/Rubber_duck_debugging).
|
||||
|
17
accounts/templates/accounts/login.html
Normal file
17
accounts/templates/accounts/login.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends 'front_template.html' %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="error">Username/password mismatch. Please try again!</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'accounts:login' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.username.label_tag }} {{ form.username }}<br>
|
||||
{{ form.password.label_tag }} {{ form.password }}<br>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
<input type="hidden" name="next" value="{% url 'booking:list' %}">
|
||||
</form>
|
||||
{% endblock %}
|
10
accounts/templates/accounts/registration.html
Normal file
10
accounts/templates/accounts/registration.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends 'front_template.html' %}
|
||||
|
||||
{% block body %}
|
||||
<p>For later convenience, please use your corporate ID as the username!</p>
|
||||
<form method="post" action="{% url 'accounts:register' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
{% endblock %}
|
117
accounts/tests.py
Normal file
117
accounts/tests.py
Normal file
@ -0,0 +1,117 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
Account management module for the Duck Booking Tool backend
|
||||
"""
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django_webtest import WebTest
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
|
||||
class FrontTest(TestCase):
|
||||
"""
|
||||
Test front-end capabilities of the accounts module
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
self.admin = User.objects.create_user(username='admin',
|
||||
password='password')
|
||||
|
||||
def test_login_page(self):
|
||||
"""
|
||||
Test for the existence of the login page
|
||||
"""
|
||||
|
||||
response = self.client.get('/accounts/login')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_login(self):
|
||||
"""
|
||||
Test login functionality
|
||||
"""
|
||||
|
||||
response = self.client.post('/accounts/login', {
|
||||
'next': '/',
|
||||
'username': 'admin',
|
||||
'password': 'password'})
|
||||
|
||||
self.assertRedirects(response, '/')
|
||||
|
||||
def test_logout(self):
|
||||
"""
|
||||
Test the logout page
|
||||
"""
|
||||
|
||||
self.client.login(username='admin', password='aeou')
|
||||
|
||||
response = self.client.get('/accounts/logout')
|
||||
self.assertRedirects(response, '/')
|
||||
|
||||
def test_registration_page(self):
|
||||
"""
|
||||
Test for existence of the registration page
|
||||
"""
|
||||
|
||||
response = self.client.get('/accounts/register')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
class RegFormTest(WebTest):
|
||||
"""
|
||||
Test case for the registration form
|
||||
"""
|
||||
|
||||
def test_valid_data(self):
|
||||
"""
|
||||
Test valid registration without actual HTTP requests
|
||||
"""
|
||||
|
||||
form_data = {
|
||||
'username': 'test',
|
||||
'password1': 'password',
|
||||
'password2': 'password'
|
||||
}
|
||||
|
||||
form = UserCreationForm(form_data)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
user = form.save()
|
||||
self.assertEqual(user.username, 'test')
|
||||
# The password must be encrypted by now
|
||||
self.assertNotEqual(user.password, 'password')
|
||||
|
||||
def test_empty(self):
|
||||
"""
|
||||
Test empty registration form
|
||||
"""
|
||||
|
||||
form_data = {}
|
||||
form = UserCreationForm(form_data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(form.errors, {
|
||||
'username': ['This field is required.'],
|
||||
'password1': ['This field is required.'],
|
||||
'password2': ['This field is required.'],
|
||||
})
|
||||
|
||||
def test_form_error(self):
|
||||
"""
|
||||
Test incomplete registration
|
||||
"""
|
||||
|
||||
page = self.app.get('/accounts/register')
|
||||
page = page.form.submit()
|
||||
self.assertContains(page, "This field is required.")
|
||||
|
||||
def test_form_success(self):
|
||||
"""
|
||||
Test for successful registrations
|
||||
"""
|
||||
|
||||
page = self.app.get('/accounts/register')
|
||||
page.form['username'] = 'test'
|
||||
page.form['password1'] = 'password'
|
||||
page.form['password2'] = 'password'
|
||||
page = page.form.submit()
|
||||
self.assertRedirects(page, '/')
|
28
accounts/urls.py
Normal file
28
accounts/urls.py
Normal file
@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
URL patterns for the accounts module
|
||||
"""
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import RegistrationFormView
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^register$',
|
||||
RegistrationFormView.as_view(),
|
||||
name='register'
|
||||
),
|
||||
url(
|
||||
r'^login$',
|
||||
'django.contrib.auth.views.login',
|
||||
{'template_name': 'accounts/login.html'},
|
||||
name='login'
|
||||
),
|
||||
url(
|
||||
r'^logout$',
|
||||
'django.contrib.auth.views.logout',
|
||||
{'next_page': 'booking:list'},
|
||||
name='logout'
|
||||
),
|
||||
]
|
40
accounts/views.py
Normal file
40
accounts/views.py
Normal file
@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
Views for the accounts module
|
||||
"""
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.views import generic
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
|
||||
class RegistrationFormView(generic.View):
|
||||
"""
|
||||
Class to display the registration form
|
||||
"""
|
||||
|
||||
form_class = UserCreationForm
|
||||
template_name = 'accounts/registration.html'
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
Implementation of the GET method
|
||||
"""
|
||||
|
||||
form = self.form_class()
|
||||
return render(request, self.template_name, {'form': form})
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
Implementation of the POST method
|
||||
"""
|
||||
|
||||
form = self.form_class(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
return HttpResponseRedirect(reverse('booking:list'))
|
||||
|
||||
return render(request, self.template_name, {'form': form})
|
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
77
api/serializers.py
Normal file
77
api/serializers.py
Normal file
@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Serializers for the Duck Booking Tool API
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework import serializers
|
||||
|
||||
from booking.models import Duck, Competence, DuckCompetence
|
||||
|
||||
class NamespacedSerializer(serializers.HyperlinkedModelSerializer):
|
||||
"""
|
||||
HyperlinkedModelSerializer with URL namespace support
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not hasattr(self, 'Meta') \
|
||||
or not hasattr(self.Meta, 'url_namespace') \
|
||||
or self.Meta.url_namespace is None \
|
||||
or self.Meta.url_namespace == '':
|
||||
raise ImproperlyConfigured("namespace must be set!")
|
||||
|
||||
self.url_namespace = self.Meta.url_namespace
|
||||
|
||||
self.url_namespace = kwargs.pop('url_namespace',
|
||||
self.url_namespace)
|
||||
|
||||
if not self.url_namespace.endswith(':'):
|
||||
self.url_namespace += ':'
|
||||
|
||||
super(NamespacedSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
def build_url_field(self, field_name, model_class):
|
||||
field_class, field_kwargs = super(NamespacedSerializer, self) \
|
||||
.build_url_field(field_name,
|
||||
model_class)
|
||||
|
||||
view_name = field_kwargs.get('view_name')
|
||||
|
||||
if not view_name.startswith(self.url_namespace):
|
||||
field_kwargs['view_name'] = self.url_namespace + view_name
|
||||
|
||||
return field_class, field_kwargs
|
||||
|
||||
class CompetenceSerializer(NamespacedSerializer):
|
||||
"""
|
||||
Serializer for Competence objects
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
url_namespace = 'api'
|
||||
model = Competence
|
||||
fields = ('url', 'name',)
|
||||
|
||||
class DuckCompetenceSerializer(NamespacedSerializer):
|
||||
"""
|
||||
Serializer for DuckCompetence objects
|
||||
"""
|
||||
|
||||
comp = CompetenceSerializer()
|
||||
|
||||
class Meta:
|
||||
url_namespace = 'api'
|
||||
model = DuckCompetence
|
||||
fields = ('comp', 'up_minutes', 'down_minutes',)
|
||||
|
||||
class DuckSerializer(NamespacedSerializer):
|
||||
"""
|
||||
Serializer for Duck objects
|
||||
"""
|
||||
|
||||
competences = DuckCompetenceSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
url_namespace = 'api'
|
||||
model = Duck
|
||||
fields = ('url', 'name', 'color', 'competences',)
|
8
api/templates/api/duck_comp_list.json
Normal file
8
api/templates/api/duck_comp_list.json
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
{% for comp in comp_list %}
|
||||
{
|
||||
"name": "{{ comp.comp.name }}",
|
||||
"level": {{ comp.level }}
|
||||
}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
326
api/tests.py
Normal file
326
api/tests.py
Normal file
@ -0,0 +1,326 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
Test cases for API calls
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django_webtest import WebTest
|
||||
|
||||
import json
|
||||
|
||||
from .serializers import NamespacedSerializer
|
||||
from booking.ducklevel import level_to_up_minutes
|
||||
from booking.models import Species, Location, Duck, Competence, DuckCompetence
|
||||
|
||||
class MetalessNamespacedSerializer(NamespacedSerializer):
|
||||
pass
|
||||
|
||||
class MissingNamespacedSerializer(NamespacedSerializer):
|
||||
class Meta:
|
||||
pass
|
||||
|
||||
class NoneNamespacedSerializer(NamespacedSerializer):
|
||||
class Meta:
|
||||
url_namespace = None
|
||||
|
||||
class EmptyNamespacedSerializer(NamespacedSerializer):
|
||||
class Meta:
|
||||
url_namespace = ''
|
||||
|
||||
class TestNamespacedSerializer(TestCase):
|
||||
"""
|
||||
Test namespaced Serializer
|
||||
"""
|
||||
|
||||
def test_no_namespace(self):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
serializer = MetalessNamespacedSerializer()
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
serializer = MissingNamespacedSerializer()
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
serializer = NoneNamespacedSerializer()
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
serializer = EmptyNamespacedSerializer()
|
||||
|
||||
def test_namespacing(self):
|
||||
class MySerializer(NamespacedSerializer):
|
||||
class Meta:
|
||||
model = Competence
|
||||
fields = ('url',)
|
||||
url_namespace = 'api'
|
||||
|
||||
competence = Competence.objects.create(
|
||||
added_by=User.objects.create())
|
||||
serializer = MySerializer(competence,
|
||||
context={
|
||||
'request': RequestFactory().get('/')
|
||||
})
|
||||
|
||||
self.assertIsNotNone(serializer.data['url'])
|
||||
|
||||
class DuckClassTest(WebTest):
|
||||
"""
|
||||
Test case for duck related API calls
|
||||
"""
|
||||
|
||||
csrf_checks = False
|
||||
|
||||
def setUp(self):
|
||||
good_minutes = level_to_up_minutes(settings.COMP_WARN_LEVEL + 1)
|
||||
bad_minutes = level_to_up_minutes(settings.COMP_WARN_LEVEL)
|
||||
|
||||
self.user = User.objects.create_user(username='test',
|
||||
password='test')
|
||||
|
||||
self.species = Species.objects.create(name='duck')
|
||||
self.location = Location.objects.create(name='temp')
|
||||
self.comp_bad = Competence.objects.create(name='test1',
|
||||
added_by=self.user)
|
||||
self.comp_good = Competence.objects.create(name='test2',
|
||||
added_by=self.user)
|
||||
self.duck = Duck.objects.create(species=self.species,
|
||||
name='test duck',
|
||||
location=self.location,
|
||||
donated_by=self.user,
|
||||
color='123456')
|
||||
|
||||
DuckCompetence.objects.create(duck=self.duck,
|
||||
comp=self.comp_bad,
|
||||
up_minutes=bad_minutes,
|
||||
down_minutes=0)
|
||||
|
||||
DuckCompetence.objects.create(duck=self.duck,
|
||||
comp=self.comp_good,
|
||||
up_minutes=good_minutes,
|
||||
down_minutes=0)
|
||||
|
||||
def test_book_nonlogged(self):
|
||||
"""
|
||||
Test booking without logging in
|
||||
"""
|
||||
|
||||
page = self.app.post('/api/v1/ducks/1/book/', expect_errors=True)
|
||||
self.assertEqual(page.status_code, 403)
|
||||
|
||||
def test_book_nonexist(self):
|
||||
"""
|
||||
Test booking a non-existing duck
|
||||
"""
|
||||
|
||||
# Try to book a non-existing duck
|
||||
page = self.app.post(
|
||||
'/api/v1/ducks/9999/book/',
|
||||
params={
|
||||
'competence': self.comp_good.pk,
|
||||
},
|
||||
user=self.user,
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, page.status_code)
|
||||
|
||||
# Try to book an existing Duck for a non-existing competence
|
||||
page = self.app.post(
|
||||
'/api/v1/ducks/%d/book/' % self.duck.pk,
|
||||
params={
|
||||
'competence': 9999
|
||||
},
|
||||
user=self.user,
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, page.status_code)
|
||||
|
||||
def test_book_warn(self):
|
||||
"""
|
||||
Test duck booking for a competence the duck is not good at
|
||||
"""
|
||||
|
||||
url = '/api/v1/ducks/%d/book/' % self.duck.pk
|
||||
comp_none = Competence.objects.create(name='test3',
|
||||
added_by=self.user)
|
||||
|
||||
# Book for a competence the duck doesn’t have at all
|
||||
test_data = {
|
||||
'competence': comp_none.pk,
|
||||
}
|
||||
|
||||
page = self.app.post(url, params=test_data, user=self.user)
|
||||
self.assertEquals(200, page.status_code)
|
||||
|
||||
page_json = json.loads(page.content)
|
||||
self.assertEquals(page_json['status'], 'bad-comp')
|
||||
|
||||
# Book for a competence with low level
|
||||
test_data = {
|
||||
'competence': self.comp_bad.pk,
|
||||
}
|
||||
|
||||
page = self.app.post(url, params=test_data, user=self.user)
|
||||
self.assertEquals(200, page.status_code)
|
||||
|
||||
page_json = json.loads(page.content)
|
||||
self.assertEquals(page_json['status'], 'bad-comp')
|
||||
|
||||
# Forcibly book for a competence with low level
|
||||
test_data['force'] = 1
|
||||
|
||||
page = self.app.post(url, params=test_data, user=self.user)
|
||||
self.assertEqual(200, page.status_code)
|
||||
|
||||
page_json = json.loads(page.content)
|
||||
self.assertEquals(page_json['status'], 'ok')
|
||||
|
||||
def test_book_good(self):
|
||||
"""
|
||||
Test duck booking for a competence the duck is good at
|
||||
"""
|
||||
|
||||
test_data = {
|
||||
"competence": self.comp_good.pk
|
||||
}
|
||||
|
||||
url = '/api/v1/ducks/%d/book/' % self.duck.pk
|
||||
# Book the duck
|
||||
page = self.app.post(url, params=test_data, user=self.user)
|
||||
self.assertEquals(200, page.status_code)
|
||||
|
||||
page_json = json.loads(page.content)
|
||||
self.assertEqual(page_json['status'], 'ok')
|
||||
|
||||
# Try to book again, it should fail
|
||||
page = self.app.post(url, params=test_data, user=self.user)
|
||||
self.assertEqual(200, page.status_code)
|
||||
|
||||
page_json = json.loads(page.content)
|
||||
self.assertEqual('already-booked', page_json['status'])
|
||||
|
||||
def test_incomplete_donation(self):
|
||||
"""
|
||||
Test duck donation with incomplete data
|
||||
"""
|
||||
|
||||
params = {
|
||||
# No parameters
|
||||
'none': '',
|
||||
# Empty parameter set
|
||||
'empty': {},
|
||||
# Species omitted
|
||||
'species-omit': {
|
||||
'location': self.location.pk,
|
||||
'color': '123456',
|
||||
},
|
||||
# Missing species
|
||||
'species-notfound': {
|
||||
'location': self.location.pk,
|
||||
'species': 9999,
|
||||
'color': '123456',
|
||||
'expected-code': 404,
|
||||
'expected-error': 'bad-species',
|
||||
},
|
||||
# Location omitted
|
||||
'location-omit': {
|
||||
'species': self.species.pk,
|
||||
'color': '123456',
|
||||
},
|
||||
# Missing location
|
||||
'location-notfound': {
|
||||
'location': 9999,
|
||||
'species': self.species.pk,
|
||||
'color': '123456',
|
||||
'expected-code': 404,
|
||||
'expected-error': 'bad-location',
|
||||
},
|
||||
# Color omitted
|
||||
'color-omit': {
|
||||
'location': self.location.pk,
|
||||
'species': self.species.pk,
|
||||
},
|
||||
# Invalid color
|
||||
'color-invalid': {
|
||||
'location': self.location.pk,
|
||||
'species': self.species.pk,
|
||||
'color': 'red',
|
||||
'expected-error': 'bad-color',
|
||||
'expected-code': 400,
|
||||
},
|
||||
}
|
||||
|
||||
url = '/api/v1/ducks/donate/'
|
||||
|
||||
for name, param in params.items():
|
||||
if param == '':
|
||||
expected_code = 400
|
||||
expected_error = 'incomplete-request'
|
||||
else:
|
||||
expected_code = param.pop('expected-code', 400)
|
||||
expected_error = param.pop('expected-error',
|
||||
'incomplete-request')
|
||||
|
||||
page = self.app.post(url,
|
||||
params=param,
|
||||
expect_errors=True,
|
||||
user=self.user)
|
||||
|
||||
self.assertEquals(
|
||||
expected_code,
|
||||
page.status_code,
|
||||
msg="Got unexpected status code ({}) for parameter set {}".format(
|
||||
page.status_code,
|
||||
name))
|
||||
page_json = json.loads(page.content)
|
||||
|
||||
self.assertEquals(
|
||||
expected_error,
|
||||
page_json['status'],
|
||||
msg="Got unexpected status code ({}) for parameter set {}".format(
|
||||
page.status_code,
|
||||
name))
|
||||
|
||||
def test_duck_donation(self):
|
||||
"""
|
||||
Test duck donating functionality
|
||||
"""
|
||||
|
||||
# Duck donation should not be allowed without logging in
|
||||
page = self.app.get('/api/v1/ducks/donate/', expect_errors=True)
|
||||
self.assertEquals(page.status_code, 403)
|
||||
|
||||
# Duck donation should not be allowed withoud logging in
|
||||
page = self.app.post('/api/v1/ducks/donate/', expect_errors=True)
|
||||
self.assertEquals(page.status_code, 403)
|
||||
|
||||
color = '123456'
|
||||
page = self.app.post(
|
||||
'/api/v1/ducks/donate/',
|
||||
params={
|
||||
'species': self.species.pk,
|
||||
'location': self.location.pk,
|
||||
'color': color,
|
||||
},
|
||||
user=self.user)
|
||||
self.assertEquals(200, page.status_code)
|
||||
page_json = json.loads(page.content)
|
||||
|
||||
self.assertIn('id', page_json)
|
||||
|
||||
duck = Duck.objects.get(pk=page_json['id'])
|
||||
|
||||
self.assertEquals(color, duck.color)
|
||||
|
||||
def test_duck_details(self):
|
||||
"""
|
||||
Test duck details view
|
||||
"""
|
||||
|
||||
url = '/api/v1/ducks/%d/' % self.duck.pk
|
||||
page = self.app.get(url)
|
||||
self.assertEqual(200, page.status_code)
|
||||
|
||||
page_json = json.loads(page.content)
|
||||
|
||||
self.assertEquals('test duck', page_json['name'])
|
||||
self.assertEquals('123456', page_json['color'])
|
||||
self.assertEqual(2, len(page_json['competences']))
|
14
api/urls.py
Normal file
14
api/urls.py
Normal file
@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
URL definitions for version 1 of the Duck Booking Tool API
|
||||
"""
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
rest_router = routers.DefaultRouter()
|
||||
rest_router.register(r'ducks', views.DuckViewSet)
|
||||
rest_router.register(r'competences', views.CompetenceViewSet)
|
||||
|
||||
urlpatterns = rest_router.urls
|
108
api/views.py
Normal file
108
api/views.py
Normal file
@ -0,0 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Views for the Duck Booking Tool API
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .serializers import DuckSerializer, CompetenceSerializer
|
||||
from booking.models import Duck, Competence, Booking, Species, Location
|
||||
|
||||
class DuckViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
View set for duck handling
|
||||
"""
|
||||
|
||||
serializer_class = DuckSerializer
|
||||
queryset = Duck.objects.all()
|
||||
|
||||
@detail_route(methods=['post'], permission_classes=[IsAuthenticated])
|
||||
def book(self, request, pk=None):
|
||||
"""
|
||||
API call to book a duck
|
||||
"""
|
||||
|
||||
duck = self.get_object()
|
||||
competence = get_object_or_404(Competence, pk=request.data['competence'])
|
||||
force = request.data.get('force', False)
|
||||
|
||||
# If the duck is already booked, return 'already-booked' as the
|
||||
# result
|
||||
if duck.booked_by() != None:
|
||||
return Response({'status': 'already-booked'})
|
||||
|
||||
# Result 'fail' means a problem
|
||||
result = 'fail'
|
||||
comp_level = 0
|
||||
|
||||
# Check if the duck has the requested competence
|
||||
dcomp_list = duck.competences.filter(comp=competence)
|
||||
|
||||
if len(dcomp_list) < 1:
|
||||
comp_level = 0
|
||||
else:
|
||||
comp_level = dcomp_list[0].level()
|
||||
|
||||
# If the competence level is too low, set result to 'bad-comp'
|
||||
if comp_level <= settings.COMP_WARN_LEVEL:
|
||||
result = 'bad-comp'
|
||||
|
||||
# If the duck has high enough competence or the booking is
|
||||
# forced, return status 'success'
|
||||
if result != 'bad-comp' or force:
|
||||
result = 'ok'
|
||||
|
||||
booking = Booking(duck=duck, user=request.user, comp_req=competence)
|
||||
booking.save()
|
||||
|
||||
return Response({'status': result})
|
||||
|
||||
@list_route(methods=['post'], permission_classes=[IsAuthenticated])
|
||||
def donate(self, request):
|
||||
"""
|
||||
API call to donate a new duck
|
||||
"""
|
||||
|
||||
color = request.data.get('color')
|
||||
species_id = request.data.get('species')
|
||||
location_id = request.data.get('location')
|
||||
|
||||
if None in (color, species_id, location_id):
|
||||
return Response({'status': 'incomplete-request'}, status=400)
|
||||
|
||||
try:
|
||||
species = Species.objects.get(pk=species_id)
|
||||
except Species.DoesNotExist:
|
||||
return Response({'status': 'bad-species'}, status=404)
|
||||
|
||||
try:
|
||||
location = Location.objects.get(pk=location_id)
|
||||
except Location.DoesNotExist:
|
||||
return Response({'status': 'bad-location'}, status=404)
|
||||
|
||||
if not re.match(r'^[0-9a-f]{6}$', color, flags=re.IGNORECASE):
|
||||
return Response({'status': 'bad-color'}, status=400)
|
||||
|
||||
color = color.lower()
|
||||
|
||||
duck = Duck.objects.create(donated_by=request.user,
|
||||
species=species,
|
||||
location=location,
|
||||
color=color)
|
||||
|
||||
return Response({'status': 'ok', 'id': duck.pk})
|
||||
|
||||
class CompetenceViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
View set for competence handling
|
||||
"""
|
||||
|
||||
serializer_class = CompetenceSerializer
|
||||
queryset = Competence.objects.all()
|
16
app.py
16
app.py
@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import connexion
|
||||
from flask_login import LoginManager
|
||||
|
||||
app = connexion.App(__name__, specification_dir='./swagger/')
|
||||
app.add_api('swagger.yaml',
|
||||
arguments={
|
||||
'title': 'Rubber Duck Booking Tool'
|
||||
})
|
||||
|
||||
login_manager = LoginManager()
|
||||
#login_manager.init_app(app)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(port=8080)
|
62
assets/rubber-duck.svg
Normal file
62
assets/rubber-duck.svg
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 744.09448819 1052.3622047"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="rubber-duck.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="2.8"
|
||||
inkscape:cx="209.89137"
|
||||
inkscape:cy="643.96794"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="702"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#ffff00;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 280.71428,433.43364 c -0.87708,20.25502 -4.02255,32.1475 -18.65993,45.25832 -14.12869,12.65517 -125.03157,21.41019 -139.19721,5.09882 -16.1284,-18.57142 -25.35715,-47.6566 -8.92858,-55 7.66995,-3.42839 35.89025,-11.69697 32.33147,-26.75204 -1.88668,-7.98141 -10.10117,-24.34109 -18.22478,-25.13995 -51.569795,-5.07127 -8.00064,-9.20375 -19.60172,-12.03589 -3.37714,-0.82445 -8.872918,-10.59179 -4.85623,-11.40007 4.55659,-0.91692 26.31075,8.37326 30.09257,5.28944 13.1741,-10.74256 16.13617,-22.62926 27.82237,-25.09748 18.76333,-3.96297 49.44197,2.90219 49.79483,34.91109 0.0982,8.90791 -9.41603,29.28445 -30.15444,41.87893 -15.07813,9.15698 61.91009,14.33434 71.01022,12.98882 16.33969,-2.41593 20.08918,-18.22738 23.29478,-19.58284 2.48093,-1.04903 6.21154,7.99293 5.27665,29.58285 z"
|
||||
id="path3336"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssssssssssssss" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
5
booking/__init__.py
Normal file
5
booking/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""
|
||||
Booking module of the Duck Booking Tool
|
||||
"""
|
||||
|
||||
default_app_config = 'booking.apps.BookingConfig'
|
18
booking/admin.py
Normal file
18
booking/admin.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
Administration site definition for the Duck Booking Tool
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from booking.models import Species, Location, Competence, Duck, \
|
||||
Booking, DuckCompetence, DuckName, \
|
||||
DuckNameVote
|
||||
|
||||
admin.site.register(Species)
|
||||
admin.site.register(Location)
|
||||
admin.site.register(Competence)
|
||||
admin.site.register(Duck)
|
||||
admin.site.register(Booking)
|
||||
admin.site.register(DuckCompetence)
|
||||
admin.site.register(DuckName)
|
||||
admin.site.register(DuckNameVote)
|
48
booking/apps.py
Normal file
48
booking/apps.py
Normal file
@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
App config for the booking module of the Duck Booking Tool. This module
|
||||
is currently needed for the MAX_DUCK_LEVEL system check.
|
||||
"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.checks import Error, register
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
@register()
|
||||
def max_level_check(app_configs, **kwargs):
|
||||
"""
|
||||
System check to see if MAX_DUCK_LEVEL has a sane value (non-zero,
|
||||
positive integer)
|
||||
"""
|
||||
|
||||
errors = []
|
||||
|
||||
if not hasattr(settings, 'MAX_DUCK_LEVEL'):
|
||||
errors.append(
|
||||
Error(
|
||||
'MAX_DUCK_LEVEL is not set!',
|
||||
id='booking.E001'
|
||||
)
|
||||
)
|
||||
else:
|
||||
if settings.MAX_DUCK_LEVEL <= 0:
|
||||
errors.append(
|
||||
Error(
|
||||
'MAX_DUCK_LEVEL should be greater than zero!',
|
||||
id='booking.E002'
|
||||
)
|
||||
)
|
||||
|
||||
if not isinstance(settings.MAX_DUCK_LEVEL, int):
|
||||
errors.append(
|
||||
Error(
|
||||
'MAX_DUCK_LEVEL must be an integer!',
|
||||
id='booking.E003'
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
class BookingConfig(AppConfig):
|
||||
name = 'booking'
|
36
booking/ducklevel.py
Normal file
36
booking/ducklevel.py
Normal file
@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
Duck level calculations
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
import math
|
||||
|
||||
def level_to_up_minutes(level):
|
||||
"""
|
||||
Convert duck level to up minutes
|
||||
"""
|
||||
|
||||
if level == 0:
|
||||
return 0
|
||||
|
||||
return 2 * pow(10, level)
|
||||
|
||||
def level_to_down_minutes(level):
|
||||
"""
|
||||
Convert duck level to down minutes
|
||||
"""
|
||||
|
||||
if level == 0:
|
||||
return 0
|
||||
|
||||
return 20 * pow(10, level)
|
||||
|
||||
def minutes_to_level(up_minutes, down_minutes):
|
||||
"""
|
||||
Convert booking minutes to duck level
|
||||
"""
|
||||
minutes = up_minutes + down_minutes / 10
|
||||
level = 0 if minutes <= 0 else min(settings.MAX_DUCK_LEVEL, math.floor(math.log10(minutes)))
|
||||
|
||||
return level
|
249
booking/models.py
Normal file
249
booking/models.py
Normal file
@ -0,0 +1,249 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Models for the Duck Booking Tool
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from fuzzywuzzy import fuzz
|
||||
|
||||
from .ducklevel import minutes_to_level
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Species(models.Model):
|
||||
"""
|
||||
Model to hold the Ducks’ species
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=40, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Location(models.Model):
|
||||
"""
|
||||
Model to hold the possible locations of the Ducks
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Competence(models.Model):
|
||||
"""
|
||||
Model to hold Duck competences
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
added_at = models.DateTimeField(default=timezone.now)
|
||||
added_by = models.ForeignKey(User)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_similar_comps(cls, name):
|
||||
"""
|
||||
Get competence names similar to name
|
||||
"""
|
||||
|
||||
comps = cls.objects.values_list('name', flat=True)
|
||||
ret = ()
|
||||
|
||||
for competence in comps:
|
||||
similarity = fuzz.ratio(name.lower(), competence.lower())
|
||||
|
||||
# This ratio is subject to change
|
||||
if similarity > settings.MIN_FUZZY_SIMILARITY:
|
||||
ret = ret + (competence,)
|
||||
|
||||
return ret
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Duck(models.Model):
|
||||
"""
|
||||
Model to hold Duck data
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=80, null=True, blank=True)
|
||||
color = models.CharField(max_length=6)
|
||||
species = models.ForeignKey(Species)
|
||||
location = models.ForeignKey(Location)
|
||||
comps = models.ManyToManyField(Competence, through='DuckCompetence')
|
||||
donated_by = models.ForeignKey(User)
|
||||
donated_at = models.DateTimeField(default=timezone.now)
|
||||
adopted_by = models.ForeignKey(User, related_name='adopted_ducks', null=True, blank=True)
|
||||
adopted_at = models.DateTimeField(null=True, blank=True)
|
||||
bookings = models.ManyToManyField(User, through='Booking', related_name='+')
|
||||
on_holiday_since = models.DateTimeField(null=True, blank=True)
|
||||
on_holiday_until = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
if self.name == None or self.name == '':
|
||||
return 'Unnamed :('
|
||||
|
||||
return self.name
|
||||
|
||||
def age(self):
|
||||
"""
|
||||
Get the age of the duck (time since the duck has been registered
|
||||
in the tool)
|
||||
"""
|
||||
|
||||
seconds_d = timezone.now() - self.donated_at
|
||||
seconds = seconds_d.total_seconds()
|
||||
|
||||
if seconds < 0:
|
||||
return -1
|
||||
|
||||
return seconds
|
||||
|
||||
def dpx(self):
|
||||
"""
|
||||
Get the Duck Popularity indeX for this duck
|
||||
"""
|
||||
|
||||
all_time = Booking.total_booking_time()
|
||||
duck_time = Booking.duck_booking_time(self)
|
||||
|
||||
if (all_time == None) or (duck_time == None):
|
||||
return 0
|
||||
|
||||
return Booking.duck_booking_time(self) / Booking.total_booking_time()
|
||||
|
||||
def booked_by(self):
|
||||
"""
|
||||
Get the user who is currently using the duck
|
||||
"""
|
||||
|
||||
booking_list = self.booking_set.filter(end_ts=None)
|
||||
|
||||
if len(booking_list) == 0:
|
||||
return None
|
||||
|
||||
if len(booking_list) > 1:
|
||||
raise RuntimeError(u"Duck is booked more than once!")
|
||||
|
||||
return booking_list[0].user
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DuckName(models.Model):
|
||||
"""
|
||||
Model to hold name suggestions for Ducks
|
||||
"""
|
||||
|
||||
duck = models.ForeignKey(Duck)
|
||||
name = models.CharField(max_length=60, null=False)
|
||||
suggested_by = models.ForeignKey(User)
|
||||
suggested_at = models.DateTimeField(default=timezone.now)
|
||||
closed_by = models.ForeignKey(User,
|
||||
related_name='+',
|
||||
null=True,
|
||||
blank=True)
|
||||
closed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
ret = "{0}, suggested by {1}".format(self.name,
|
||||
self.suggested_by)
|
||||
|
||||
if self.closed_by:
|
||||
ret += " <closed>"
|
||||
|
||||
return ret
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DuckNameVote(models.Model):
|
||||
"""
|
||||
Model to hold votes to Duck names
|
||||
"""
|
||||
|
||||
duck_name = models.ForeignKey(DuckName)
|
||||
vote_timestamp = models.DateTimeField(default=timezone.now)
|
||||
voter = models.ForeignKey(User)
|
||||
upvote = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{0} voted {1} for {2}".format(self.voter,
|
||||
"up" if self.upvote else "down",
|
||||
self.duck_name)
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DuckCompetence(models.Model):
|
||||
"""
|
||||
Duck competence governor table
|
||||
"""
|
||||
|
||||
duck = models.ForeignKey(Duck, related_name='competences')
|
||||
comp = models.ForeignKey(Competence, related_name='ducks')
|
||||
up_minutes = models.IntegerField(default=0)
|
||||
down_minutes = models.IntegerField(default=0)
|
||||
|
||||
def level(self):
|
||||
"""
|
||||
Return the actual level of a duck
|
||||
"""
|
||||
|
||||
return minutes_to_level(self.up_minutes, self.down_minutes)
|
||||
|
||||
def __str__(self):
|
||||
return "{0} with +{1}/-{2} minutes in {3}".format(self.duck,
|
||||
self.up_minutes,
|
||||
self.down_minutes,
|
||||
self.comp)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('duck', 'comp')
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Booking(models.Model):
|
||||
"""
|
||||
Duck booking governor table
|
||||
"""
|
||||
|
||||
duck = models.ForeignKey(Duck)
|
||||
user = models.ForeignKey(User)
|
||||
comp_req = models.ForeignKey(Competence)
|
||||
start_ts = models.DateTimeField(default=timezone.now)
|
||||
end_ts = models.DateTimeField(null=True, blank=True)
|
||||
successful = models.BooleanField(default=True)
|
||||
|
||||
@classmethod
|
||||
def total_booking_time(cls):
|
||||
"""
|
||||
Get the sum of booked hours for all ducks
|
||||
"""
|
||||
|
||||
return cls.objects.filter(
|
||||
start_ts__isnull=False,
|
||||
end_ts__isnull=False).extra(
|
||||
select={
|
||||
'amount': 'sum(strftime(%s, end_ts) - strftime(%s, start_ts))'
|
||||
},
|
||||
select_params=('%s', '%s'))[0].amount
|
||||
|
||||
@classmethod
|
||||
def duck_booking_time(cls, duck):
|
||||
"""
|
||||
Get the sum of booked hours of a duck
|
||||
"""
|
||||
|
||||
return cls.objects.filter(
|
||||
start_ts__isnull=False,
|
||||
end_ts__isnull=False, duck=duck).extra(
|
||||
select={
|
||||
'amount': 'sum(strftime(%s, end_ts) - strftime(%s, start_ts))'
|
||||
},
|
||||
select_params=('%s', '%s'))[0].amount
|
||||
|
||||
def __str__(self):
|
||||
return "{0} booked by {1} for {2} since {3}".format(self.duck,
|
||||
self.user,
|
||||
self.comp_req,
|
||||
self.start_ts)
|
11
booking/static/booking.css
Normal file
11
booking/static/booking.css
Normal file
@ -0,0 +1,11 @@
|
||||
.button {
|
||||
background-color: #aaa;
|
||||
display: inline;
|
||||
padding: .2em;
|
||||
margin: .1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.no-close .ui-dialog-titlebar-close {
|
||||
display: none;
|
||||
}
|
10
booking/templates/booking/disclaimer.html
Normal file
10
booking/templates/booking/disclaimer.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends 'front_template.html' %}
|
||||
|
||||
{% block body %}
|
||||
<h2>Disclaimer</h2>
|
||||
<p>
|
||||
Basic idea is from
|
||||
<a href="http://en.wikipedia.org/wiki/The_Pragmatic_Programmer">The Pragmatic Programmer</a>.
|
||||
I suggest to read this book, even if you are not a programmer.
|
||||
</p>
|
||||
{% endblock %}
|
23
booking/templates/booking/duck_list.html
Normal file
23
booking/templates/booking/duck_list.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends 'front_template.html' %}
|
||||
{% load booking_tags %}
|
||||
|
||||
{% block body %}
|
||||
{% if duck_list %}
|
||||
{% for duck in duck_list %}
|
||||
<div class="duck" id="duck-icon-{{ duck.id }}" style="background-color: #{{ duck.color }};">
|
||||
{{ duck }}<br>
|
||||
{{ duck.species }}
|
||||
</div>
|
||||
<div class="profile" id="duck-profile-{{ duck.id }}">
|
||||
Employee for {{ duck.age|age_format:1 }}<br>
|
||||
Location: {{ duck.location }}<br>
|
||||
DPX: {{ duck.dpx }}<br>
|
||||
<div class="button" id="duck-book-{{ duck.id }}">book-button</div>
|
||||
<div class="button complist-button" id="duck-complist-{{ duck.id }}">Competence list</div>
|
||||
<div class="button" id="duck-namesugg-{{ duck.id }}">namesugg-button</div>
|
||||
<div class="button" id="duck-adopt-{{ duck.id }}">adopt-button</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<br class="clear">
|
||||
{% endif %}
|
||||
{% endblock %}
|
5
booking/templates/booking/terms.html
Normal file
5
booking/templates/booking/terms.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends 'front_template.html' %}
|
||||
|
||||
{% block body %}
|
||||
<h2>Terms and Conditions</h2>
|
||||
{% endblock %}
|
10
booking/templates/booking/vocabulary.html
Normal file
10
booking/templates/booking/vocabulary.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends 'front_template.html' %}
|
||||
|
||||
{% block body %}
|
||||
<h2>Vocabulary</h2>
|
||||
|
||||
<dl>
|
||||
<dt>Duck</dt>
|
||||
<dd>Despite the name, <em>duck</em> refers to any rubber or plastic toy bookable in this app.</dd>
|
||||
</dl>
|
||||
{% endblock %}
|
34
booking/templates/front_template.html
Normal file
34
booking/templates/front_template.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% load staticfiles %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Rubber Duck Booking Tool</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'booking.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="//code.jquery.com/ui/1.11.2/themes/ui-lightness/jquery-ui.css">
|
||||
|
||||
<script src="//code.jquery.com/jquery-2.1.3.min.js"></script>
|
||||
<script src="//code.jquery.com/ui/1.11.2/jquery-ui.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rubber Duck Booking Tool</h1>
|
||||
{% if user.is_authenticated %}
|
||||
<div>
|
||||
Logged in as {{ user }}
|
||||
<a href="{% url 'accounts:logout' %}">Logout</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<a href="{% url 'accounts:login' %}">Login</a> or <a href="{% url 'accounts:register' %}">Register</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'booking:list' %}">Home</a>
|
||||
<a href="{% url 'booking:terms' %}">Terms and Conditions</a>
|
||||
<a href="{% url 'booking:vocabulary' %}">Vocabulary</a>
|
||||
<a href="{% url 'booking:disclaimer' %}">Disclaimer</a>
|
||||
{% block body %}{% endblock %}
|
||||
<script type="text/javascript">
|
||||
{% block endscript %}{% endblock %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
0
booking/templatetags/__init__.py
Normal file
0
booking/templatetags/__init__.py
Normal file
101
booking/templatetags/booking_tags.py
Normal file
101
booking/templatetags/booking_tags.py
Normal file
@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
Template tags for the booking templates
|
||||
"""
|
||||
|
||||
from django import template
|
||||
import math
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def is_number(string):
|
||||
"""
|
||||
Check if s is a number in string representation
|
||||
"""
|
||||
|
||||
try:
|
||||
float(string)
|
||||
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@register.filter
|
||||
def age_format(value, arg=None):
|
||||
"""
|
||||
Create human readable string from the duck age
|
||||
"""
|
||||
|
||||
if not is_number(value):
|
||||
return value
|
||||
|
||||
ret = u""
|
||||
|
||||
years = math.floor(value / 31536000)
|
||||
remainder = value % 31536000
|
||||
|
||||
if years > 0:
|
||||
ret += u"%d year%s" % (years, "" if years == 1 else "s")
|
||||
|
||||
if arg != None:
|
||||
return ret
|
||||
|
||||
months = math.floor(remainder / 2592000)
|
||||
remainder = remainder % 2592000
|
||||
|
||||
if months > 0:
|
||||
if ret != "":
|
||||
ret += " "
|
||||
|
||||
ret += u"%d month%s" % (months, "" if months == 1 else "s")
|
||||
|
||||
if arg != None:
|
||||
return ret
|
||||
|
||||
days = math.floor(remainder / 86400)
|
||||
remainder = remainder % 86400
|
||||
|
||||
if arg != None and days == 0:
|
||||
days = 1
|
||||
|
||||
if days > 0:
|
||||
if ret != "":
|
||||
ret += " "
|
||||
|
||||
ret += u"%d day%s" % (days, "" if days == 1 else "s")
|
||||
|
||||
if arg != None:
|
||||
return ret
|
||||
|
||||
if arg != None:
|
||||
raise RuntimeError("Value is strange, we should never arrive here!")
|
||||
|
||||
hours = math.floor(remainder / 3600)
|
||||
remainder = remainder % 3600
|
||||
|
||||
if hours > 0:
|
||||
if ret != "":
|
||||
ret += " "
|
||||
|
||||
ret += u"%d hour%s" % (hours, "" if hours == 1 else "s")
|
||||
|
||||
minutes = math.floor(remainder / 60)
|
||||
|
||||
if minutes > 0:
|
||||
if ret != "":
|
||||
ret += " "
|
||||
|
||||
ret += u"%d minute%s" % (minutes, "" if minutes == 1 else "s")
|
||||
|
||||
seconds = round(remainder % 60)
|
||||
|
||||
if seconds > 0:
|
||||
if ret != "":
|
||||
ret += " "
|
||||
|
||||
ret += u"%d second%s" % (seconds, "" if seconds == 1 else "s")
|
||||
|
||||
if ret == "":
|
||||
ret = "a few moments"
|
||||
|
||||
return ret
|
513
booking/tests.py
Normal file
513
booking/tests.py
Normal file
@ -0,0 +1,513 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests for the Duck Booking Tool frontend
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, Client, override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from .ducklevel import level_to_up_minutes, level_to_down_minutes, minutes_to_level
|
||||
from .templatetags import booking_tags
|
||||
from .models import Duck, Competence, DuckCompetence, Species, \
|
||||
Location, Booking, DuckName, DuckNameVote
|
||||
from .apps import max_level_check
|
||||
|
||||
class FrontTest(TestCase):
|
||||
"""
|
||||
Test case for the front end
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_index_page(self):
|
||||
"""
|
||||
Test for the existence of the main page
|
||||
"""
|
||||
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_vocabulary_page(self):
|
||||
"""
|
||||
Test for the existence of the vocabulary page
|
||||
"""
|
||||
|
||||
response = self.client.get('/vocabulary.html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_terms_page(self):
|
||||
"""
|
||||
Test for the existence of the terms page
|
||||
"""
|
||||
|
||||
response = self.client.get('/terms.html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_disclaimer_page(self):
|
||||
"""
|
||||
Test for the existence of the disclaimer page
|
||||
"""
|
||||
|
||||
response = self.client.get('/disclaimer.html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
class DuckCompLevelTest(TestCase):
|
||||
"""
|
||||
Test case for competence level calculation
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
user = User.objects.create_user(username='test', password='test')
|
||||
|
||||
species = Species.objects.create(name='test species')
|
||||
|
||||
location = Location.objects.create(name='test location')
|
||||
|
||||
duck = Duck.objects.create(species=species,
|
||||
location=location,
|
||||
donated_by=user)
|
||||
|
||||
comp = Competence.objects.create(name='testing',
|
||||
added_by=user)
|
||||
|
||||
self.duckcomp = DuckCompetence.objects.create(duck=duck,
|
||||
comp=comp,
|
||||
up_minutes=0,
|
||||
down_minutes=0)
|
||||
|
||||
def test_max_minutes(self):
|
||||
"""
|
||||
Test if level can not go above settings.MAX_DUCK_LEVEL)
|
||||
"""
|
||||
|
||||
max_up_minutes = level_to_up_minutes(settings.MAX_DUCK_LEVEL)
|
||||
double_minutes = level_to_up_minutes(settings.MAX_DUCK_LEVEL * 2)
|
||||
max_down_minutes = level_to_down_minutes(settings.MAX_DUCK_LEVEL)
|
||||
|
||||
level = minutes_to_level(max_up_minutes, 0)
|
||||
self.assertEqual(level, settings.MAX_DUCK_LEVEL)
|
||||
|
||||
level = minutes_to_level(max_up_minutes + 1, 0)
|
||||
self.assertEqual(level, settings.MAX_DUCK_LEVEL)
|
||||
|
||||
level = minutes_to_level(double_minutes, 0)
|
||||
self.assertEqual(level, settings.MAX_DUCK_LEVEL)
|
||||
|
||||
level = minutes_to_level(0, max_down_minutes)
|
||||
self.assertEqual(level, settings.MAX_DUCK_LEVEL)
|
||||
|
||||
level = minutes_to_level(0, max_down_minutes + 1)
|
||||
self.assertEqual(level, settings.MAX_DUCK_LEVEL)
|
||||
|
||||
def test_conversions(self):
|
||||
"""
|
||||
Test minutes to level conversations
|
||||
"""
|
||||
|
||||
for i in range(1, settings.MAX_DUCK_LEVEL):
|
||||
up_minutes = level_to_up_minutes(i)
|
||||
down_minutes = level_to_down_minutes(i)
|
||||
|
||||
up_level = minutes_to_level(up_minutes, 0)
|
||||
down_level = minutes_to_level(0, down_minutes)
|
||||
|
||||
self.assertEqual(up_level, i, msg="Test failed for value %d" % i)
|
||||
self.assertEqual(
|
||||
down_level, i,
|
||||
msg="Test failed for value %d" % i)
|
||||
|
||||
def test_level_to_minutes(self):
|
||||
"""
|
||||
Test level to minutes conversations
|
||||
"""
|
||||
|
||||
self.assertEqual(level_to_up_minutes(0), 0)
|
||||
self.assertEqual(level_to_up_minutes(1), 20)
|
||||
self.assertEqual(level_to_up_minutes(2), 200)
|
||||
self.assertEqual(level_to_up_minutes(3), 2000)
|
||||
self.assertEqual(level_to_up_minutes(4), 20000)
|
||||
self.assertEqual(level_to_up_minutes(5), 200000)
|
||||
|
||||
self.assertEqual(level_to_down_minutes(0), 0)
|
||||
self.assertEqual(level_to_down_minutes(1), 200)
|
||||
self.assertEqual(level_to_down_minutes(2), 2000)
|
||||
self.assertEqual(level_to_down_minutes(3), 20000)
|
||||
self.assertEqual(level_to_down_minutes(4), 200000)
|
||||
self.assertEqual(level_to_down_minutes(5), 2000000)
|
||||
|
||||
def test_no_comp(self):
|
||||
"""
|
||||
Test if level equals 0 if minutes count is 0
|
||||
"""
|
||||
|
||||
self.duckcomp.up_minutes = 0
|
||||
self.duckcomp.down_minutes = 0
|
||||
self.assertEquals(self.duckcomp.level(), 0)
|
||||
|
||||
def test_comp_levels(self):
|
||||
"""
|
||||
Test competence level calculation
|
||||
"""
|
||||
|
||||
self.duckcomp.down_minutes = 0
|
||||
|
||||
for lvl in range(1, settings.MAX_DUCK_LEVEL):
|
||||
minutes = level_to_up_minutes(lvl)
|
||||
self.duckcomp.up_minutes = minutes
|
||||
self.assertEqual(self.duckcomp.level(), lvl)
|
||||
|
||||
def test_high_minutes(self):
|
||||
"""
|
||||
Test duck level calculation with a very high amount of minutes
|
||||
"""
|
||||
|
||||
self.duckcomp.up_minutes = level_to_up_minutes(settings.MAX_DUCK_LEVEL)
|
||||
self.duckcomp.down_minutes = level_to_down_minutes(settings.MAX_DUCK_LEVEL)
|
||||
self.assertEqual(self.duckcomp.level(), settings.MAX_DUCK_LEVEL)
|
||||
|
||||
class DuckAgeTest(TestCase):
|
||||
"""
|
||||
Tests related to duck age
|
||||
"""
|
||||
|
||||
def test_duck_is_from_the_future(self):
|
||||
"""
|
||||
Test if the duck came from the future (ie. donation time is in
|
||||
the future)
|
||||
"""
|
||||
|
||||
future_duck = Duck(donated_at=timezone.now() + datetime.timedelta(days=2))
|
||||
self.assertEqual(future_duck.age(), -1)
|
||||
|
||||
def test_duck_age_formatter(self):
|
||||
"""
|
||||
Test duck age formatter
|
||||
"""
|
||||
|
||||
self.assertEqual(booking_tags.age_format("aoeu"), "aoeu")
|
||||
self.assertEqual(booking_tags.age_format(0), "a few moments")
|
||||
self.assertEqual(booking_tags.age_format(1), "1 second")
|
||||
self.assertEqual(booking_tags.age_format(2), "2 seconds")
|
||||
self.assertEqual(booking_tags.age_format(60), "1 minute")
|
||||
self.assertEqual(booking_tags.age_format(61), "1 minute 1 second")
|
||||
self.assertEqual(booking_tags.age_format(62), "1 minute 2 seconds")
|
||||
self.assertEqual(booking_tags.age_format(120), "2 minutes")
|
||||
self.assertEqual(booking_tags.age_format(3600), "1 hour")
|
||||
self.assertEqual(booking_tags.age_format(3601), "1 hour 1 second")
|
||||
self.assertEqual(booking_tags.age_format(3660), "1 hour 1 minute")
|
||||
self.assertEqual(booking_tags.age_format(3720), "1 hour 2 minutes")
|
||||
self.assertEqual(booking_tags.age_format(7200), "2 hours")
|
||||
self.assertEqual(booking_tags.age_format(86400), "1 day")
|
||||
self.assertEqual(booking_tags.age_format(86401), "1 day 1 second")
|
||||
self.assertEqual(booking_tags.age_format(86460), "1 day 1 minute")
|
||||
self.assertEqual(booking_tags.age_format(90000), "1 day 1 hour")
|
||||
self.assertEqual(booking_tags.age_format(93600), "1 day 2 hours")
|
||||
self.assertEqual(booking_tags.age_format(172800), "2 days")
|
||||
self.assertEqual(booking_tags.age_format(2592000), "1 month")
|
||||
self.assertEqual(booking_tags.age_format(2592001), "1 month 1 second")
|
||||
self.assertEqual(booking_tags.age_format(2592060), "1 month 1 minute")
|
||||
self.assertEqual(booking_tags.age_format(2595600), "1 month 1 hour")
|
||||
self.assertEqual(booking_tags.age_format(2678400), "1 month 1 day")
|
||||
self.assertEqual(booking_tags.age_format(2764800), "1 month 2 days")
|
||||
self.assertEqual(booking_tags.age_format(5184000), "2 months")
|
||||
self.assertEqual(booking_tags.age_format(31536000), "1 year")
|
||||
self.assertEqual(booking_tags.age_format(31536001), "1 year 1 second")
|
||||
self.assertEqual(booking_tags.age_format(31536060), "1 year 1 minute")
|
||||
self.assertEqual(booking_tags.age_format(31539600), "1 year 1 hour")
|
||||
self.assertEqual(booking_tags.age_format(31622400), "1 year 1 day")
|
||||
self.assertEqual(booking_tags.age_format(34128000), "1 year 1 month")
|
||||
self.assertEqual(booking_tags.age_format(36720000), "1 year 2 months")
|
||||
self.assertEqual(booking_tags.age_format(63072000), "2 years")
|
||||
|
||||
class BookingTimeTest(TestCase):
|
||||
"""
|
||||
Test case for calculating booking time and popularity
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
user = User()
|
||||
user.save()
|
||||
|
||||
species = Species.objects.create(name='duck')
|
||||
location = Location.objects.create(name='start')
|
||||
|
||||
self.duck1 = Duck.objects.create(species=species,
|
||||
location=location,
|
||||
donated_by=user)
|
||||
|
||||
competence = Competence.objects.create(name='test',
|
||||
added_by=user)
|
||||
|
||||
now = timezone.now()
|
||||
Booking.objects.create(duck=self.duck1,
|
||||
start_ts=now - datetime.timedelta(days=2),
|
||||
end_ts=now - datetime.timedelta(days=1),
|
||||
user=user,
|
||||
comp_req=competence)
|
||||
|
||||
self.duck2 = Duck.objects.create(species=species,
|
||||
location=location,
|
||||
donated_by=user)
|
||||
|
||||
Booking.objects.create(duck=self.duck2,
|
||||
start_ts=now - datetime.timedelta(days=3),
|
||||
end_ts=now - datetime.timedelta(days=2),
|
||||
user=user,
|
||||
comp_req=competence)
|
||||
|
||||
Booking.objects.create(duck=self.duck2,
|
||||
start_ts=now - datetime.timedelta(days=2),
|
||||
end_ts=now - datetime.timedelta(days=1),
|
||||
user=user,
|
||||
comp_req=competence)
|
||||
|
||||
def test_total_booking_time(self):
|
||||
"""
|
||||
Test total booking time
|
||||
"""
|
||||
|
||||
self.assertEqual(259200, Booking.total_booking_time())
|
||||
|
||||
def test_duck_booking_time(self):
|
||||
"""
|
||||
Test duck booking time
|
||||
"""
|
||||
|
||||
self.assertEqual(86400, Booking.duck_booking_time(self.duck1))
|
||||
self.assertEqual(172800, Booking.duck_booking_time(self.duck2))
|
||||
|
||||
def test_dpx(self):
|
||||
"""
|
||||
Test Duck Popularity indeX calculation
|
||||
"""
|
||||
|
||||
self.assertEqual(1/3, self.duck1.dpx())
|
||||
self.assertEqual(2/3, self.duck2.dpx())
|
||||
|
||||
class TestListing(TestCase):
|
||||
"""
|
||||
Test case for duck listing
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
species = Species.objects.create()
|
||||
loc = Location.objects.create()
|
||||
user = User.objects.create_user(username='test',
|
||||
password='test')
|
||||
|
||||
self.duck = Duck.objects.create(species=species,
|
||||
location=loc,
|
||||
donated_by=user)
|
||||
|
||||
def test_front_page(self):
|
||||
"""
|
||||
Test existence of the front page
|
||||
"""
|
||||
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
self.assertEqual(1, len(response.context['duck_list']))
|
||||
|
||||
class SimilarCompTest(TestCase):
|
||||
"""
|
||||
Test case for competence name fuzzy search
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
admin = User.objects.create_user(username='admin',
|
||||
password='test')
|
||||
|
||||
competence_list = (
|
||||
'Creativity',
|
||||
'Administration',
|
||||
'Perl',
|
||||
'Python',
|
||||
'TCSH',
|
||||
)
|
||||
|
||||
for competence in competence_list:
|
||||
Competence.objects.create(name=competence,
|
||||
added_by=admin)
|
||||
|
||||
def test_good_similar_competences(self):
|
||||
"""
|
||||
Test similar competence list with different inputs
|
||||
"""
|
||||
|
||||
comp_list = Competence.get_similar_comps('perl')
|
||||
self.assertEquals(1, len(comp_list))
|
||||
|
||||
comp_list = Competence.get_similar_comps('pzthon')
|
||||
self.assertEquals(1, len(comp_list))
|
||||
|
||||
comp_list = Competence.get_similar_comps(u'kreativitás')
|
||||
self.assertEqual(1, len(comp_list))
|
||||
|
||||
def test_bad_similar_competence(self):
|
||||
"""
|
||||
Test similar competence list with a totally new and unmatching
|
||||
competence name
|
||||
"""
|
||||
|
||||
comp_list = Competence.get_similar_comps('development')
|
||||
self.assertEqual(0, len(comp_list))
|
||||
|
||||
class BookingTest(TestCase):
|
||||
"""
|
||||
Test duck booking functionality
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.spec = Species.objects.create(name='test')
|
||||
self.loc = Location.objects.create(name='test')
|
||||
self.user = User.objects.create_user(username='test')
|
||||
self.booked_duck = Duck.objects.create(species=self.spec,
|
||||
location=self.loc,
|
||||
donated_by=self.user)
|
||||
self.comp = Competence.objects.create(name='test',
|
||||
added_by=self.user)
|
||||
|
||||
Booking.objects.create(duck=self.booked_duck,
|
||||
user=self.user,
|
||||
comp_req=self.comp)
|
||||
|
||||
def test_booked_duck(self):
|
||||
"""
|
||||
Test if booked duck returns the booking user from booked_by()
|
||||
"""
|
||||
|
||||
self.assertNotEqual(self.booked_duck.booked_by(), None)
|
||||
|
||||
def test_unbooked_duck(self):
|
||||
"""
|
||||
Test if unbooked duck returns None from booked_by()
|
||||
"""
|
||||
|
||||
unbooked_duck = Duck.objects.create(species=self.spec,
|
||||
location=self.loc,
|
||||
donated_by=self.user)
|
||||
self.assertEqual(unbooked_duck.booked_by(), None)
|
||||
|
||||
def test_multiple_booking(self):
|
||||
"""
|
||||
Test error presence in case of multiple bookings for the same
|
||||
duck
|
||||
"""
|
||||
|
||||
Booking.objects.create(duck=self.booked_duck,
|
||||
user=self.user,
|
||||
comp_req=self.comp)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.booked_duck.booked_by()
|
||||
|
||||
class StrTest(TestCase):
|
||||
"""
|
||||
Test case for models’ __str__() method
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username='test')
|
||||
self.location = Location.objects.create(name="A Location")
|
||||
self.species = Species.objects.create(name="Duck")
|
||||
self.competence = Competence.objects.create(name="Testing",
|
||||
added_by=self.user)
|
||||
|
||||
self.duck = Duck.objects.create(name="First Duck",
|
||||
species=self.species,
|
||||
location=self.location,
|
||||
donated_by=self.user)
|
||||
|
||||
def test_location_str(self):
|
||||
self.assertEquals("A Location", self.location.__str__())
|
||||
|
||||
def test_species_str(self):
|
||||
self.assertEquals("Duck", self.species.__str__())
|
||||
|
||||
def test_competence_str(self):
|
||||
self.assertEquals("Testing", self.competence.__str__())
|
||||
|
||||
def test_duck_str(self):
|
||||
self.assertEquals("First Duck", self.duck.__str__())
|
||||
|
||||
def test_duckname_str(self):
|
||||
name_suggestion = DuckName.objects.create(name="New Duck",
|
||||
duck=self.duck,
|
||||
suggested_by=self.user)
|
||||
self.assertEquals("New Duck, suggested by test",
|
||||
name_suggestion.__str__())
|
||||
|
||||
name_suggestion.closed_by = self.user
|
||||
|
||||
self.assertEquals("New Duck, suggested by test <closed>",
|
||||
name_suggestion.__str__())
|
||||
|
||||
def test_ducknamevote_str(self):
|
||||
name_suggestion = DuckName.objects.create(name="New Duck",
|
||||
duck=self.duck,
|
||||
suggested_by=self.user)
|
||||
|
||||
vote = DuckNameVote.objects.create(duck_name=name_suggestion,
|
||||
voter=self.user,
|
||||
upvote=False)
|
||||
|
||||
self.assertEquals("test voted down for New Duck, suggested by test",
|
||||
vote.__str__())
|
||||
|
||||
vote.upvote = True
|
||||
|
||||
self.assertEquals("test voted up for New Duck, suggested by test",
|
||||
vote.__str__())
|
||||
|
||||
def test_duckcompetence_str(self):
|
||||
dcomp = DuckCompetence.objects.create(duck=self.duck,
|
||||
comp=self.competence)
|
||||
|
||||
self.assertEquals("First Duck with +0/-0 minutes in Testing",
|
||||
dcomp.__str__())
|
||||
|
||||
def test_booking_str(self):
|
||||
start = timezone.now()
|
||||
booking = Booking.objects.create(duck=self.duck,
|
||||
user=self.user,
|
||||
comp_req=self.competence,
|
||||
start_ts=start)
|
||||
|
||||
self.assertEquals("First Duck booked by test for Testing since {0}".format(start),
|
||||
booking.__str__())
|
||||
|
||||
class SystemCheckTest(TestCase):
|
||||
@override_settings()
|
||||
def test_max_duck_level_missing(self):
|
||||
del settings.MAX_DUCK_LEVEL
|
||||
|
||||
errors = max_level_check(None)
|
||||
|
||||
self.assertGreater(
|
||||
len([e for e in errors if e.id == 'booking.E001']),
|
||||
0)
|
||||
|
||||
def test_max_duck_level_illegal(self):
|
||||
with self.settings(MAX_DUCK_LEVEL=0):
|
||||
errors = max_level_check(None)
|
||||
|
||||
self.assertGreater(
|
||||
len([e for e in errors if e.id == 'booking.E002']),
|
||||
0
|
||||
)
|
||||
|
||||
with self.settings(MAX_DUCK_LEVEL=1.1):
|
||||
errors = max_level_check(None)
|
||||
|
||||
self.assertGreater(
|
||||
len([e for e in errors if e.id == 'booking.E003']),
|
||||
0
|
||||
)
|
28
booking/urls.py
Normal file
28
booking/urls.py
Normal file
@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
URL definitions for the Duck Booking Tool frontend
|
||||
"""
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from .views import DuckListView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', DuckListView.as_view(), name='list'),
|
||||
url(
|
||||
r'^vocabulary.html$',
|
||||
TemplateView.as_view(template_name='booking/vocabulary.html'),
|
||||
name='vocabulary'
|
||||
),
|
||||
url(
|
||||
r'^terms.html$',
|
||||
TemplateView.as_view(template_name='booking/terms.html'),
|
||||
name='terms'
|
||||
),
|
||||
url(
|
||||
r'^disclaimer.html$',
|
||||
TemplateView.as_view(template_name='booking/disclaimer.html'),
|
||||
name='disclaimer'
|
||||
),
|
||||
]
|
19
booking/views.py
Normal file
19
booking/views.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
Views for the Duck Booking Tool frontend
|
||||
"""
|
||||
|
||||
from django.views import generic
|
||||
|
||||
from .models import Duck
|
||||
|
||||
class DuckListView(generic.ListView):
|
||||
"""
|
||||
View for duck listing (the main page)
|
||||
"""
|
||||
|
||||
template_name = 'booking/duck_list.html'
|
||||
context_object_name = 'duck_list'
|
||||
|
||||
def get_queryset(self):
|
||||
return Duck.objects.all()
|
@ -1,6 +0,0 @@
|
||||
def ducks_get():
|
||||
return 'do some magic!'
|
||||
|
||||
def duck_get():
|
||||
return 'do some duck magic!'
|
||||
|
7
dev-requirements.txt
Normal file
7
dev-requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
-r requirements.txt
|
||||
coverage==4.0.1
|
||||
django-coverage-plugin==1.0
|
||||
django-juno-testrunner==0.3.1
|
||||
pylint==1.4.4
|
||||
pylint-django==0.6.1
|
||||
|
0
duckbook/__init__.py
Normal file
0
duckbook/__init__.py
Normal file
98
duckbook/settings.py
Normal file
98
duckbook/settings.py
Normal file
@ -0,0 +1,98 @@
|
||||
"""
|
||||
Django settings for duckbook project.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.7/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import dj_database_url
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '-j$x6v))e4=6qy0vm@!@z7-y3k18s2d2j1r&*06x9%rmm$0w1s'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
TEMPLATE_DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_FOR', 'https')
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'accounts',
|
||||
'booking',
|
||||
'api',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'duckbook.urls'
|
||||
|
||||
WSGI_APPLICATION = 'duckbook.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
|
||||
os.environ.setdefault('DATABASE_URL', 'sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'))
|
||||
DATABASES = {
|
||||
'default': dj_database_url.config()
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'Europe/Budapest'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = 'static'
|
||||
|
||||
# The following will be always True on local machines, which makes
|
||||
# coverage data ugly
|
||||
if STATIC_ROOT != 'static': # pragma: no cover
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static'),
|
||||
)
|
||||
|
||||
TEST_RUNNER = 'junorunner.testrunner.TestSuiteRunner'
|
||||
|
||||
MAX_DUCK_LEVEL = 5
|
||||
COMP_WARN_LEVEL = 2
|
||||
MIN_FUZZY_SIMILARITY = 75
|
30
duckbook/urls.py
Normal file
30
duckbook/urls.py
Normal file
@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8
|
||||
"""
|
||||
Main URL definitions file
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^static/(?P<path>.*)$',
|
||||
'django.views.static.serve',
|
||||
{'document_root': settings.STATIC_ROOT}
|
||||
),
|
||||
url(
|
||||
r'^admin/',
|
||||
include(admin.site.urls)),
|
||||
url(
|
||||
r'^accounts/',
|
||||
include('accounts.urls', namespace='accounts')),
|
||||
url(
|
||||
r'^api/v1/',
|
||||
include('api.urls', namespace='api')),
|
||||
url(
|
||||
'',
|
||||
include('booking.urls', namespace='booking')),
|
||||
]
|
14
duckbook/wsgi.py
Normal file
14
duckbook/wsgi.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""
|
||||
WSGI config for duckbook project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "duckbook.settings")
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
application = get_wsgi_application()
|
10
manage.py
Executable file
10
manage.py
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "duckbook.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
@ -1,5 +1,10 @@
|
||||
Flask==0.11.1
|
||||
Flask-RESTful==0.3.5
|
||||
connexion==1.0.129
|
||||
gunicorn
|
||||
flask-login
|
||||
Django==1.8.5
|
||||
WebTest==2.0.17
|
||||
django-webtest==1.7.7
|
||||
django-js-reverse==0.3.3
|
||||
djangorestframework==3.2.4
|
||||
fuzzywuzzy==0.4.0
|
||||
python-Levenshtein==0.12.0
|
||||
gunicorn==19.3.0
|
||||
dj-database-url==0.3.0
|
||||
psycopg2
|
||||
|
1
runtime.txt
Normal file
1
runtime.txt
Normal file
@ -0,0 +1 @@
|
||||
python-2.7.11
|
@ -1,67 +0,0 @@
|
||||
---
|
||||
swagger: "2.0"
|
||||
info:
|
||||
version: "0.1.0"
|
||||
title: "Rubber Duck Booking Tool API"
|
||||
basePath: "/api/v1"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/ducks:
|
||||
get:
|
||||
tags:
|
||||
- "default_controller"
|
||||
description: "Get the list of all ducks"
|
||||
operationId: "controllers.default_controller.ducks_get"
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
description: "An object which contains metadata and an array of ducks"
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Duck'
|
||||
security:
|
||||
- api_key: []
|
||||
/duck:
|
||||
get:
|
||||
tags:
|
||||
- "default_controller"
|
||||
operationId: "controllers.default_controller.duck_get"
|
||||
responses:
|
||||
200:
|
||||
description: "All data regarding the specified duck"
|
||||
securityDefinitions:
|
||||
api_key:
|
||||
type: "apiKey"
|
||||
name: "token"
|
||||
in: "header"
|
||||
definitions:
|
||||
Duck:
|
||||
type: object
|
||||
properties:
|
||||
duck_id:
|
||||
type: number
|
||||
description: ID number of the duck
|
||||
name:
|
||||
type: string
|
||||
description: The name of the duck
|
||||
color:
|
||||
type: string
|
||||
description: Color of the duck
|
||||
#species = models.ForeignKey(Species)
|
||||
#location = models.ForeignKey(Location)
|
||||
#competencies:
|
||||
# type: array
|
||||
# items: strings
|
||||
# description: A list of competencies the duck has
|
||||
#donated_by = models.ForeignKey(User)
|
||||
#donated_at = models.DateTimeField(default=timezone.now)
|
||||
#adopted_by = models.ForeignKey(User, related_name='adopted_ducks', null=True, blank=True)
|
||||
#adopted_at = models.DateTimeField(null=True, blank=True)
|
||||
#bookings = models.ManyToManyField(User, through='Booking', related_name='+')
|
||||
#on_holiday_since = models.DateTimeField(null=True, blank=True)
|
||||
#on_holiday_until = models.DateTimeField(null=True, blank=True)
|
||||
|
Loading…
Reference in New Issue
Block a user