Skip to content
Snippets Groups Projects
Commit 7814fa55 authored by borzechof99's avatar borzechof99 :whale2:
Browse files

Merge branch 'master' into...

Merge branch 'master' into 49-implement-sport-scraper-and-sport-archive-api-endpoints-for-admin-frontend
parents 3cd7b658 1bb726f1
No related branches found
No related tags found
No related merge requests found
Showing
with 736 additions and 16 deletions
......@@ -8,4 +8,7 @@ disable=line-too-long,
no-self-use,
abstract-method,
arguments-differ,
invalid-name
invalid-name,
no-else-raise,
arguments-renamed,
......@@ -134,4 +134,9 @@ instead of returning Result(data), the paginator needs to be used again so it ca
return paginator.get_paginated_response(serializer.data)
```
This function already returns a fully valid Response, so it can be directly returned.
\ No newline at end of file
This function already returns a fully valid response, so it can be directly returned.
## Our Handling of REST Standards
In our usecase of the Admin Frontend, we either explicitly work on objects we chose from a list of given objects, or we create a new object all together. Because of these circumstances, it is not relevant for the PUT request to create new objects entirely, since we know that all objects currently being edited exist. This means that the PUT implementations in `views.py` do not create new objects if called with an unknown primary key. Instead, they are similar to PATCH, but require all fields to be sent along instead of just a portion of them as PATCH does.
\ No newline at end of file
......@@ -15,6 +15,7 @@ from quiz.models import (
Question,
CallToMove,
KnowledgeSnack,
QuestionOrderEntry,
)
......@@ -132,11 +133,12 @@ class Command(BaseCommand):
),
("Mache vor der nächsten Frage 3 Jumping Jacks", "Do Three Jumping Jacks."),
]
image = SimpleUploadedFile(
name="test_image.png",
content=open("quiz/fixtures/images/test_image.png", "rb").read(),
content_type="image/png",
)
with open("quiz/fixtures/images/test_image.png", "rb") as read_file:
image = SimpleUploadedFile(
name="test_image.png",
content=read_file.read(),
content_type="image/png",
)
for text in calls_to_move:
activate("de")
......@@ -160,14 +162,25 @@ class Command(BaseCommand):
"That proper training prevents heart disease?",
),
]
image = SimpleUploadedFile(
name="logo.png",
content=open("quiz/fixtures/images/logo.png", "rb").read(),
content_type="image/png",
)
with open("quiz/fixtures/images/logo.png", "rb") as read_file:
image = SimpleUploadedFile(
name="logo.png",
content=read_file.read(),
content_type="image/png",
)
for text in knowledge_snacks:
activate("de")
k_s = KnowledgeSnack(text=text[0], image=image)
activate("en")
k_s.text = text[1]
k_s.save()
# Create Entries in the QuestionOrder DB
QuestionOrderEntry.objects.create_entry_at_end("question", 1)
QuestionOrderEntry.objects.create_entry_at_end("snack")
QuestionOrderEntry.objects.create_entry_at_end("question", 3)
QuestionOrderEntry.objects.create_entry_at_end("activity")
QuestionOrderEntry.objects.create_entry_at_end("question", 2)
QuestionOrderEntry.objects.create_entry_at_end("question", 4)
# Generated by Django 3.2 on 2021-06-27 14:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0006_auto_20210612_1230"),
]
operations = [
migrations.CreateModel(
name="QuestionOrder",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order_id", models.IntegerField(null=True)),
(
"type_of_slot",
models.TextField(
choices=[
("question", "question"),
("snack", "snack"),
("activity", "activity"),
],
default="snack",
),
),
("question_id", models.IntegerField(default=-1)),
],
),
]
# Generated by Django 3.2 on 2021-06-27 18:12
from django.db import migrations, models
import quiz.models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0007_questionorder"),
]
operations = [
migrations.RenameModel(
old_name="QuestionOrder",
new_name="QuestionOrderEntry",
),
migrations.AlterField(
model_name="criterionrating",
name="rating",
field=models.IntegerField(validators=[quiz.models.validate_rating]),
),
]
# Generated by Django 3.2 on 2021-06-27 22:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("quiz", "0008_auto_20210627_1812"),
("quiz", "0011_alter_sport_last_used"),
]
operations = []
# Generated by Django 3.2 on 2021-06-28 17:51
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("quiz", "0012_merge_20210627_2254"),
]
operations = [
migrations.CreateModel(
name="GreetingEndTexts",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"greeting",
models.TextField(default="Willkommen zum Uni-Sport-O-Mat!"),
),
(
"greeting_de",
models.TextField(
default="Willkommen zum Uni-Sport-O-Mat!", null=True
),
),
(
"greeting_en",
models.TextField(
default="Willkommen zum Uni-Sport-O-Mat!", null=True
),
),
("end", models.TextField(default="Wähle deinen Sport!")),
("end_de", models.TextField(default="Wähle deinen Sport!", null=True)),
("end_en", models.TextField(default="Wähle deinen Sport!", null=True)),
],
),
migrations.AlterField(
model_name="sport",
name="last_used",
field=models.DateField(default=django.utils.timezone.localdate),
),
]
# Generated by Django 3.2 on 2021-06-28 18:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("quiz", "0013_auto_20210628_1751"),
]
operations = [
migrations.CreateModel(
name="EndText",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("text", models.TextField(default="Wähle deinen Sport!")),
("text_de", models.TextField(default="Wähle deinen Sport!", null=True)),
("text_en", models.TextField(default="Wähle deinen Sport!", null=True)),
],
),
migrations.CreateModel(
name="GreetingText",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("text", models.TextField(default="Willkommen zum Uni-Sport-O-Mat!")),
(
"text_de",
models.TextField(
default="Willkommen zum Uni-Sport-O-Mat!", null=True
),
),
(
"text_en",
models.TextField(
default="Willkommen zum Uni-Sport-O-Mat!", null=True
),
),
],
),
migrations.DeleteModel(
name="GreetingEndTexts",
),
]
......@@ -12,7 +12,7 @@ def validate_rating(value):
"""
if not ((10 >= value >= 1) or value == -1):
raise ValidationError(u"%s is not a valid rating!" % value)
raise ValidationError("%s is not a valid rating!" % value)
class CriterionRating(models.Model):
......@@ -197,3 +197,131 @@ class Question(models.Model):
def __str__(self):
return self.text
class QuestionOrderEntryManager(models.Manager):
"""
Manages Handling of QuestionOrderEntry Objects
Includes create_entry_at_end, which handles Creation of new Entries
and delete_entry, which deletes one entry and changes the order_id of the following entries
"""
def create_entry_at_end(self, type_of_slot, question_id=None):
"""
Creates a new OrderEntry at the end of the current Order (so the object gets given the highest order_id)
"""
if type_of_slot == "question" and question_id is None:
raise ValueError(
"A Question ID must be given if the Type of the Slot is Question"
)
elif type_of_slot not in [
"question",
"snack",
"activity",
]:
raise ValueError(
f'{type_of_slot} not in valid choice list ["question", "snack", "activity"]'
)
entry = QuestionOrderEntry()
entry.type_of_slot = type_of_slot
# If question_id is None, fill it out as -1, else take the question_id value
if entry.type_of_slot == "question":
entry.question_id = question_id
# If no Entry exists, highest_current_order is 0, otherwise highest order_id
if self.count() == 0:
highest_current_order = 0
else:
# "latest" returns highest value (normally used for dates, I believe)
highest_current_order = self.latest("order_id").order_id
entry.order_id = highest_current_order + 1
entry.save()
return entry
def delete_entry(self, given_order_id):
"""
Delete an Entry in the QuestionOrderEntry Database and decrement every object that had a larger order_id than the one deleted
"""
# Delete must be called on an instance, not a queryset
entry = self.get(order_id=given_order_id)
entry.delete()
larger_entries = self.filter(order_id__gte=given_order_id)
# The Primary Key of an Object cannot be changed, really, instead a new Object is created when the PK changes
# This means that we need to delete the original Object so that we only have the new pk
for entry in larger_entries.iterator():
entry.order_id = entry.order_id - 1
entry.save()
def delete_entry_by_question_id(self, given_question_id):
"""
Delete an Entry in the QuestionOrderEntry DB by the Question_ID
"""
queryset = self.filter(question_id=given_question_id)
if queryset.count() == 0:
# If the Question doesn't exist in the Order, we don't need to do anything
return
# If the question is in the Order more than once for some reason, sweat not, we will delete them all
for entry in queryset.iterator():
self.delete_entry(entry.order_id)
class QuestionOrderEntry(models.Model):
"""
Defines the order of the Questions, Snacks, and Activities given in the Quiz
Default Choice for type_of_slot is "snack" instead of "question",
because "snack" doesn't need a question_id and is therefore a safer choice
"""
objects = QuestionOrderEntryManager()
order_id = models.IntegerField(null=True)
type_of_slot = models.TextField(
choices=[
("question", "question"),
("snack", "snack"),
("activity", "activity"),
],
default="snack",
)
question_id = models.IntegerField(default=-1)
def __str__(self):
return f"Entry {self.order_id}: {self.type_of_slot}"
class GreetingText(models.Model):
"""
Database with only one row (if everything is done right)
Includes start text as column
"""
text = models.TextField(default="Willkommen zum Uni-Sport-O-Mat!")
def __str__(self):
return f"{self.text}"
class EndText(models.Model):
"""
Database with only one row (if everything is done right)
Includes end text as column
"""
text = models.TextField(default="Wähle deinen Sport!")
def __str__(self):
return f"{self.text}"
......@@ -230,3 +230,21 @@ class ArchiveSerializer(serializers.BaseSerializer):
)
return json_sport_list
class GreetingEndSerializer(serializers.BaseSerializer):
"""
Serializer for GreetingText and EndText
"""
def to_representation(self, obj):
"""
Represents the object with German and English text
"""
json_obj = {
"text_de": obj.text_de,
"text_en": obj.text_en,
}
return json_obj
""" This module tests all our quiz models"""
import os
import re
import shutil
import tempfile
......@@ -13,6 +14,7 @@ from django.test import TestCase, override_settings
from rest_framework.test import APITestCase
from django.conf import settings
from .models import (
QuestionOrderEntry,
Sport,
Criterion,
CriterionRating,
......@@ -534,7 +536,11 @@ class APITest(APITestCase):
def test_currently_active(self):
"""
Tests if PATCHing the 'currently_active' value to false correctly changes the sport
<<<<<<< HEAD
Tests if PATCHing the 'currently_active' value to false correctly changes the sport
=======
Tests if PATCHing the "currently_active" value to false correctly changes the sport
>>>>>>> master
"""
# Set Up Values
......@@ -597,3 +603,217 @@ class APITest(APITestCase):
sport_response = self.client.get(reverse("archive"))
self.assertEqual(len(sport_response.data["results"]), 1)
def test_greeting_view(self):
"""
Test whether the greeting behaves correctly
"""
response = self.client.get(reverse("greeting"))
self.assertEqual(response.data["text_de"], "Willkommen zum Uni-Sport-O-Mat!")
self.assertEqual(response.data["text_de"], response.data["text_en"])
# Test whether new Values change correctly
data = {"text_de": "Hallo", "text_en": "Hi"}
response = self.client.post(reverse("greeting"), format="json", data=data)
response = self.client.get(reverse("greeting"))
self.assertEqual(response.data["text_de"], "Hallo")
self.assertEqual(response.data["text_en"], "Hi")
# If the object is deleted, the default values are returned again
response = self.client.delete(reverse("greeting"))
self.assertEqual(response.data["text_de"], "Willkommen zum Uni-Sport-O-Mat!")
self.assertEqual(response.data["text_de"], response.data["text_en"])
def test_end_view(self):
"""
Test whether the end behaves correctly
"""
response = self.client.get(reverse("end"))
self.assertEqual(response.data["text_de"], "Wähle deinen Sport!")
self.assertEqual(response.data["text_de"], response.data["text_en"])
# Test whether new values change correctly
data = {"text_de": "Hallo", "text_en": "Hi"}
response = self.client.post(reverse("end"), format="json", data=data)
response = self.client.get(reverse("end"))
self.assertEqual(response.data["text_de"], "Hallo")
self.assertEqual(response.data["text_en"], "Hi")
# If the object is deleted, the default values are returned again
response = self.client.delete(reverse("end"))
self.assertEqual(response.data["text_de"], "Wähle deinen Sport!")
self.assertEqual(response.data["text_de"], response.data["text_en"])
class QuestionOrderEntry_Test(TestCase):
"""
Tests the QuestionOrderEntry Model and its Manager
"""
def setUp(self):
"""
Sets up DB with Five Default Questions from seed_db
"""
# Setup the Database with seeded values
call_command("seed_db", ["--yes", "--no-superuser"])
# We get Five Questions, that's all we need
# Also, we assume that we are working with an empty QuestionOrderEntry Table
QuestionOrderEntry.objects.all().delete()
def test_check_empty_oder_table(self):
"""
Checks Whether Initial QuestionOrder Table is Empty
"""
self.assertEqual(QuestionOrderEntry.objects.count(), 0)
def test_add_question_to_table(self):
"""
Tests adding a Question to the Table
"""
entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=2
)
self.assertEqual(QuestionOrderEntry.objects.count(), 1)
self.assertEqual(entry.order_id, 1)
self.assertEqual(entry.type_of_slot, "question")
self.assertEqual(entry.question_id, 2)
def test_add_snack_activity_to_table(self):
"""
Tests adding a Snack and Activity to the Table
"""
entry = QuestionOrderEntry.objects.create_entry_at_end("snack")
self.assertEqual(QuestionOrderEntry.objects.count(), 1)
self.assertEqual(entry.order_id, 1)
self.assertEqual(entry.type_of_slot, "snack")
self.assertEqual(entry.question_id, -1)
# Add Question_ID to activity to see whether it is correctly ignored
entry = QuestionOrderEntry.objects.create_entry_at_end(
"activity", question_id=3
)
self.assertEqual(QuestionOrderEntry.objects.count(), 2)
self.assertEqual(entry.order_id, 2)
self.assertEqual(entry.type_of_slot, "activity")
self.assertEqual(entry.question_id, -1)
def test_autoincrementing_id(self):
"""
When Entries are deleted, do the automatically generated PK-Fields reuse the IDs or do they stil increment?
"""
# Create First and Second Entry
entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=1
)
self.assertEqual(QuestionOrderEntry.objects.count(), 1)
self.assertEqual(entry.order_id, 1)
entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=2
)
self.assertEqual(QuestionOrderEntry.objects.count(), 2)
self.assertEqual(entry.order_id, 2)
# Check Second Entry
most_recent_entry = QuestionOrderEntry.objects.get(order_id=2)
self.assertEqual(most_recent_entry.order_id, 2)
# Delete Second Entry
QuestionOrderEntry.objects.delete_entry(2)
self.assertEqual(QuestionOrderEntry.objects.count(), 1)
# Create Third Entry and Check whether the ID of the Second Entry is Reused (it should be)
entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=3
)
self.assertEqual(QuestionOrderEntry.objects.count(), 2)
self.assertEqual(entry.order_id, 2)
def test_removing_entry_in_middle(self):
"""
When an Entry in the Middle is Removed, the order_ids larger than the removed entry should be decremented
"""
# Create Three Questions
first_entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=1
)
self.assertEqual(QuestionOrderEntry.objects.count(), 1)
self.assertEqual(first_entry.order_id, 1)
second_entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=2
)
self.assertEqual(QuestionOrderEntry.objects.count(), 2)
self.assertEqual(second_entry.order_id, 2)
third_entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=3
)
self.assertEqual(QuestionOrderEntry.objects.count(), 3)
self.assertEqual(third_entry.order_id, 3)
# Use Custom Deletion Method to remove second Entry
QuestionOrderEntry.objects.delete_entry(2)
# Check whether Question Higher than the deleted one went down one slot
self.assertEqual(QuestionOrderEntry.objects.count(), 2)
last_entry = QuestionOrderEntry.objects.last()
self.assertEqual(last_entry.order_id, 2)
self.assertEqual(last_entry.question_id, 3)
def test_removing_entry_in_middle_by_question_id(self):
"""
When an Entry in the Middle is Removed, the order_ids larger than the removed entry should be decremented
"""
# Create Three Questions
first_entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=1
)
self.assertEqual(QuestionOrderEntry.objects.count(), 1)
self.assertEqual(first_entry.order_id, 1)
second_entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=2
)
self.assertEqual(QuestionOrderEntry.objects.count(), 2)
self.assertEqual(second_entry.order_id, 2)
third_entry = QuestionOrderEntry.objects.create_entry_at_end(
"question", question_id=3
)
self.assertEqual(QuestionOrderEntry.objects.count(), 3)
self.assertEqual(third_entry.order_id, 3)
# Use Custom Deletion Method to remove second Entry
QuestionOrderEntry.objects.delete_entry_by_question_id(2)
# Check whether Question Higher than the deleted one went down one slot
self.assertEqual(QuestionOrderEntry.objects.count(), 2)
last_entry = QuestionOrderEntry.objects.last()
self.assertEqual(last_entry.order_id, 2)
self.assertEqual(last_entry.question_id, 3)
......@@ -4,7 +4,7 @@ Here, every Model which needs translation fields is registered.
from modeltranslation.translator import register, TranslationOptions
from .models import Question, CallToMove, KnowledgeSnack
from .models import Question, CallToMove, KnowledgeSnack, GreetingText, EndText
@register(Question)
......@@ -41,3 +41,21 @@ class KnowledgeSnackTranslationOptions(TranslationOptions):
fields = ("text",)
required_languages = ("de",)
fallback_values = ("No Translation for this Field",)
@register(GreetingText)
class GreetingTextTranslationOptions(TranslationOptions):
"""
Translation options for GreetingText.
"""
fields = ("text",)
@register(EndText)
class EndTextTranslationOptions(TranslationOptions):
"""
Translation options for EndText.
"""
fields = ("text",)
......@@ -21,8 +21,9 @@ from .serializers import (
CriteriaSerializer,
IncompleteSportSerializer,
ArchiveSerializer,
GreetingEndSerializer,
)
from .models import Sport, Criterion, Question
from .models import Sport, Criterion, Question, GreetingText, EndText
# Create your views here.
......@@ -456,3 +457,127 @@ class ScraperView(APIView):
sport.save()
return Response(status=200)
class GreetingEndView(APIView):
"""
View for handling the beginning sentence
"""
given_object = None
def post(self, request):
"""
api/greeting POST
Creates a new object if none exist, otherwise forwards to PUT
"""
if self.given_object.objects.count() > 0:
return self.put(request)
sentence = self.given_object() # pylint: disable=not-callable
if "text_de" in request.data.keys():
sentence.text_de = request.data["text_de"]
if "text_en" in request.data.keys():
sentence.text_en = request.data["text_en"]
sentence.save()
return Response(GreetingEndSerializer(sentence).data)
def get(self, request):
"""
api/greeting GET
Sends out the greeting
"""
sentence = self.given_object.objects.all()
if sentence.count() == 0:
sentence = self.given_object() # pylint: disable=not-callable
else:
sentence = sentence.first()
return Response(GreetingEndSerializer(sentence).data)
def put(self, request):
"""
api/greeting PUT
Overwrites German and English beginning
"""
sentence = self.given_object.objects.all()
if sentence.count() == 0:
return Response(status=404)
sentence = sentence.first()
sentence.text_de = request.data["text_de"]
sentence.text_en = request.data["text_en"]
sentence.save()
return Response(GreetingEndSerializer(sentence).data)
def patch(self, request):
"""
api/greeting PATCH
Overwrites German and/or English beginning, if they exist
"""
sentence = self.given_object.objects.all()
if sentence.count() == 0:
return Response(status=404)
sentence = sentence.first()
if "text_de" in request.data.keys():
sentence.text_de = request.data["text_de"]
if "text_en" in request.data.keys():
sentence.text_en = request.data["text_en"]
sentence.save()
return Response(GreetingEndSerializer(sentence).data)
def delete(self, request):
"""
api/greeting DELETE
Deletes the object so default values are reinstated
"""
sentence = self.given_object.objects.all()
if sentence.count() == 0:
return Response(status=404)
sentence = sentence.first()
sentence.text_de = self.given_object._meta.get_field("text").get_default()
sentence.text_en = self.given_object._meta.get_field("text").get_default()
sentence.save()
return Response(GreetingEndSerializer(sentence).data)
class GreetingView(GreetingEndView):
"""
View for the greeting strings
Inherited CRUD from GreetingEndView
"""
given_object = GreetingText
class EndView(GreetingEndView):
"""
View for the end strings
Inherited CRUD from GreetingEndView
"""
given_object = EndText
......@@ -35,4 +35,6 @@ urlpatterns = [
path("api/admin/criteria/", views.CriteriaView.as_view(), name="criteria"),
path("api/admin/sport/archive/", views.SportArchiveView.as_view(), name="archive"),
path("api/admin/sport/scraper/", views.ScraperView.as_view(), name="scraper"),
path("api/admin/greeting/", views.GreetingView.as_view(), name="greeting"),
path("api/admin/end/", views.EndView.as_view(), name="end"),
]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment