diff --git a/poetry.lock b/poetry.lock
index e9a3024..e2c4d5e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -181,6 +181,18 @@ files = [
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
]
+[[package]]
+name = "tblib"
+version = "1.7.0"
+description = "Traceback serialization library."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "tblib-1.7.0-py2.py3-none-any.whl", hash = "sha256:289fa7359e580950e7d9743eab36b0691f0310fce64dee7d9c31065b8f723e23"},
+ {file = "tblib-1.7.0.tar.gz", hash = "sha256:059bd77306ea7b419d4f76016aef6d7027cc8a0785579b5aad198803435f882c"},
+]
+
[[package]]
name = "tzdata"
version = "2022.7"
@@ -223,4 +235,4 @@ brotli = ["Brotli"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "c0a2d81dcf191d69565b3de08983d96c7f5ad00a6de9eba5b350b1431a5e99b3"
+content-hash = "ca22a0efd5d4acfb0a4a09e8567d0a8c90debca1203b7a80262919d8055366da"
diff --git a/pyproject.toml b/pyproject.toml
index 90f6f4a..3e5586c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,6 +16,9 @@ gunicorn = "^20.1.0"
whitenoise = "^6.3.0"
+[tool.poetry.group.dev.dependencies]
+tblib = "^1.7.0"
+
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
diff --git a/templates/404.html b/templates/404.html
index eccc3b3..b11bc6f 100644
--- a/templates/404.html
+++ b/templates/404.html
@@ -1,4 +1,6 @@
{% extends "base.html" %}
{% block content %}
-
Nothing here...
+
+
Nothing here...
+
{% endblock %}
\ No newline at end of file
diff --git a/templates/watchlist/index.html b/templates/watchlist/index.html
index a5201d9..0dbedc7 100644
--- a/templates/watchlist/index.html
+++ b/templates/watchlist/index.html
@@ -1,19 +1,23 @@
{% extends 'base.html' %}
{% block content %}
-
Watchlist
+
Watchlist
+ {% if object_list %}
{% for movie in object_list %}
- - {{movie.name}}{% if request.user.is_authenticated and movie not in voted_movies %}*{% endif %} — {{movie.score}}
+ - {% if movie.watched %}{% endif %}{{movie.name}}{% if movie.watched %}{% endif %}{% if request.user.is_authenticated and movie not in voted_movies %}*{% endif %} — {{movie.score}}
{% endfor %}
+ {% else %}
+
No movies yet.
+ {% endif %}
{% if can_add_movie %}
Submit new movie
{% endif %}
diff --git a/watchlist/tests/test_views.py b/watchlist/tests/test_views.py
new file mode 100644
index 0000000..291e08f
--- /dev/null
+++ b/watchlist/tests/test_views.py
@@ -0,0 +1,175 @@
+from django.test import TestCase
+from django.urls import reverse
+from django.contrib.auth.models import User, Permission
+from django.contrib.auth.hashers import make_password
+
+from watchlist.models import Movie, MovieVote
+
+def create_user(name="user", is_staff=False):
+ user = User.objects.create(username=name, password=make_password("dummy"))
+ user.user_permissions.add(Permission.objects.get(codename="add_movie"))
+ user.save()
+ return user
+
+def create_movie(name="Test movie", added_by="user"):
+ return Movie.objects.create(name=name, suggested_by=User.objects.filter(username=added_by).first(), watched=False)
+
+class IndexViewTests(TestCase):
+
+ def setUp(self):
+ self.user = create_user("user")
+
+ def test_no_movies(self):
+ """Tests the index page with no movies"""
+ response = self.client.get(reverse('watchlist:index'))
+ self.assertQuerysetEqual(response.context["object_list"], [])
+ self.assertContains(response, "No movies yet.")
+ self.assertNotContains(response, "Submit new movie")
+
+ def test_no_movies_logged_in(self):
+ """Tests the index page with no movies, while the user is logged in."""
+ self.client.login(username="user", password="dummy")
+ response = self.client.get(reverse('watchlist:index'))
+ self.assertQuerysetEqual(response.context["object_list"], [])
+ self.assertContains(response, "No movies yet.")
+ self.assertContains(response, "Submit new movie")
+
+ def test_with_movie(self):
+ """Tests that the index page shows a movie, hides it if it watched, but shows
+ it with proper filter"""
+ create_movie()
+ with self.subTest(watched=False):
+ response = self.client.get(reverse('watchlist:index'))
+ self.assertQuerysetEqual(response.context["object_list"], Movie.objects.all())
+ self.assertContains(response, "Test movie")
+ movie = Movie.objects.first()
+ movie.watched = True
+ movie.save()
+ with self.subTest(watched=True, filter=False):
+ response = self.client.get(reverse('watchlist:index'))
+ self.assertQuerysetEqual(response.context["object_list"], [])
+ self.assertNotContains(response, "Test movie")
+ with self.subTest(watched=True, filter=True):
+ response = self.client.get(reverse('watchlist:index',) + "?watched")
+ self.assertQuerysetEqual(response.context["object_list"], Movie.objects.all())
+ self.assertContains(response, "Test movie")
+
+ def test_unvoted(self):
+ """Test that the index shows an asterisk with movies that the user hasn't voted on yet"""
+ create_movie()
+ self.client.login(username="user",password="dummy")
+ response = self.client.get(reverse('watchlist:index'))
+ self.assertContains(response, "Test movie*")
+
+ def test_voted(self):
+ """Test that the index doesn't show an asterisk with movies the has voted on."""
+ movie = create_movie()
+ MovieVote.objects.create(movie=movie, user=self.user, vote=1)
+ self.client.login(username="user",password="dummy")
+ response = self.client.get(reverse('watchlist:index'))
+ self.assertContains(response, "Test movie")
+ self.assertNotContains(response, "Test movie*")
+
+
+ def test_movie_sorting(self):
+ """Test various methods of movie sorting"""
+ m1 = create_movie(name="ZZZ: A movie test")
+ m2 = create_movie(name="Test movie 2")
+ tests = [
+ ('?sort=id', [m1,m2]),
+ ('?sort=-id', [m2,m1]),
+ ('?sort=name', [m2,m1]),
+ ('?sort=-name', [m1,m2]),
+ ]
+ for param, qs in tests:
+ with self.subTest(param=param, qs=qs):
+ response = self.client.get(reverse('watchlist:index') + param)
+ self.assertQuerysetEqual(response.context["object_list"], qs)
+ self.assertContains(response, m1.name)
+ self.assertContains(response, m2.name)
+
+class MovieDetailViewTests(TestCase):
+
+ def setUp(self):
+ self.user = create_user("user")
+
+ def test_detail_logged_out(self):
+ m = create_movie()
+ response = self.client.get(reverse('watchlist:detail', args=(m.id,)))
+ self.assertContains(response, m.name)
+ self.assertEqual(response.context["movie"], m)
+ self.assertNotContains(response, "Edit")
+
+ def test_detail_logged_in(self):
+ m = create_movie()
+ self.client.login(username="user", password="dummy")
+ response = self.client.get(reverse('watchlist:detail', args=(m.id,)))
+ self.assertContains(response, m.name)
+ self.assertEqual(response.context["movie"], m)
+ self.assertContains(response, "Edit")
+
+ def test_no_movie(self):
+ response = self.client.get(reverse('watchlist:detail', args=(1,)))
+ self.assertEqual(response.status_code, 404)
+
+class VoteTests(TestCase):
+
+ def setUp(self):
+ self.user = create_user("user")
+
+ def test_no_user_voting(self):
+ m = create_movie()
+ response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": 1, "seen": True})
+ self.assertRedirects(response, "/accounts/login/?next=" + response.request["PATH_INFO"], fetch_redirect_response=False)
+
+ def test_user_voting(self):
+ m = create_movie()
+ score = m.score
+ self.client.login(username="user", password="dummy")
+ for vote in [1, 0, -1]:
+ for seen in True, False:
+ with self.subTest(vote=vote, seen=seen):
+ response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": str(vote), "seen": True})
+ self.assertRedirects(response, reverse('watchlist:index'))
+ self.assertEqual(m.score, score + vote)
+
+ def test_invalid_votes(self):
+ m = create_movie()
+ score = m.score
+ self.client.login(username="user", password="dummy")
+ for vote in [2,3,4,-5,0.5,'abc','']:
+ with self.subTest(vote=vote):
+ response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": str(vote), "seen": True})
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(m.score, score)
+ with self.subTest(vote="empty"):
+ response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"seen": True})
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(m.score, score)
+
+ def test_seen(self):
+ m = create_movie()
+ self.client.login(username="user", password="dummy")
+ for seen in (True, False) * 2:
+ with self.subTest(seen=seen):
+ data = {"vote": "0", "seen": "on"} if seen else {"vote": "0"}
+ response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data=data)
+ self.assertRedirects(response, reverse('watchlist:index'))
+ mv = m.movievote_set.get(user=self.user)
+ self.assertEqual(mv.seen, seen)
+
+
+ def test_comment(self):
+ m = create_movie()
+ self.client.login(username="user", password="dummy")
+ for comment in ["aaa", "", "TEST"]:
+ with self.subTest(comment=comment):
+ response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": "0", "comment": comment})
+ mv = m.movievote_set.get(user=self.user)
+ self.assertEqual(mv.comment, None if comment == "" else comment)
+ with self.subTest(comment=None):
+ response = self.client.post(reverse('watchlist:vote', args=(m.id,)), data={"vote": "0"})
+ mv = m.movievote_set.get(user=self.user)
+ self.assertEqual(mv.comment, None)
+
+
diff --git a/watchlist/views.py b/watchlist/views.py
index 6c138e9..99ba2a0 100644
--- a/watchlist/views.py
+++ b/watchlist/views.py
@@ -51,6 +51,8 @@ def vote(request, pk):
user_vote = movie.movievote_set.filter(user=request.user).first()
if user_vote is None:
user_vote = models.MovieVote(movie=movie, user=request.user)
+ if not request.POST.get("vote", None) in ("1", "0", "-1"):
+ return HttpResponseBadRequest("Invalid vote.")
user_vote.vote = request.POST['vote']
user_vote.seen = request.POST.get('seen', False) == "on"
comment = request.POST.get('comment', '').strip()