diff --git a/.gitignore b/.gitignore index f84e705..65421a3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,10 @@ *.sublime-* *.sqlite3* *.sass-cache +*app.js +*services.js +*controllers.js +*directives.js /node_modules diff --git a/djangular/frontend/app.coffee b/djangular/frontend/app.coffee index 9d29d2c..d2c6712 100644 --- a/djangular/frontend/app.coffee +++ b/djangular/frontend/app.coffee @@ -28,6 +28,16 @@ app.config(($interpolateProvider, $stateProvider, $urlRouterProvider) -> question.get($stateParams.questionId) return question ) + .state('questionResults' + url: '/{questionId:[0-9]+}/results' + templateUrl: 'questionResults' + controller: 'questionResultsController' + resolve: + question : ($stateParams, $log, Question)-> + question = new Question(null) + question.get($stateParams.questionId) + return question + ) ) diff --git a/djangular/frontend/controllers.coffee b/djangular/frontend/controllers.coffee index c60a5e6..9d46530 100644 --- a/djangular/frontend/controllers.coffee +++ b/djangular/frontend/controllers.coffee @@ -2,21 +2,31 @@ controllers = angular.module('pollApp.controllers', []) controllers.controller('questionListController', ($scope, $state, $log, questions) -> $scope.questions = questions.all + $scope.sortQuestions = (questions) -> + questions.sort((a,b) -> b.upvotes - a.upvotes ) + + $scope.upvote = (question) -> + question.upvote() + $scope.questions = $scope.sortQuestions($scope.questions) + + $scope.downvote = (question) -> + question.downvote() + $scope.questions = $scope.sortQuestions($scope.questions) ) controllers.controller('questionDetailController', ($scope, $state, $log, question) -> $scope.question = question - $scope.voted = false $scope.voteChoice = 0 $scope.vote = -> - for choice in $scope.question.choices - if choice.id == parseInt($scope.voteChoice) - choice.votes+=1 - $scope.question.totalVotes+=1 - choice.update() - break - $scope.voted = true + question.voteOnChoice(parseInt($scope.voteChoice), () -> $state.go('questionResults', {questionId:question.id})) ) +controllers.controller('questionResultsController', ($scope, $state, $log, question) -> + $scope.question = question + + $scope.submitFeedback = -> + question.addFeedback($scope.feedbackToSubmit.feedback_text) + $scope.feedbackToSubmit.feedback_text = '' +) diff --git a/djangular/frontend/services.coffee b/djangular/frontend/services.coffee index 7193d14..6786d09 100644 --- a/djangular/frontend/services.coffee +++ b/djangular/frontend/services.coffee @@ -8,18 +8,40 @@ services.factory('Choice', ($http, $log)-> @id = data.id @votes = data.votes - update : -> + update : (cb) -> data = {'votes' : @votes, 'choice_text' : @choice_text} $http({method: 'PUT', url: '/polls/choices/' + @id + '/', data:data}) .success (data) => $log.info("Succesfully voted") + cb() .error (data) => $log.info("Failed to vote.") + cb() return Choice ) -services.factory('Question', (Choice, $http, $log) -> +services.factory('Feedback', ($http, $log)-> + #log = $log + class Feedback + constructor: (data) -> + @feedback_text = data.feedback_text + @id = data.id + @pub_date = data.pub_date + @pub_date_fmt = moment(data.pub_date).calendar() + + update : -> + data = {'feedback_text' : @feedback_text} + $http({method: 'PUT', url: '/polls/feedbacks/' + @id + '/', data:data}) + .success (data) => + $log.info("Succesfully left feedback") + .error (data) => + $log.info("Failed to leave feedback.") + + return Feedback +) + +services.factory('Question', (Choice, Feedback, $http, $log) -> class Question constructor : (data) -> if data != null @@ -28,11 +50,41 @@ services.factory('Question', (Choice, $http, $log) -> @question_text = data.question_text @id = data.id @choices = [] + @feedbacks = [] @totalVotes = 0 + @lastResponseDate = data.last_response_date + @lastResponseDateFmt = moment(data.last_response_date).fromNow() + @upvotes = data.upvotes for choice in data.choices c = new Choice(choice) @totalVotes += c.votes @choices.push(new Choice(choice)) + for feedback in data.feedbacks + @feedbacks.push(new Feedback(feedback)) + + upvote : () -> + if @upvotes + @upvotes = @upvotes + 1 + else + @upvotes = 1 + $http({method: 'POST', url: '/polls/questions/' + @id + '/upvote'}) + .success (data) => + @upvotes = data.upvotes + $log.info("Succesfully upvoted") + .error (data) => + $log.info("Failed to upvote.") + + downvote : () -> + if @upvotes + @upvotes = @upvotes - 1 + else + @upvotes = -1 + $http({method: 'POST', url: '/polls/questions/' + @id + '/downvote'}) + .success (data) => + @upvotes = data.upvotes + $log.info("Succesfully downvoted") + .error (data) => + $log.info("Failed to downvote.") get : (questionId) -> $http({method: 'GET', url: '/polls/questions/' + questionId + '/'}) @@ -41,6 +93,24 @@ services.factory('Question', (Choice, $http, $log) -> $log.info("Succesfully fetched question") .error (data) => $log.info("Failed to fetch question.") + + voteOnChoice : (voteChoice, cb) -> + for choice in @choices + if choice.id == voteChoice + choice.votes+=1 + @totalVotes+=1 + choice.update(cb) + break + + addFeedback: (feedbackText) -> + data = {'feedback_text' : feedbackText, 'question' : @id } + $http({method: 'POST', url: '/polls/feedbacks', data:data}) + .success (data) => + $log.info("Succesfully added feedback") + @feedbacks.push(new Feedback(data)) + .error (data) => + $log.info("Failed to add feedback") + return Question ) @@ -53,6 +123,7 @@ services.factory('Questions', ($log, $http, Question) -> questions['all'].length = 0 for question in data questions['all'].push(new Question(question)) + questions['all'] = questions['all'].sort((a,b) -> b.upvotes - a.upvotes ) fetch: -> $http({method: 'GET', url: '/polls/questions'}) diff --git a/djangular/polls/admin.py b/djangular/polls/admin.py index a8ebcca..8672da7 100644 --- a/djangular/polls/admin.py +++ b/djangular/polls/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from polls.models import Choice, Question +from polls.models import Choice, Question, Feedback class ChoiceInline(admin.StackedInline): @@ -7,12 +7,17 @@ class ChoiceInline(admin.StackedInline): extra = 3 +class FeedbackInline(admin.StackedInline): + model = Feedback + extra = 1 + + class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question_text']}), ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), ] - inlines = [ChoiceInline] + inlines = [ChoiceInline, FeedbackInline] admin.site.register(Question, QuestionAdmin) \ No newline at end of file diff --git a/djangular/polls/models.py b/djangular/polls/models.py index f45c090..43d8ea0 100644 --- a/djangular/polls/models.py +++ b/djangular/polls/models.py @@ -1,8 +1,11 @@ from django.db import models +import datetime class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') + last_response_date = models.DateTimeField('last response date', null=True, blank=True) + upvotes = models.IntegerField(null=True, blank=True) def __unicode__(self): return self.question_text @@ -16,3 +19,25 @@ class Choice(models.Model): def __unicode__(self): return self.choice_text + def save(self, *args, **kwargs): + ''' On save, update timestamp on question ''' + if self.votes > 0: + self.question.last_response_date = datetime.datetime.today() + self.question.save() + return super(Choice, self).save(*args, **kwargs) + + +class Feedback(models.Model): + question = models.ForeignKey(Question, related_name='feedbacks') + feedback_text = models.CharField(max_length=500) + pub_date = models.DateTimeField('date published') + + def __unicode__(self): + return self.feedback_text + + def save(self, *args, **kwargs): + ''' On save, update timestamps ''' + if not self.id: + self.pub_date = datetime.datetime.today() + return super(Feedback, self).save(*args, **kwargs) + diff --git a/djangular/polls/serializers.py b/djangular/polls/serializers.py index 2a10139..9cb8da7 100644 --- a/djangular/polls/serializers.py +++ b/djangular/polls/serializers.py @@ -1,17 +1,29 @@ from rest_framework import serializers -from .models import Question, Choice +from .models import Question, Choice, Feedback class ChoiceSerializer(serializers.ModelSerializer): + question = serializers.PrimaryKeyRelatedField(required=False, read_only=True) class Meta: model = Choice - fields = ('choice_text', 'id', 'votes') + fields = ('choice_text', 'question', 'id', 'votes') + +class FeedbackSerializer(serializers.ModelSerializer): + pub_date = serializers.DateTimeField(required=False) + + class Meta: + model = Feedback + fields = ('feedback_text', 'question', 'id', 'pub_date') class QuestionSerializer(serializers.ModelSerializer): choices = ChoiceSerializer(many=True) + feedbacks = FeedbackSerializer(many=True) + last_response_date = serializers.DateTimeField(required=False) + upvotes = serializers.IntegerField(required=False) class Meta: model = Question - fields = ('question_text', 'choices', 'id') + # 'last_response_date' + fields = ('question_text', 'last_response_date', 'upvotes', 'choices', 'feedbacks', 'id') diff --git a/djangular/polls/urls.py b/djangular/polls/urls.py index 8797535..b5c6e7a 100644 --- a/djangular/polls/urls.py +++ b/djangular/polls/urls.py @@ -1,14 +1,21 @@ from django.conf.urls import patterns, url, include -from .views import QuestionList, ChoiceList, QuestionDetail, ChoiceUpdate +from .views import QuestionList, ChoiceList, QuestionDetail, ChoiceUpdate, FeedbackList, FeedbackUpdate, UpvoteApiView, DownvoteApiView urlpatterns = patterns('polls.views', url(r'^questions$', QuestionList.as_view(), name='questions_list'), url(r'^questions/(?P[0-9]+)/$', QuestionDetail.as_view(), name="questions_detail"), + url(r'^questions/(?P[0-9]+)/upvote$', UpvoteApiView.as_view(), + name="upvote"), + url(r'^questions/(?P[0-9]+)/downvote$', DownvoteApiView.as_view(), + name="downvote"), url(r'^choices$', ChoiceList.as_view(), name='choices_list'), url(r'^choices/(?P[0-9]+)/$', ChoiceUpdate.as_view(), name='choices_update'), + url(r'^feedbacks$', FeedbackList.as_view(), name='feedbacks_list'), + url(r'^feedbacks/(?P[0-9]+)/$', FeedbackUpdate.as_view(), + name='feedback_update'), url(r'^$', 'index', name='questions_index'), ) diff --git a/djangular/polls/views.py b/djangular/polls/views.py index 9680098..a74c025 100644 --- a/djangular/polls/views.py +++ b/djangular/polls/views.py @@ -1,18 +1,21 @@ -from rest_framework import generics, permissions -from .models import Question, Choice -from .serializers import QuestionSerializer, ChoiceSerializer +from rest_framework import generics, permissions, views +from rest_framework.response import Response +from .models import Question, Choice, Feedback +from .serializers import QuestionSerializer, ChoiceSerializer, FeedbackSerializer from django.shortcuts import render class QuestionList(generics.ListCreateAPIView): - model = Question + queryset = Question.objects.all() + #model = Question serializer_class = QuestionSerializer permission_classes = [ permissions.AllowAny ] class QuestionDetail(generics.RetrieveAPIView): - model = Question + queryset = Question.objects.all() + #model = Question serializer_class = QuestionSerializer lookup_url_kwarg = 'question_pk' permission_classes = [ @@ -20,7 +23,8 @@ class QuestionDetail(generics.RetrieveAPIView): ] class ChoiceUpdate(generics.UpdateAPIView): - model = Choice + queryset = Choice.objects.all() + #model = Choice serializer_class = ChoiceSerializer lookup_url_kwarg = 'choice_pk' permission_classes = [ @@ -28,11 +32,49 @@ class ChoiceUpdate(generics.UpdateAPIView): ] class ChoiceList(generics.ListCreateAPIView): - model = Choice + queryset = Choice.objects.all() + #model = Choice serializer_class = ChoiceSerializer permission_classes = [ permissions.AllowAny ] +class FeedbackUpdate(generics.UpdateAPIView): + queryset = Feedback.objects.all() + serializer_class = FeedbackSerializer + lookup_url_kwarg = 'feedback_pk' + permission_classes = [ + permissions.AllowAny + ] + +class FeedbackList(generics.ListCreateAPIView): + queryset = Feedback.objects.all() + serializer_class = FeedbackSerializer + permission_classes = [ + permissions.AllowAny + ] + +class UpvoteApiView(views.APIView): + + def post(self, request, *args, **kwargs): + question = Question.objects.get(id=kwargs['question_pk']) + if question.upvotes: + question.upvotes = question.upvotes+1 + else: + question.upvotes = 1 + question.save() + return Response({"question": question.id, "upvotes": question.upvotes, "success": True}) + +class DownvoteApiView(views.APIView): + + def post(self, request, *args, **kwargs): + question = Question.objects.get(id=kwargs['question_pk']) + if question.upvotes: + question.upvotes = question.upvotes-1 + else: + question.upvotes = -1 + question.save() + return Response({"question": question.id, "upvotes": question.upvotes, "success": True}) + def index(request): return render(request, 'polls/index.html') diff --git a/djangular/static/js/app.js b/djangular/static/js/app.js deleted file mode 100644 index a2bd0c0..0000000 --- a/djangular/static/js/app.js +++ /dev/null @@ -1,51 +0,0 @@ -(function() { - var app; - - app = angular.module('pollApp', ['ui.router', 'pollApp.controllers', 'pollApp.services', 'pollApp.directives']); - - app.config(function($interpolateProvider, $stateProvider, $urlRouterProvider) { - $interpolateProvider.startSymbol('[['); - $interpolateProvider.endSymbol(']]'); - $urlRouterProvider.otherwise('/'); - return $stateProvider.state('questionList', { - url: '/', - templateUrl: 'questionList', - controller: 'questionListController', - resolve: { - questions: function(Questions) { - Questions.fetch(); - return Questions.data(); - } - } - }).state('questionDetail', { - url: '/{questionId:[0-9]+}/', - templateUrl: 'questionDetail', - controller: 'questionDetailController', - resolve: { - question: function($stateParams, $log, Question) { - var question; - question = new Question(null); - question.get($stateParams.questionId); - return question; - } - } - }); - }); - - app.config(function($httpProvider) { - var getCookie; - getCookie = function(name) { - var cookie, _i, _len, _ref; - _ref = document.cookie.split(';'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - cookie = _ref[_i]; - if (cookie && name === (cookie.trim().split('='))[0]) { - return decodeURIComponent(cookie.trim().slice(1 + name.length)); - } - } - return null; - }; - return $httpProvider.defaults.headers.common['X-CSRFToken'] = getCookie("csrftoken"); - }); - -}).call(this); diff --git a/djangular/static/js/controllers.js b/djangular/static/js/controllers.js deleted file mode 100644 index dcb935c..0000000 --- a/djangular/static/js/controllers.js +++ /dev/null @@ -1,30 +0,0 @@ -(function() { - var controllers; - - controllers = angular.module('pollApp.controllers', []); - - controllers.controller('questionListController', function($scope, $state, $log, questions) { - return $scope.questions = questions.all; - }); - - controllers.controller('questionDetailController', function($scope, $state, $log, question) { - $scope.question = question; - $scope.voted = false; - $scope.voteChoice = 0; - return $scope.vote = function() { - var choice, _i, _len, _ref; - _ref = $scope.question.choices; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - choice = _ref[_i]; - if (choice.id === parseInt($scope.voteChoice)) { - choice.votes += 1; - $scope.question.totalVotes += 1; - choice.update(); - break; - } - } - return $scope.voted = true; - }; - }); - -}).call(this); diff --git a/djangular/static/js/directives.js b/djangular/static/js/directives.js deleted file mode 100644 index bee7b30..0000000 --- a/djangular/static/js/directives.js +++ /dev/null @@ -1,34 +0,0 @@ -(function() { - var directives; - - directives = angular.module('pollApp.directives', []); - - directives.directive('choicePercentage', function() { - return { - restrict: 'A', - scope: { - votes: '=', - total: '=' - }, - link: function(scope, element, attrs) { - var update; - update = function() { - var percentage; - if (scope.total > 0) { - percentage = scope.votes / scope.total * 100; - } else { - percentage = 0; - } - return element.css('width', percentage + '%'); - }; - scope.$watch('total', function(value) { - return update(); - }); - return scope.$watch('votes', function(value) { - return update(); - }); - } - }; - }); - -}).call(this); diff --git a/djangular/static/js/services.js b/djangular/static/js/services.js deleted file mode 100644 index 28f3280..0000000 --- a/djangular/static/js/services.js +++ /dev/null @@ -1,118 +0,0 @@ -(function() { - var services; - - services = angular.module('pollApp.services', []); - - services.factory('Choice', function($http, $log) { - var Choice; - Choice = (function() { - function Choice(data) { - this.choice_text = data.choice_text; - this.id = data.id; - this.votes = data.votes; - } - - Choice.prototype.update = function() { - var data, - _this = this; - data = { - 'votes': this.votes, - 'choice_text': this.choice_text - }; - return $http({ - method: 'PUT', - url: '/polls/choices/' + this.id + '/', - data: data - }).success(function(data) { - return $log.info("Succesfully voted"); - }).error(function(data) { - return $log.info("Failed to vote."); - }); - }; - - return Choice; - - })(); - return Choice; - }); - - services.factory('Question', function(Choice, $http, $log) { - var Question; - Question = (function() { - function Question(data) { - if (data !== null) { - this.init(data); - } - } - - Question.prototype.init = function(data) { - var c, choice, _i, _len, _ref, _results; - this.question_text = data.question_text; - this.id = data.id; - this.choices = []; - this.totalVotes = 0; - _ref = data.choices; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - choice = _ref[_i]; - c = new Choice(choice); - this.totalVotes += c.votes; - _results.push(this.choices.push(new Choice(choice))); - } - return _results; - }; - - Question.prototype.get = function(questionId) { - var _this = this; - return $http({ - method: 'GET', - url: '/polls/questions/' + questionId + '/' - }).success(function(data) { - _this.init(data); - return $log.info("Succesfully fetched question"); - }).error(function(data) { - return $log.info("Failed to fetch question."); - }); - }; - - return Question; - - })(); - return Question; - }); - - services.factory('Questions', function($log, $http, Question) { - var questions; - questions = { - all: [] - }; - return { - fromServer: function(data) { - var question, _i, _len, _results; - questions['all'].length = 0; - _results = []; - for (_i = 0, _len = data.length; _i < _len; _i++) { - question = data[_i]; - _results.push(questions['all'].push(new Question(question))); - } - return _results; - }, - fetch: function() { - var _this = this; - return $http({ - method: 'GET', - url: '/polls/questions' - }).success(function(data) { - _this.fromServer(data); - return $log.info("Succesfully fetched questions."); - }).error(function(data) { - return $log.info("Failed to fetch questions."); - }); - }, - data: function() { - return questions; - } - }; - }); - -}).call(this); diff --git a/djangular/templates/polls/index.html b/djangular/templates/polls/index.html index 144f33b..b940d50 100644 --- a/djangular/templates/polls/index.html +++ b/djangular/templates/polls/index.html @@ -10,19 +10,40 @@
+
+
+
+
+
+ +
+ [[question.upvotes ? question.upvotes : 0]] +
+ +
+
+ +

[[question.question_text]]

+
+
+ + [[question.lastResponseDate ? "Last responded to " + question.lastResponseDateFmt + " -- " : "" ]] + [[question.totalVotes]] [[ question.totalVotes != 1 ? "responses" : "response" ]] -- View Responses + +
+ +
+
+ + + + + {% endblock %} {% block javascript %} @@ -55,4 +107,5 @@

[[question.question_text]]

+ {% endblock %}