diff --git a/core/colorize.py b/core/colorize.py index 10596fa..ef573f4 100644 --- a/core/colorize.py +++ b/core/colorize.py @@ -1,3 +1,5 @@ +import itertools + def diffract(hex_encoding): return [int(band, 16) for band in (hex_encoding[i:i+2] for i in (0, 2, 4))] @@ -31,11 +33,19 @@ def populate_stops(color_stops): ) return full_stops -def style_block(value, color): - return "\n".join(["[data-value=\"{}\"] {{".format(value), - " color: #{};".format(color), + +def style_block(data_attribute, style_property, state, color): + return "\n".join(["[data-{}=\"{}\"] {{".format(data_attribute, state), + " {}: #{};".format(style_property, color), "}\n"]) +def value_style_block(value, color): + return style_block("value", "color", value, color) + +def mark_style_block(mark, color): + return style_block("mark", "background-color", mark, color) + + def stylesheet(low_score, low_color, high_score, high_color): stops = {0: "000000"} if low_score < 0: @@ -43,5 +53,11 @@ def stylesheet(low_score, low_color, high_score, high_color): if high_score > 0: stops.update({high_score: high_color}) colors = populate_stops(stops) - return "\n".join(style_block(value, color) - for value, color in colors.items()) + return "\n".join( + itertools.chain( + (value_style_block(value, color) + for value, color in colors.items()), + (mark_style_block(mark, color) + for mark, color in ((-1, "FFD6D6"), (1, "D6D6FF"))) + ) + ) diff --git a/core/tests/view_tests.py b/core/tests/view_tests.py index 7111e9e..fe8c99f 100644 --- a/core/tests/view_tests.py +++ b/core/tests/view_tests.py @@ -162,7 +162,20 @@ def setUpTestData(cls): cls.the_user = f.FinetoothUserFactory.create() cls.the_post = f.PostFactory.create(content="hello Django world") - def test_canot_vote_if_not_logged_in(self): + def test_can_vote(self): + self.client.login( + username=self.the_user.username, password=f.FACTORY_USER_PASSWORD + ) + for start_index, end_index in ((0, 1), (1, 3), (5, 8)): + response = self.client.post( + reverse('vote', args=("post", self.the_post.pk)), + {'startIndex': start_index, 'endIndex': end_index, + 'value': 1} + ) + self.assertEqual(response.status_code, 204) + + + def test_cannot_vote_if_not_logged_in(self): response = self.client.post( reverse('vote', args=("post", self.the_post.pk)), {'startIndex': 1, 'endIndex': 5, 'value': 1} @@ -181,6 +194,21 @@ def test_cannot_submit_invalid_vote(self): ) self.assertEqual(response.status_code, 400) + def test_one_user_one_vote(self): + self.client.login( + username=self.the_user.username, password=f.FACTORY_USER_PASSWORD + ) + response = self.client.post( + reverse('vote', args=("post", self.the_post.pk)), + {'startIndex': 0, 'endIndex': 5, 'value': 1} + ) + self.assertEqual(response.status_code, 204) + response = self.client.post( + reverse('vote', args=("post", self.the_post.pk)), + {'startIndex': 2, 'endIndex': 4, 'value': 1} + ) + self.assertContains(response, "Overlapping votes are not allowed!", + status_code=403) class ProfileTestCase(TestCase): diff --git a/core/tests/votable_tests.py b/core/tests/votable_tests.py index 4203d20..05fcf80 100644 --- a/core/tests/votable_tests.py +++ b/core/tests/votable_tests.py @@ -5,7 +5,8 @@ from core.models import Post from core.votable import Tagnostic -from core.tests.factories import PostFactory, PostVoteFactory +from core.tests.factories import ( + FinetoothUserFactory, PostFactory, PostVoteFactory) class TagnosticismTestCase(TestCase): @@ -42,8 +43,8 @@ def setUpTestData(self): def test_scored_plaintext(self): self.assertEqual( self.the_post.scored_plaintext(), - (('f', 1), ('r', 2), ('i', 3), ('e', 4), ('n', 5), ('d', 6), - ('s', 6), ('h', 6), ('i', 6), ('p', 6)) + (('f', 1, 0), ('r', 2, 0), ('i', 3, 0), ('e', 4, 0), ('n', 5, 0), + ('d', 6, 0), ('s', 6, 0), ('h', 6, 0), ('i', 6, 0), ('p', 6, 0)) ) class RenderingTestCase(TestCase): @@ -65,17 +66,19 @@ def test_rendering(self): self.assertHTMLEqual( self.the_post.render(), """

- We\'ll + We\'ll - al - ways - find - a way + al + ways + find + a way - ; that\'s why the people of + + ; that\'s why the people of + - this + this - world believe + world believe

""" ) diff --git a/core/views/service.py b/core/views/service.py index ae8c711..c6d191f 100644 --- a/core/views/service.py +++ b/core/views/service.py @@ -48,11 +48,14 @@ def ballot_box(request, kind, pk): item = kinds[kind].objects.get(pk=pk) if start_index < 0 or end_index > len(item.plaintext): return HttpResponseBadRequest("Invalid vote not recorded!") - item.vote_set.create( - voter=request.user, value=value, - start_index=start_index, end_index=end_index - ) - return HttpResponse(status=204) + if item.vote_in_range_for_user(request.user, start_index, end_index): + return HttpResponseForbidden("Overlapping votes are not allowed!") + else: + item.vote_set.create( + voter=request.user, value=value, + start_index=start_index, end_index=end_index + ) + return HttpResponse(status=204) def check_slug(request): slug = request.GET.get('slug') diff --git a/core/views/views.py b/core/views/views.py index 4d72d39..004edce 100644 --- a/core/views/views.py +++ b/core/views/views.py @@ -30,6 +30,8 @@ def home(request, page_number): .prefetch_related('vote_set') \ .prefetch_related('comment_set') \ .select_related('author') + for post in all_posts: + post.request_user = request.user context = paginated_context(request, 'posts', all_posts, page_number, {}) context = scored_context(context['posts'], context) return render(request, "home.html", context) @@ -66,6 +68,7 @@ def show_post(request, year, month, slug): post = Post.objects.get( slug=slug, published_at__year=int(year), published_at__month=int(month) ) + post.request_user = request.user top_level_comments = post.comment_set.filter(parent=None) return render( request, "post.html", @@ -85,6 +88,8 @@ def get_queryset(self): end_year = year if month < 12 else (year + 1) next_month = (month % 12) + 1 end_of_month = datetime(year=end_year, month=next_month, day=1) + # XXX: is it possible to use my post.request_user hack with + # class-based views?? return Post.objects.filter( published_at__gte=start_of_month, published_at__lt=end_of_month ) @@ -128,6 +133,8 @@ def new_post(request): def tagged(request, label, page_number): tag = Tag.objects.get(label=label) tagged_posts = tag.posts.all() + for post in tagged_posts: + post.request_user = request.user context = paginated_context( request, 'posts', tagged_posts, page_number, {'tag': tag} ) diff --git a/core/votable.py b/core/votable.py index 4835303..ecba624 100644 --- a/core/votable.py +++ b/core/votable.py @@ -4,6 +4,8 @@ from markdown import markdown as markdown_to_html +from django.db.models import Q + logger = logging.getLogger(__name__) class Tagnostic(HTMLParser): @@ -41,40 +43,50 @@ def score(self): def plaintext(self): return Tagnostic(self.content).plaintext() - def scored_plaintext(self): + def scored_plaintext(self, for_voter=None): plaintext = Tagnostic(self.content).plaintext() score_increments = [0] * (len(plaintext) + 1) + mark_increments = [0] * (len(plaintext) + 1) for vote in self.vote_set.all(): score_increments[vote.start_index] += vote.value score_increments[vote.end_index] -= vote.value - return tuple(zip(plaintext, itertools.accumulate(score_increments))) + if for_voter and vote.voter == for_voter: + mark_increments[vote.start_index] += vote.value + mark_increments[vote.end_index] -= vote.value + return tuple(zip(plaintext, + itertools.accumulate(score_increments), + itertools.accumulate(mark_increments))) @staticmethod def _render_scored_substring(scored_characters): join_to_render_partial = [] value_at_index = None + mark_at_index = None open_span = False - for character, value in scored_characters: - if value == value_at_index: + for character, value, mark in scored_characters: + if value == value_at_index and mark == mark_at_index: join_to_render_partial.append(character) else: if open_span: join_to_render_partial.append('') open_span = False join_to_render_partial.append( - ''.format(value) + ''.format(value, mark) ) open_span = True value_at_index = value + mark_at_index = mark join_to_render_partial.append(character) if open_span: join_to_render_partial.append('') return ''.join(join_to_render_partial) def render(self): + for_voter = getattr(self, 'request_user', None) parsed_content = Tagnostic(self.content).content # XXX inefficiency - scored_plaintext_stack = list(reversed(self.scored_plaintext())) + scored_plaintext_stack = list( + reversed(self.scored_plaintext(for_voter))) join_to_render = [] for token in parsed_content: if isinstance(token, str): # text @@ -100,7 +112,14 @@ def render(self): return "".join(join_to_render) def low_score(self): - return min(v for c, v in self.scored_plaintext()) + return min(v for c, v, _m in self.scored_plaintext()) def high_score(self): - return max(v for c, v in self.scored_plaintext()) + return max(v for c, v, _m in self.scored_plaintext()) + + def vote_in_range_for_user(self, voter, + ballot_start_index, ballot_end_index): + return self.vote_set.filter( + end_index__gt=ballot_start_index, start_index__lt=ballot_end_index, + voter=voter + ).first() diff --git a/static/finetooth.js b/static/finetooth.js index 9882ce7..891b803 100644 --- a/static/finetooth.js +++ b/static/finetooth.js @@ -69,9 +69,12 @@ function instarender(range, value) { $node = $(node); if (node.nodeType === Node.TEXT_NODE) { var oldValue = $node.parents('[data-value]').data('value'); - $node.wrap($('').attr('data-value', oldValue + value)); + $node.wrap($('') + .attr('data-value', oldValue + value) + .attr('data-mark', value)); } else { - $node.attr('data-value', $node.data('value') + value); + $node.attr('data-value', $node.data('value') + value) + .attr('data-mark', value); } }); window.getSelection().collapse($('body')[0],0);