Compare commits

...

No commits in common. "master" and "django" have entirely different histories.

48 changed files with 2151 additions and 124 deletions

4
.coveragerc Normal file
View File

@ -0,0 +1,4 @@
[report]
omit =
venv/*
duckbook/wsgi.py

11
.gitignore vendored
View File

@ -1,3 +1,10 @@
*~
*.pyc
/db.sqlite3
/venv/
.env
**/*.pyc
/static/
/.coverage
/htmlcov/
/test_failures.txt
/test_rerun.txt
/pylint/

8
.pylintrc Normal file
View File

@ -0,0 +1,8 @@
[MASTER]
load-plugins=pylint_django
[REPORTS]
files-output=yes

View File

@ -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
View 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
View 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"

View File

@ -1 +1 @@
web: gunicorn app:app --log-file -
web: gunicorn duckbook.wsgi --log-file=-

View 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).

View 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 %}

View 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
View 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
View 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
View 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
View File

77
api/serializers.py Normal file
View 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',)

View 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
View 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 doesnt 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
View 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
View 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
View File

@ -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
View 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
View File

@ -0,0 +1,5 @@
"""
Booking module of the Duck Booking Tool
"""
default_app_config = 'booking.apps.BookingConfig'

18
booking/admin.py Normal file
View 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
View 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
View 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
View 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)

View 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;
}

View 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 %}

View 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 %}

View File

@ -0,0 +1,5 @@
{% extends 'front_template.html' %}
{% block body %}
<h2>Terms and Conditions</h2>
{% endblock %}

View 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 %}

View 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>

View File

View 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
View 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
View 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
View 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()

View File

@ -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
View 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
View File

98
duckbook/settings.py Normal file
View 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
View 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
View 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
View 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)

View File

@ -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
View File

@ -0,0 +1 @@
python-2.7.11

View File

@ -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)