Skip to content

Commit 0af5fe9

Browse files
committed
first commit
0 parents  commit 0af5fe9

31 files changed

+892
-0
lines changed

.coverage

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!coverage.py: This is a private format, don't read it directly!{"lines":{"/Users/da/code/misc/u2me-assignment/u2me/polls/serializers.py":[1,2,4,7,8,10,11,12],"/Users/da/code/misc/u2me-assignment/u2me/polls/tests/test_views.py":[1,3,4,6,9,10,12,14,16,17,19,21,22,24,25,26,28,29],"/Users/da/code/misc/u2me-assignment/u2me/polls/__init__.py":[1],"/Users/da/code/misc/u2me-assignment/u2me/polls/tests/test_models.py":[1,3,4,6,7,10,11,13,14,16,21,22,23],"/Users/da/code/misc/u2me-assignment/u2me/polls/views.py":[1,2,3,4,5,7,10,11,12,14,19,20,21,24,25,26,29],"/Users/da/code/misc/u2me-assignment/u2me/polls/tests/__init__.py":[1],"/Users/da/code/misc/u2me-assignment/u2me/polls/urls.py":[1,2,4,5,8,9,11,14,15,16,17,18],"/Users/da/code/misc/u2me-assignment/u2me/polls/api_views.py":[1,2,4,5,8,9,10,11,12],"/Users/da/code/misc/u2me-assignment/u2me/polls/data.py":[1,2,5,6,7,8],"/Users/da/code/misc/u2me-assignment/u2me/polls/tests/test_data.py":[1,3,4,7,8,10,12,14,15,17,19,20,22,23,24,25,26],"/Users/da/code/misc/u2me-assignment/u2me/polls/tests/factories.py":[1,2,3,5,8,9,10,12,15,16,17,19],"/Users/da/code/misc/u2me-assignment/u2me/polls/apps.py":[1,3,6,7],"/Users/da/code/misc/u2me-assignment/u2me/polls/admin.py":[1,3,5,6],"/Users/da/code/misc/u2me-assignment/u2me/polls/models.py":[1,2,4,5,8,9,10,12,13,14,16,17,20,21,22,23,25]}}

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Python
2+
*.py[cod]
3+
4+
# Installer logs
5+
pip-log.txt
6+
7+
# Misc UNIX Files
8+
nohup.out
9+
venv/
10+
11+
# Database
12+
db.sqlite3
13+
14+
# Client-side
15+
npm-debug.log
16+
node_modules/
17+
18+
# OSX stuff
19+
.DS_Store

client/static/app.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
var App = angular.module('App', []);
2+
3+
App.config(function($interpolateProvider, $httpProvider) {
4+
$interpolateProvider.startSymbol('{[{');
5+
$interpolateProvider.endSymbol('}]}');
6+
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
7+
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
8+
});
9+
10+
App.controller('AppController', function ($scope, $http) {
11+
$scope.items = [];
12+
13+
$http({
14+
method: 'GET',
15+
url: '/polls/questions/'
16+
}).then(success, error);
17+
18+
function success(response) {
19+
console.log(response.data);
20+
$scope.items = response.data;
21+
}
22+
23+
function error(response) {
24+
console.log('Error retrieving items.');
25+
}
26+
});

client/static/lib.css

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/static/lib.js

Lines changed: 334 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

manage.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env python
2+
import os
3+
import sys
4+
5+
if __name__ == "__main__":
6+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "u2me.settings")
7+
8+
from django.core.management import execute_from_command_line
9+
10+
execute_from_command_line(sys.argv)

polls/__init__.py

Whitespace-only changes.

polls/admin.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.contrib import admin
2+
3+
from .models import Question, Choice
4+
5+
admin.site.register(Question)
6+
admin.site.register(Choice)

polls/api_views.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from rest_framework.response import Response
2+
from rest_framework.viewsets import ViewSet
3+
from django_timer import Timer
4+
5+
from polls.serializers import QuestionSerializer
6+
from polls.data import list_all_questions_with_total_votes
7+
8+
9+
class QuestionSet(ViewSet):
10+
def list(self, request):
11+
with Timer() as t:
12+
questions = list_all_questions_with_total_votes()
13+
serializer = QuestionSerializer(questions, many=True)
14+
res = {
15+
'data': serializer.data,
16+
'took': t.delta
17+
}
18+
return Response(res)

polls/apps.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from __future__ import unicode_literals
2+
3+
from django.apps import AppConfig
4+
5+
6+
class PollsConfig(AppConfig):
7+
name = 'polls'

polls/data.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.db.models import Sum
2+
from django.utils import timezone
3+
from polls.models import Question
4+
5+
6+
def list_all_questions_with_total_votes():
7+
return Question.objects.all() \
8+
.filter(pub_date__lte=timezone.now()) \
9+
.annotate(total_votes=Sum('choice__votes')) \
10+
.order_by('-total_votes', '-pub_date')

polls/migrations/0001_initial.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9.7 on 2017-01-22 23:43
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='Choice',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('choice_text', models.CharField(max_length=200)),
22+
('votes', models.IntegerField(default=0)),
23+
],
24+
),
25+
migrations.CreateModel(
26+
name='Question',
27+
fields=[
28+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29+
('question_text', models.CharField(max_length=200)),
30+
('pub_date', models.DateTimeField(verbose_name=b'date published')),
31+
],
32+
),
33+
migrations.AddField(
34+
model_name='choice',
35+
name='question',
36+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.Question'),
37+
),
38+
]

polls/migrations/__init__.py

Whitespace-only changes.

polls/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import unicode_literals
2+
import datetime
3+
4+
from django.db import models
5+
from django.utils import timezone
6+
7+
8+
class Question(models.Model):
9+
question_text = models.CharField(max_length=200)
10+
pub_date = models.DateTimeField('date published')
11+
12+
def was_published_recently(self):
13+
now = timezone.now()
14+
return self.pub_date >= now - datetime.timedelta(days=1) and now >= self.pub_date
15+
16+
def __str__(self):
17+
return self.question_text
18+
19+
20+
class Choice(models.Model):
21+
question = models.ForeignKey(Question, on_delete=models.CASCADE)
22+
choice_text = models.CharField(max_length=200)
23+
votes = models.IntegerField(default=0)
24+
25+
def __str__(self):
26+
return self.choice_text

polls/serializers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from rest_framework.serializers import ModelSerializer
2+
from rest_framework import serializers
3+
4+
from polls.models import Question
5+
6+
7+
class QuestionSerializer(ModelSerializer):
8+
total_votes = serializers.FloatField(max_value=None, min_value=None)
9+
10+
class Meta:
11+
model = Question
12+
fields = ('question_text', 'total_votes')

polls/templates/polls/detail.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<h1>{{ question.question_text }}</h1>
2+
3+
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
4+
5+
<form action="{% url 'polls:vote' question.id %}" method="post">
6+
{% csrf_token %}
7+
{% for choice in question.choice_set.all %}
8+
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
9+
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
10+
{% endfor %}
11+
<input type="submit" value="Vote" />
12+
</form>

polls/templates/polls/index.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{% load staticfiles %}
2+
3+
<!DOCTYPE html>
4+
<html lang="en" ng-app="App">
5+
6+
<head>
7+
<meta charset="UTF-8">
8+
<title>Poll</title>
9+
10+
<script src="{% static 'lib.js' %}"></script>
11+
<script src="{% static 'app.js' %}"></script>
12+
13+
<link rel="stylesheet" type="text/css" href="{% static 'lib.css' %}">
14+
<link rel="stylesheet" type="text/css" href="{% static 'ranking.css' %}">
15+
</head>
16+
17+
<body ng-controller="AppController">
18+
19+
<div class="container-fluid">
20+
21+
<p>Took {{ took | floatformat:-6 }} seconds for query</p>
22+
<div class="row">
23+
24+
<div class="col-sm-12 table">
25+
26+
<!-- Table header -->
27+
<div class="row header">
28+
<div class="col-xs-6">Votes</div>
29+
<div class="col-xs-6">Total votes</div>
30+
</div>
31+
32+
{% if latest_question_list %}
33+
{% for question in latest_question_list %}
34+
<div class="row">
35+
<div class="col-xs-6"><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></div>
36+
<div class="col-xs-6">{{ question.total_votes }}</div>
37+
</div>
38+
{% endfor %}
39+
{% else %}
40+
<p>No polls are available.</p>
41+
{% endif %}
42+
43+
</div>
44+
</div>
45+
</div>
46+
</body>
47+
48+
</html>

