diff --git a/.gitignore b/.gitignore index b9d9cb3c54d93f3eca735a006887b75ae04e1bb9..88a1443c1a7118729a36d2ac0f38e036da5540ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ #SOURCE: https://www.codingforentrepreneurs.com/blog/django-virtualenv-python-gitignore-file/ # Project related -/media +/unisportomat/media/* # Virtualenv related bin/ diff --git a/README.md b/README.md index 256d9dab28c4f86dfc8b0b7d08ca2db7b53ee165..ae711e5a0aad7cdffd034bd74ceea0d5a6476db7 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,19 @@ python manage.py runserver ``` If successful, you can now see the running server in your browser at `http://127.0.0.1:8000`. + +## Populate the database with test data +To populate the database with some test data run +``` +python manage.py seed_db [-y] [--seed SEED] [--no-superuser] +``` +All the existing data from your database will be lost! +Per default a super user called "admin" will be created for development. +You will be prompted for a password. +Run `python manage.py seed_db --help` for more information. + +## Use the django admin interface to view and edit data during development +If you started the server as described above, you can access the django admin interface on +[localhost:8000/admin](localhost:8000/admin). +If you seeded the database you can login with username: "admin" and the password you specified. + diff --git a/unisportomat/quiz/admin.py b/unisportomat/quiz/admin.py index c3e5d6568c072bcb6891aedb274a5e0ec7f3333d..c1adb96b061ad56beceec3c8f21d084366660d07 100644 --- a/unisportomat/quiz/admin.py +++ b/unisportomat/quiz/admin.py @@ -3,8 +3,18 @@ from django.contrib import admin # Register your models here. -from .models import Sport, Criterion, CriterionRating +from .models import ( + Sport, + Question, + Criterion, + CriterionRating, + CallToMove, + KnowledgeSnack, +) admin.site.register(Sport) admin.site.register(Criterion) +admin.site.register(Question) admin.site.register(CriterionRating) +admin.site.register(CallToMove) +admin.site.register(KnowledgeSnack) diff --git a/unisportomat/quiz/fixtures/calls_to_move.json b/unisportomat/quiz/fixtures/calls_to_move.json new file mode 100644 index 0000000000000000000000000000000000000000..acb39dd05c2ed062d9bd6eac922e35c5f1de8291 --- /dev/null +++ b/unisportomat/quiz/fixtures/calls_to_move.json @@ -0,0 +1,10 @@ +[ + { + "model": "quiz.calltomove", + "pk": 1, + "fields": { + "text": "Kreise deine Arme vor der nächsten Frage 3x nach hinten", + "image": "test_image.png" + } + } +] \ No newline at end of file diff --git a/unisportomat/quiz/fixtures/criteria.json b/unisportomat/quiz/fixtures/criteria.json new file mode 100644 index 0000000000000000000000000000000000000000..584ded6433489d92a803179cea13f8313434e63b --- /dev/null +++ b/unisportomat/quiz/fixtures/criteria.json @@ -0,0 +1,9 @@ +[ + { + "model": "quiz.criterion", + "pk": 1, + "fields": { + "name": "Outdoorsport" + } + } +] \ No newline at end of file diff --git a/unisportomat/quiz/fixtures/criterion_ratings.json b/unisportomat/quiz/fixtures/criterion_ratings.json new file mode 100644 index 0000000000000000000000000000000000000000..3e27200ccc25d663003d4d5f20af160006f4101b --- /dev/null +++ b/unisportomat/quiz/fixtures/criterion_ratings.json @@ -0,0 +1,11 @@ +[ + { + "model": "quiz.criterionrating", + "pk": 1, + "fields": { + "rating": 1, + "criterion": 1, + "sport": 1 + } + } +] \ No newline at end of file diff --git a/unisportomat/quiz/fixtures/images/logo.png b/unisportomat/quiz/fixtures/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..28ab70d2a640f9b429f12a6ae73e5dca72fd95ef Binary files /dev/null and b/unisportomat/quiz/fixtures/images/logo.png differ diff --git a/unisportomat/quiz/fixtures/knowledge_snacks.json b/unisportomat/quiz/fixtures/knowledge_snacks.json new file mode 100644 index 0000000000000000000000000000000000000000..8b0e3045e3e312b3424579f5618832135f70c998 --- /dev/null +++ b/unisportomat/quiz/fixtures/knowledge_snacks.json @@ -0,0 +1,10 @@ +[ + { + "model": "quiz.knowledgesnack", + "pk": 1, + "fields": { + "text": "Dass Treppensteigen fast 5x so viele Kalorien verbrennt, als die Nutzung des Aufzuges?", + "image": "logo.png" + } + } +] \ No newline at end of file diff --git a/unisportomat/quiz/fixtures/questions.json b/unisportomat/quiz/fixtures/questions.json new file mode 100644 index 0000000000000000000000000000000000000000..d9f455362e4cdbc5858d96e4a0dc1cc95e83079a --- /dev/null +++ b/unisportomat/quiz/fixtures/questions.json @@ -0,0 +1,9 @@ +[ + { + "model": "quiz.question", + "pk": 1, + "fields": { + "text": "Ich mache am liebsten draußen Sport" + } + } +] \ No newline at end of file diff --git a/unisportomat/quiz/fixtures/sports.json b/unisportomat/quiz/fixtures/sports.json new file mode 100644 index 0000000000000000000000000000000000000000..f941796850b903990a16ac8b4c7d822a3a46759b --- /dev/null +++ b/unisportomat/quiz/fixtures/sports.json @@ -0,0 +1,10 @@ +[ + { + "model": "quiz.sport", + "pk": 1, + "fields": { + "name": "Jiu Jitsu", + "url": "http://www.test.de" + } + } +] \ No newline at end of file diff --git a/unisportomat/quiz/fixtures/users.json b/unisportomat/quiz/fixtures/users.json new file mode 100644 index 0000000000000000000000000000000000000000..ab5c93354d2f78a8c99dc8e3218838c668a04798 --- /dev/null +++ b/unisportomat/quiz/fixtures/users.json @@ -0,0 +1 @@ +[{"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$260000$75I180x9FwfD0xuW3TGMU4$B1mg2ywC0kSYkbkmFuSyCq8/yxD9nnU7TNq7VmJsnFs=", "last_login": "2021-06-06T07:41:10.617Z", "is_superuser": true, "username": "admin", "first_name": "", "last_name": "", "email": "", "is_staff": true, "is_active": true, "date_joined": "2021-06-05T15:02:57.005Z", "groups": [], "user_permissions": []}}] \ No newline at end of file diff --git a/unisportomat/quiz/management/__init__.py b/unisportomat/quiz/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/unisportomat/quiz/management/commands/__init__.py b/unisportomat/quiz/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/unisportomat/quiz/management/commands/seed_db.py b/unisportomat/quiz/management/commands/seed_db.py new file mode 100644 index 0000000000000000000000000000000000000000..31f0e9c44073ac1f8ef9641429ea1443efe5f817 --- /dev/null +++ b/unisportomat/quiz/management/commands/seed_db.py @@ -0,0 +1,133 @@ +""" +Seeds the database with test data. +You can call this by python manage.py seed_db +""" + +import random + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.management import call_command +from django.core.management.base import BaseCommand +from quiz.models import ( + Sport, + Criterion, + Question, + CallToMove, + KnowledgeSnack, +) + + +class Command(BaseCommand): + """ + Seeds the database with test data. + You can call this by python manage.py seed_db + """ + + help = "Seeds the database with test data" + + def add_arguments(self, parser): + """ + Add arguments to command that can be added to the command in command line + In this case there's only one arg that you can call by + python manage.py seed_db --yes + """ + parser.add_argument( + "-y", + "--yes", + action="store_true", + help="Don't ask to confirm database flushing", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="Optional seed for random generator. Defaults to 42", + ) + parser.add_argument( + "--no-superuser", + action="store_true", + help="No super user shall be created", + ) + + def handle(self, *args, **options): + """Create some objects for all models""" + + # delete all present database entries (necessary because of unique constraints) + call_command("flush", "--no-input" if options["yes"] else []) + + # Create admin user for using django admin interface during development + if not options["no_superuser"]: + self.stdout.write("\nSpecify admin password for development:") + call_command("createsuperuser", "--username=admin", "--email=''") + + # Seed random generator to make this script deterministic + random.seed(options["seed"]) + + # Create sports + sports_names = [ + "After Work Fitness", + "Ballett", + "Basketball", + "Beachvolleyball", + "Bouldern", + ] + for name in sports_names: + Sport(name=name).save() + + # Create criteria + criteria_names = [ + "Einzelsport", + "Mannschaftssport", + "Ausdauer", + "Kraft", + "Kampfsport", + ] + for name in criteria_names: + Criterion(name=name).save() + + # Create ratings for all sports and criterions + for sport in Sport.objects.all(): + for criterion in Criterion.objects.all(): + sport.rate(criterion, random.randint(1, 10)) + + # Create questions + questions = [ + "Ich würde am liebsten gemeinsam mit anderen trainieren.", + "Teamgeist und Wir-Gefühl sind für mich beim Sport eine große Motivation.", + "Ich betreibe lieber alleine Sport.", + "Ich bin bereit, mir ggf. Material für die Sportart zu kaufen.", + "Ich bevorzuge das Sportangebot draußen in der Natur vor dem Indoor-Angebot.", + ] + + for number, criterion in enumerate(Criterion.objects.all()): + Question(text=questions[number], criterion=criterion).save() + + # Create Calls to Move + calls_to_move = [ + "Kreise deine Schultern vor der nächsten Frage 3x nach hinten", + "Stehe auf, beuge dich mit gestrecktem Rücken nach vorne und greife deinen Stuhl.", + "Mache vor der nächsten Frage 3 Jumping Jacks", + ] + image = SimpleUploadedFile( + name="test_image.png", + content=open("quiz/fixtures/images/test_image.png", "rb").read(), + content_type="image/png", + ) + + for text in calls_to_move: + CallToMove(text=text, image=image).save() + + # Create Knowledge Snacks + knowledge_snacks = [ + "Dass Treppensteigen fast 5x so viele Kalorien verbrennt, wie das Nutzen des Aufzuges?", + "Dass das Spielemobil zur Mittagszeit immer auf dem Campus unterwegs ist?", + "Dass regelmäßige Bewegung Herz-Kreislauf-Erkrankungen vorbeugt?", + ] + image = SimpleUploadedFile( + name="logo.png", + content=open("quiz/fixtures/images/logo.png", "rb").read(), + content_type="image/png", + ) + for text in knowledge_snacks: + KnowledgeSnack(text=text, image=image).save() diff --git a/unisportomat/quiz/models.py b/unisportomat/quiz/models.py index b5eae00bd876bc22db65cfb9efe2373645d96372..d582994f73b3de31d04f59e1a1e8d16539cf3ed8 100644 --- a/unisportomat/quiz/models.py +++ b/unisportomat/quiz/models.py @@ -17,6 +17,9 @@ class CriterionRating(models.Model): criterion = models.ForeignKey("Criterion", on_delete=models.CASCADE) sport = models.ForeignKey("Sport", on_delete=models.CASCADE) + def __str__(self): + return str(self.sport) + " - " + str(self.criterion) + ": " + str(self.rating) + class Sport(models.Model): """ @@ -55,6 +58,9 @@ class Criterion(models.Model): name = models.TextField() + def __str__(self): + return self.name + class CallToMove(models.Model): """Defines text and image that are used to show a call to move between questions""" @@ -62,6 +68,9 @@ class CallToMove(models.Model): text = models.TextField() image = models.ImageField(null=True, max_length=200) + def __str__(self): + return self.text + class KnowledgeSnack(models.Model): """Defines text and image that are used to show a KnowledgeSnack between questions""" @@ -69,6 +78,9 @@ class KnowledgeSnack(models.Model): text = models.TextField() image = models.ImageField(null=True, max_length=200) + def __str__(self): + return self.text + class Question(models.Model): """Defines a Question that is assigned to exactly one Criterion""" @@ -77,3 +89,6 @@ class Question(models.Model): criterion = models.OneToOneField( Criterion, on_delete=models.CASCADE, primary_key=True ) + + def __str__(self): + return self.text diff --git a/unisportomat/quiz/tests.py b/unisportomat/quiz/tests.py index a81a66b6fdcbcc184c2c93fc877372cda3002291..26b095bc147727e05d65c1b63b4b5c1f9beedf28 100644 --- a/unisportomat/quiz/tests.py +++ b/unisportomat/quiz/tests.py @@ -5,9 +5,17 @@ import shutil import tempfile from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.management import call_command from django.test import TestCase, override_settings from django.conf import settings -from .models import Sport, Criterion, CallToMove, KnowledgeSnack, Question +from .models import ( + Sport, + Criterion, + CriterionRating, + CallToMove, + KnowledgeSnack, + Question, +) class SportModelTest(TestCase): @@ -188,3 +196,83 @@ class CriterionAndQuestionModelTest(TestCase): Question(text=text, criterion=self.criterion).save() test_question = Question.objects.first() self.assertEqual(test_question.text, text) + + +class FixturesTest(TestCase): + """ + These are the tests for the fixtures in quiz/fixtures. + Fixtures can be used to populate the database with test data. + They can be used in automated tests, but also in development. + """ + + fixtures = [ + "sports.json", + "criteria.json", + "criterion_ratings.json", + "questions.json", + "calls_to_move.json", + "knowledge_snacks.json", + ] + + def test_sports_created_by_fixture(self): + """If the sports fixture is loaded there exists a sport with the given data.""" + sport = Sport.objects.get(pk=1) + self.assertEqual(sport.name, "Jiu Jitsu") + self.assertEqual(sport.url, "http://www.test.de") + + def test_criterion_created_by_fixture(self): + """If the criteria fixture is loaded there exists a criterion with the given data""" + criterion = Criterion.objects.get(pk=1) + self.assertEqual(criterion.name, "Outdoorsport") + + def test_criterion_rating_created_by_fixture(self): + """If the criterion_ratings fixture is loaded the given sport has a corresponding rating""" + criterion = Criterion.objects.get(name="Outdoorsport") + sport = Sport.objects.get(name="Jiu Jitsu") + self.assertEqual(sport.get_rating(criterion), 1) + + def test_question_created_by_fixture(self): + """If the questions fixture is loaded there exists a question with the given data""" + question = Question.objects.get(pk=1) + criterion = Criterion.objects.get(name="Outdoorsport") + self.assertEqual(question.text, "Ich mache am liebsten draußen Sport") + self.assertEqual(question.criterion, criterion) + + def test_call_to_move_created_by_fixture(self): + """If the call to move fixture is loaded there exists a call to move with the given data""" + call_to_move = CallToMove.objects.get(pk=1) + self.assertEqual( + call_to_move.text, "Kreise deine Arme vor der nächsten Frage 3x nach hinten" + ) + self.assertEqual(call_to_move.image.name, "test_image.png") + + def test_knowledge_snack_created_by_fixture(self): + """ + If the knowledge snack fixture is loaded there exists a knowledge snack with the given data + """ + knowledge_snack = KnowledgeSnack.objects.get(pk=1) + self.assertEqual( + knowledge_snack.text, + "Dass Treppensteigen fast 5x so viele Kalorien verbrennt, " + "als die Nutzung des Aufzuges?", + ) + self.assertEqual(knowledge_snack.image.name, "logo.png") + + +class SeedingTest(TestCase): + """Tests the seed_db command in quiz/management/commands""" + + def test_seed_with_complete_data(self): + """If seed_db is called then there exists a certain number of elements per model""" + + # call the seed command without asking for confirmation + call_command("seed_db", ["--yes", "--no-superuser"]) + + n_sports = 5 + n_criteria = 5 + self.assertEqual(Sport.objects.count(), n_sports) + self.assertEqual(Criterion.objects.count(), n_criteria) + self.assertEqual(CriterionRating.objects.count(), n_sports * n_criteria) + self.assertEqual(Question.objects.count(), n_criteria) + self.assertEqual(CallToMove.objects.count(), 3) + self.assertEqual(KnowledgeSnack.objects.count(), 3)