.. _3_entwicklung: ########### Entwicklung ########### Nachdem der Entwurf im Großen und Ganzen fertig war, konnte ich mich an die Programmierung der Webseite machen. Ich habe mich dazu entschieden, mit Django zu arbeiten, da mir vor allem die Django-Administrationsoberfläche sehr gut gefällt und ich dieses Framework gerne einmal ausprobieren möchte. Als Entwicklungsumgebung habe ich hauptsächlich PyCharm Professional verwendet. Ich verwende die JetBrains-IDEs schon seit Jahren und habe damit nur gute Erfahrungen gemacht. Die Dokumentation habe ich mit VSCode geschrieben, da PyCharm keine Preview-Funktion für Sphinx besitzt. Django hat fast alle Funktionen, die für eine Webanwendung benötigt werden, bereits eingebaut. Die einzigen Bibliotheken, ich zusätlich benötigt habe, sind `requests `_, `django-hashid-field `_ und `django-import-export `_. Requests wird für den Zugriff auf die Unsplash-API benötigt. Django-Hashid-Field erlaubt es, IDs von Datenbankfeldern als 7stelligen Code auszugeben, was für Spiel- und Spieler-IDs benötigt wird. Django-Import-Export ermöglicht es, Daten über die Adminoberfläche zum importieren und exportieren, was beim Einfügen von Fragen sehr hilfreich ist. Spiellogik ########## Ich habe mich dazu entschieden, die Anwendungslogik von der Darstellung zu trennen. Deswegen habe ich das Verzeichnis ``Controller`` angelegt, in dem sich sämtliche Klassen der Anwendungslogik befinden. Die Klasse ``GameLogic`` kann mit dem Spiel- und Spielercode instanziiert werden und beinhaltet Methoden zur Ausführung sämtlicher Spieleraktionen (Auswahl des zu entfernenden Bildes, Abstimmung) sowie zum Abrufen der Spielinformationen. Die Klasse ``Images`` beinhaltet die Methoden für den Zugriff auf die Unsplash-API und das Abrufen von neuen Bildern. Diese Methoden können dann in den `Views`_ aufgerufen werden. Die genaue Dokumentation dieser Klassen findet sich ebenso wie eine Dokumentation des Datenmodells im Abschnitt :ref:`reference`. Views ##### Scio besteht aus insgesamt 5 Views, davon 3 Webseiten und 2 API-Endpoints. .. autoclass:: scio.views.home .. autoclass:: scio.views.game .. autoclass:: scio.views.play .. autoclass:: scio.views.api_gameinfo .. autoclass:: scio.views.api_playerinfo Design ###### Das Design habe ich auf `codepen.io `_ erstellt und getestet. Ich konnte das vorher entworfene Design mit minimalen Änderungen umsetzen. .. figure:: _static/img/design1.png :height: 350 Bilder anzeigen, auswählen und entfernen .. figure:: _static/img/design2.png :height: 350 Testformular .. figure:: _static/img/design3.png :height: 350 Wertungstabelle, Punktzahlen der einzelnen Spieler Frontend ######## Das Frontend für das Spiel wurde mit Vue.js implementiert. Die Spielseite ruft alle 2 Sekunden über ein Javascript die Spieldaten von der Webseite ab. Wenn sich die Spielinhalte durch die Aktion eines anderen Spielers (z.B. Abstimmung) ändern, wird die Ansicht so automatisch aktualisiert. Diese Implementation ist nicht optimal, es wäre auch möglich, den Datenaustausch zwischen Javascript und Server mittels Websockets durchzuführen. Dies ist aber mit Django nicht ohne weiteres möglich. Da ich hier keine zeitkritische Anwendung habe und die Abfragen nur für einen relativ kurzen Zeitraum und nicht permanent im Hintergrund ausgeführt werden müssen, ist Polling dennoch eine adäquate Lösung. Probleme ######## Das Django-Framework ist sehr gut dokumentiert und ich konnte fast alle Informationen auf deren Webseite finden. Insbesondere die Handhabung des Datenmodells brauchte etwas Einarbeitungszeit, da sich die Verwendung des Django-ORMs doch sehr von einfachen SQL-Abfragen unterscheidet, mit denen ich bisher meist gearbeitet hatte. Es gab ein Problem bei der Modellierung von Beziehungen, bei dem ich etwas länger suchen musste, um es zu lösen. Wenn man in Django mit ``models.ForeignKey`` eine Beziehung erstellt, ist diese standardmäßig bidirektional. Erstellt man also in Klasse A 2 Beziehungen auf Klasse B, so sind diese Beziehungen zwar in A->B - Richtung eindeutig, jedoch nicht in die umgekehrte Richtung. .. code-block:: python class Round(models.Model): question1 = models.ForeignKey(Question, on_delete=models.SET_NULL) question2 = models.ForeignKey(Question, on_delete=models.SET_NULL) In diesem Beispiel könnte man mit einem Filter-Statement wie ``Round.objects.filter(question1=q)`` zwar eindeutig alle Runden herausfiltern, die eine bestimmte Frage1 beinhalten. Umgekehrt gäbe es aber ein Problem, da Django standardmäßig den Namen der Klasse A als Schlüssel für den Beziehungspartner auf der Seite von Klasse B verwendet. Sollte also der Befehl ``Question.objects.filter(round=r)`` Frage 1 oder Frage 2 der Runde r zurückgeben? Diese Situation wäre uneindeutig, weswegen Django bei Erstellung des Modells einen Fehler ausgibt: .. code-block:: text ERRORS: : (admin.E108) The value of 'list_display[4]' refers to 'question', which is not a callable, an attribute of 'RoundAdmin', or an attribute or method on 'scio.Round'. scio.Round.question1: (fields.E304) Reverse accessor for 'Round.question1' clashes with reverse accessor for 'Round.question2'. HINT: Add or change a related_name argument to the definition for 'Round.question1' or 'Round.question2'. scio.Round.question2: (fields.E304) Reverse accessor for 'Round.question2' clashes with reverse accessor for 'Round.question1'. HINT: Add or change a related_name argument to the definition for 'Round.question2' or 'Round.question1'. Die Lösung für dieses Problem ist es, den Schlüssel für den Beziehungspartner mit dem Feld ``related_name`` zu überschreiben. .. code-block:: python class Round(models.Model): question1 = models.ForeignKey(Question, related_name='round_q1' on_delete=models.SET_NULL) question2 = models.ForeignKey(Question, related_name='round_q2', on_delete=models.SET_NULL) Man kann ``related_name`` auch auf einen leeren String setzen. In diesem Fall kann von Klasse B nicht mehr auf den Beziehungspartner A zugegriffen werden, man erhält also eine unidirektionale Beziehung.