polls/templates/polls/results.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<h1>{{ question.question_text }}</h1>
2+
3+
<ul>
4+
{% for choice in question.choice_set.all %}
5+
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
6+
{% endfor %}
7+
</ul>
8+
9+
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

polls/tests/__init__.py

Whitespace-only changes.

polls/tests/factories.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from factory.declarations import SubFactory
2+
from factory.django import DjangoModelFactory
3+
from django.utils import timezone
4+
5+
from polls.models import Question, Choice
6+
7+
8+
class QuestionFactory(DjangoModelFactory):
9+
class Meta:
10+
model = Question
11+
12+
pub_date = timezone.now()
13+
14+
15+
class ChoiceFactory(DjangoModelFactory):
16+
class Meta:
17+
model = Choice
18+
19+
question = SubFactory(QuestionFactory)

polls/tests/test_data.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from django.test.testcases import TestCase
2+
3+
from .factories import QuestionFactory, ChoiceFactory
4+
from polls.data import list_all_questions_with_total_votes
5+
6+
7+
class QuestionsApiViewTest(TestCase):
8+
@classmethod
9+
def setUpTestData(cls):
10+
super(QuestionsApiViewTest, cls).setUpTestData()
11+
12+
cls.question_a = QuestionFactory()
13+
14+
cls.choice_a = ChoiceFactory(question=cls.question_a, votes=2)
15+
cls.choice_b = ChoiceFactory(question=cls.question_a, votes=2)
16+
17+
cls.question_b = QuestionFactory()
18+
19+
cls.choice_c = ChoiceFactory(question=cls.question_b, votes=3)
20+
cls.choice_d = ChoiceFactory(question=cls.question_b, votes=3)
21+
22+
def test_list_all_questions_returns_correct_vote_counts(self):
23+
questions = list_all_questions_with_total_votes()
24+
self.assertEqual(len(questions), 2)
25+
self.assertEqual(questions[0].total_votes, 6)
26+
self.assertEqual(questions[1].total_votes, 4)

polls/tests/test_models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import datetime
2+
3+
from django.utils import timezone
4+
from django.test import TestCase
5+
6+
from polls.models import Question
7+
from .factories import QuestionFactory
8+
9+
10+
class QuestionTests(TestCase):
11+
@classmethod
12+
def setUpTestData(cls):
13+
super(QuestionTests, cls).setUpTestData()
14+
pass
15+
16+
def test_was_published_recently_with_future_question(self):
17+
"""
18+
was_published_recently() should return False for questions whose
19+
pub_date is in the future.
20+
"""
21+
time = timezone.now() + datetime.timedelta(days=30)
22+
future_question = QuestionFactory(pub_date=time)
23+
self.assertIs(future_question.was_published_recently(), False)

polls/tests/test_views.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from json import loads
2+
3+
from django.test.testcases import TestCase
4+
from django.urls import reverse
5+
6+
from .factories import QuestionFactory, ChoiceFactory
7+
8+
9+
class QuestionsApiViewTest(TestCase):
10+
@classmethod
11+
def setUpTestData(cls):
12+
super(QuestionsApiViewTest, cls).setUpTestData()
13+
14+
cls.question_a = QuestionFactory()
15+
16+
cls.choice_a = ChoiceFactory(question=cls.question_a)
17+
cls.choice_b = ChoiceFactory(question=cls.question_a)
18+
19+
cls.question_b = QuestionFactory()
20+
21+
cls.choice_c = ChoiceFactory(question=cls.question_b)
22+
cls.choice_d = ChoiceFactory(question=cls.question_b)
23+
24+
def test_json_view_returns_all_quesitons(self):
25+
response = self.client.get(reverse('polls:api-question-list'))
26+
self.assertEqual(200, response.status_code)
27+
28+
actual = loads(response.content)
29+
self.assertEqual(2, len(actual))

polls/urls.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from django.conf.urls import url, include
2+
from rest_framework.routers import DefaultRouter
3+
4+
import views
5+
from .api_views import QuestionSet
6+
7+
8+
router = DefaultRouter()
9+
router.register(r'questions', QuestionSet, base_name='api-question')
10+
11+
app_name = 'polls'
12+
13+
urlpatterns = [
14+
url(r'^$', views.IndexView.as_view(), name='index'),
15+
url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
16+
url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
17+
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
18+
url(r'^', include(router.urls)),
19+
]

0 commit comments

Comments
 (0)