diff --git a/functional_tests/test_all_matches.py b/functional_tests/test_all_matches.py new file mode 100644 index 0000000..237a3c8 --- /dev/null +++ b/functional_tests/test_all_matches.py @@ -0,0 +1,102 @@ +import time + +from django.test import LiveServerTestCase +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException + +from leaderboard.models import Player, Match + + +class AllMatchesTest(LiveServerTestCase): + + def setUp(self): + """Start all tests by setting up an authenticated browser.""" + self.browser = webdriver.Firefox() + + def tearDown(self): + """Stop all tests by shutting down the browser.""" + self.browser.quit() + + def test_all_matches(self): + """ + Test all matches page view. + + The page should load the 50 most recent matches, and show links + at the bottom to switch pages. + """ + # Load database with Bob and Sue Hope + bob = Player.objects.create(first_name='Bob', last_name='Hope') + sue = Player.objects.create(first_name='Sue', last_name='Hope') + + # Input 30 games + for _ in range(30): + Match.objects.create( + winner=bob, + loser=sue, + winning_score=21, + losing_score=19 + ) + + # Bob loads the home page, and notices a link for all matches. + self.browser.get(self.live_server_url) + all_matches_link = self.browser.find_element_by_id('all-matches-link') + all_matches_link.click() + + # It takes him to a page where he sees all matches. + matches_url = self.live_server_url + '/matches/' + self.assertEqual(matches_url, self.browser.current_url) + matches = self.browser.find_elements_by_id('match') + self.assertEqual(len(matches), 30) + + # He notices a paginator showing him what page he's on. + current_page = self.browser.find_element_by_id('current-page') + self.assertEqual(current_page.text, 'Page 1 of 1') + + # Because there's only 1 page, there are no links to next or previous page + with self.assertRaises(NoSuchElementException): + self.browser.find_element_by_id('previous-page-link') + with self.assertRaises(NoSuchElementException): + self.browser.find_element_by_id('next-page-link') + + # Input 80 more games and refresh page + for _ in range(80): + Match.objects.create( + winner=bob, + loser=sue, + winning_score=21, + losing_score=19 + ) + self.browser.refresh() + + # He notices a paginator showing him he's on page 1 of 3 with a link to the next page. + matches = self.browser.find_elements_by_id('match') + self.assertEqual(len(matches), 50) + current_page = self.browser.find_element_by_id('current-page') + self.assertEqual(current_page.text, 'Page 1 of 3') + with self.assertRaises(NoSuchElementException): + self.browser.find_element_by_id('previous-page-link') + next_page_link = self.browser.find_element_by_id('next-page-link') + self.assertEqual(next_page_link.text, 'Next') + + # He goes to the next page, and sees a previous page link. + next_page_link.click() + matches = self.browser.find_elements_by_id('match') + self.assertEqual(len(matches), 50) + current_page = self.browser.find_element_by_id('current-page') + self.assertEqual(current_page.text, 'Page 2 of 3') + previous_page_link = self.browser.find_element_by_id('previous-page-link') + self.assertEqual(previous_page_link.text, 'Previous') + + # He goes to the final page, and sees no next page link. + self.browser.find_element_by_id('next-page-link').click() + matches = self.browser.find_elements_by_id('match') + self.assertEqual(len(matches), 10) + current_page = self.browser.find_element_by_id('current-page') + self.assertEqual(current_page.text, 'Page 3 of 3') + with self.assertRaises(NoSuchElementException): + self.browser.find_element_by_id('next-page-link') + + # He goes back to the previous page. + self.browser.find_element_by_id('previous-page-link').click() + current_page = self.browser.find_element_by_id('current-page') + self.assertEqual(current_page.text, 'Page 2 of 3') diff --git a/leaderboard/models.py b/leaderboard/models.py index 8cb6c68..f93e84d 100644 --- a/leaderboard/models.py +++ b/leaderboard/models.py @@ -48,12 +48,17 @@ def score(self): """Hyphenated version of match score, i.e. 21-19""" score = f'{self.winning_score}-{self.losing_score}' return score + + @property + def date(self): + """Date part of match datetime.""" + date = self.datetime.strftime('%m/%d/%Y') + return date @property def description(self): - match_date = self.datetime.strftime('%m/%d/%Y') description = ( - f'{match_date}: {self.winner} defeated {self.loser} {self.score}' + f'{self.date}: {self.winner} defeated {self.loser} {self.score}' ) return description diff --git a/leaderboard/templates/all_matches.html b/leaderboard/templates/all_matches.html new file mode 100644 index 0000000..c99ef4b --- /dev/null +++ b/leaderboard/templates/all_matches.html @@ -0,0 +1,45 @@ + + + + + + + PongBoard - Matches + + + +

All Matches

+ + + + + + + + + {% for match in matches %} + + + + + + + {% endfor %} +
DateWinnerLoserScore
{{ match.date }}{{ match.winner }}{{ match.loser }}{{ match.score }}
+ + + {% if matches.has_previous %} + Previous + {% endif %} + + + Page {{ matches.number }} of {{ matches.paginator.num_pages }} + + + {% if matches.has_next %} + Next + {% endif %} + + + + \ No newline at end of file diff --git a/leaderboard/templates/home.html b/leaderboard/templates/home.html index 8dbc343..2acab66 100644 --- a/leaderboard/templates/home.html +++ b/leaderboard/templates/home.html @@ -82,6 +82,7 @@

PongBoard

{% for match in recent_matches %}
  • {{ match.description }}
  • {% endfor %} - + + See all matches \ No newline at end of file diff --git a/leaderboard/tests/test_models.py b/leaderboard/tests/test_models.py index d4a53d3..fe00420 100644 --- a/leaderboard/tests/test_models.py +++ b/leaderboard/tests/test_models.py @@ -94,6 +94,16 @@ def test_score(self): expected_score = f'{self.winning_score}-{self.losing_score}' self.assertEqual(self.match.score, expected_score) + def test_date(self): + """ + Test match model has a date property. + + Date is a truncated version of the datetime with only + the date part. + """ + expected_date = self.match.datetime.strftime('%m/%d/%Y') + self.assertEqual(self.match.date, expected_date) + def test_description(self): """ Test match model has a description property. diff --git a/leaderboard/tests/test_views.py b/leaderboard/tests/test_views.py index b73d6de..9b8cdd0 100644 --- a/leaderboard/tests/test_views.py +++ b/leaderboard/tests/test_views.py @@ -1,6 +1,7 @@ from django.test import TestCase from django.utils.html import escape from django.contrib.auth.models import User +from django.core.paginator import Paginator from leaderboard.models import Player, Match from leaderboard.forms import MatchForm, PlayerForm, DUPLICATE_ERROR @@ -140,3 +141,67 @@ def test_invalid_player_returns_form(self): form = response.context['player_form'] expected_form = PlayerForm(self.invalid_player_data) self.assertEqual(form.as_p(), expected_form.as_p()) + + +class AllMatchesTest(TestCase): + + @classmethod + def setUpClass(cls): + """Add matches to database for each test.""" + super().setUpClass() # Needed to run only once for all tests + cls.player1 = Player.objects.create(first_name='Bob', last_name='Hope') + cls.player2 = Player.objects.create(first_name='Sue', last_name='Hope') + for _ in range(51): + Match.objects.create( + winner=cls.player1, + loser=cls.player2, + winning_score=21, + losing_score=10 + ) + + def test_correct_template(self): + """Test that the match HTML template is used.""" + response = self.client.get('/matches/') + self.assertTemplateUsed(response, 'all_matches.html') + + def test_paginator_with_all_matches(self): + """Test view has paginator with all matches.""" + response = self.client.get('/matches/') + matches = response.context['matches'] + self.assertEqual(matches.paginator.count, 51) + + def test_num_pages(self): + """Test paginator has correct number of pages.""" + response = self.client.get('/matches/') + matches = response.context['matches'] + self.assertEqual(matches.paginator.num_pages, 2) + + def test_returns_num_matches(self): + """Test that 50 matches are returned.""" + response = self.client.get('/matches/') + matches = response.context['matches'] + self.assertEqual(len(matches), 50) + + def test_default_first_page(self): + """Test that the default page is page 1.""" + response = self.client.get('/matches/') + matches = response.context['matches'] + self.assertFalse(matches.has_previous()) + + def test_get_page(self): + """Test page requested through GET.""" + response = self.client.get('/matches/?page=2') + matches = response.context['matches'] + self.assertEqual(matches.number, 2) + + def test_empty_page(self): + """Test empty page defaults to last page.""" + response = self.client.get('/matches/?page=1000') + matches = response.context['matches'] + self.assertFalse(matches.has_next()) + + def test_num_matches_last_page(self): + """Test that 1 match returned in last page.""" + response = self.client.get('/matches/?page=2') + matches = response.context['matches'] + self.assertEqual(len(matches), 1) diff --git a/leaderboard/views.py b/leaderboard/views.py index 3c7da7d..a1d5394 100644 --- a/leaderboard/views.py +++ b/leaderboard/views.py @@ -1,4 +1,5 @@ from django.shortcuts import render, redirect +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from leaderboard.models import Match, PlayerRating from leaderboard.forms import MatchForm, PlayerForm @@ -34,3 +35,23 @@ def home_page(request): 'unranked_players': unranked_players } ) + + +def all_matches(request): + """Render page to view all matches.""" + all_matches = Match.objects.all().order_by('-datetime') + paginator = Paginator(all_matches, per_page=50) + page = request.GET.get('page') + try: + matches = paginator.page(page) + except PageNotAnInteger: # occurs when no page is passed through + matches = paginator.page(1) + except EmptyPage: # occurs when page is out of range + matches = paginator.page(paginator.num_pages) # deliver last page of results + return render( + request, + 'all_matches.html', + context={ + 'matches': matches + } + ) diff --git a/pongboard/urls.py b/pongboard/urls.py index 9588461..9a8376d 100644 --- a/pongboard/urls.py +++ b/pongboard/urls.py @@ -13,12 +13,15 @@ 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ + from django.conf.urls import url, include from django.contrib import admin -from leaderboard.views import home_page + +from leaderboard.views import home_page, all_matches urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^$', view=home_page, name='home'), url(r'accounts/', include('django.contrib.auth.urls')), + url(r'matches/', view=all_matches, name='all_matches'), ]