Django: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
No edit summary
 
(132 intermediate revisions by the same user not shown)
Line 1: Line 1:
=Setting up REST API within Django=
=Django=
These are just notes from a demo
=Introduction=
=Useful Commands=
Named after Django Reinhardt. It is a Python web framework. Packages to support Django can be found at http://awesom-django.com It includes
==Login to Container==
*ORM (Object mapping to DB)
Login to the container with
*URL Mapping
*Templates
*Forms
*Admin
*Package
Some of the design principles can be found at http://goo.gl/PRrEMe. Django using an Model Template View approach which is like MVC Where the template is the view and the view is a controller.
[[File:Django MTV.png|600px]]
=Installation And Create am App=
We can create a python environment and install with. Using and environment keeps the packages local and not available at a user level or global level
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
docker-compose exec backend bash
python3 -m venv django-env
. django-env/bin/activate
pip install django
</syntaxhighlight>
We can set the environment in VS Code with <br>
[[File:Django Python.png|500px]]
Within Python we can now create our project
<syntaxhighlight lang="python">
django-admin startproject myapp
</syntaxhighlight>
<br>
Key Files created are
*manage.py (Management)
*settings.py (Settings)
*urls.py (Routing)
*wsgi.py (Packaging)
We can run the development server with
<syntaxhighlight lang="python">
python3 manage.py runserver
</syntaxhighlight>
=Creating a View=
We can create view by creating a view and add a route.
<syntaxhighlight lang="python">
from django.http import HttpResponse
 
def welcome(request):
    return HttpResponse("Hello World")
</syntaxhighlight>
And adding the route to urls.py
<syntaxhighlight lang="python">
from .views import welcome
from django.contrib import admin
from django.urls import path
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('welcome/', welcome),
]
</syntaxhighlight>
Not you can use regex for urls with re_path e.g.
<syntaxhighlight lang="python">
    re_path(r'welcome/', welcome),
</syntaxhighlight>
=Creating Apps=
Within in the directory we can divide the app into apps. So go into the directory containing manage.py
<syntaxhighlight lang="python">
python3 manage.py startapp gameplay
</syntaxhighlight>
Within the settings file we need to add the new app, in this case gameplay.
<syntaxhighlight lang="python">
# Application definition
 
INSTALLED_APPS = [
    'django.contrib.admin',
...
    'gameplay'
]
</syntaxhighlight>
</syntaxhighlight>
=Creating A Container With Composer=
=Models=
==Create a Container==
==Introduction==
We can build a container using docker-compose. Create a docker file with
We Create models using the django models class. You can see we can create foreign keys and cascade deletes which are all documented.
<syntaxhighlight lang="Dockerfile">
<syntaxhighlight lang="python">
FROM python:3.9
from django.db import models
ENV  PYTHONNUMBUFFERED 1
from django.contrib.auth.models import User
WORKDIR /app
 
COPY requirements.txt /app/requirements.txt
class Game(models.Model):
RUN pip install -r requirements.txt
    first_player = models.ForeignKey(
COPY . /app
        User,
        related_name="games_first_player",
        on_delete=models.CASCADE,)
 
    second_player = models.ForeignKey(
        User,
        related_name="games_second_player",
        on_delete=models.CASCADE,)
 
    start_time = models.DateTimeField(auto_now_add=True)
    last_active = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=1, default='F')
 
 
class Move(models.Model):
    x = models.IntegerField()
    y = models.IntegerField()
    comment = models.CharField(max_length=300, blank=True)
    by_first_player = models.BooleanField()


CMD python manage.py runserver 0.0.0.0:8000
    game = models.ForeignKey(Game, on_delete=models.CASCADE)
</syntaxhighlight>
</syntaxhighlight>
==Field Types==
Options
* Make Field Nullable (Default is non Null)
Model.IntegerField(null = True)
* Allow Empty Values in Forms (Not db-related)
Model.CharField(blank = True)
* Default Value
Model.IntegerField(default = 'F')
* Typed-specific options
Model.DateTimeField(auto_now = 'True')
<br>
<br>
Next write a yaml to install software
<br>
<syntaxhighlight lang="yaml">
Here is a list of some of the fields provided.
version: '3.8'
<br>
services:
<table>
  backend:
<tr><th>Field Name     </th><th>Description</th></tr>
     build:
<tr><td>AutoField     </td><td>It An IntegerField that automatically increments.</td></tr>
      context: .
<tr><td>BigAutoField</td><td>It is a 64-bit integer, much like an AutoField except that it is guaranteed to fit numbers from 1 to 9223372036854775807.</td></tr>
      dockerfile: Dockerfile
<tr><td>BigIntegerField</td><td>It is a 64-bit integer, much like an IntegerField except that it is guaranteed to fit numbers from -9223372036854775808 to 9223372036854775807.</td></tr>
     ports:
<tr><td>BinaryField </td><td>A field to store raw binary data.</td></tr>
      - 8000:8000
<tr><td>BooleanField </td><td>A true/false field. The default form widget for this field is a CheckboxInput.</td></tr>
     volumes:
<tr><td>CharField </td><td>It is a date, represented in Python by a datetime.date instance.</td></tr>
      - .:/app
<tr><td>DateField </td><td>A date, represented in Python by a datetime.date instance. It is used for date and time, represented in Python by a datetime.datetime instance.</td></tr>
    depends_on:
<tr><td>DecimalField </td><td>It is a fixed-precision decimal number, represented in Python by a Decimal instance.</td></tr>
      - db
<tr><td>DurationField </td><td>A field for storing periods of time.</td></tr>
  db:  
<tr><td>EmailField </td><td>It is a CharField that checks that the value is a valid email address.</td></tr>
     image: mysql:5.7.32
<tr><td>FileField </td><td>It is a file-upload field.</td></tr>
     restart: always
<tr><td>FloatField </td><td>It is a floating-point number represented in Python by a float instance.</td></tr>
     environment:  
<tr><td>ImageField </td><td>It inherits all attributes and methods from FileField, but also validates that the uploaded object is a valid image.</td></tr>
      MYSQL_DATABASE: admin
<tr><td>IntegerField </td><td>It is an integer field. Values from -2147483648 to 2147483647 are safe in all |databases supported by Django.</td></tr>
      MYSQL_USER: root
<tr><td>GenericIPAddressField </td><td>An IPv4 or IPv6 address, in string format (e.g. 192.0.2.30 or 2a02:42fe::4).</td></tr>
      MYSQL_PASSWORD: root
<tr><td>NullBooleanField </td><td>Like a BooleanField, but allows NULL as one of the options.</td></tr>
      MYSQL_ROOT_PASSWORD: root
<tr><td>PositiveIntegerField </td><td>Like an IntegerField, but must be either positive or zero (0).</td></tr>
    volumes:
<tr><td>PositiveSmallIntegerField </td><td>Like a PositiveIntegerField, but only allows values under a certain (database-dependent) point.</td></tr>
      - .dbdata:/var/lib/nysql
<tr><td>SlugField </td><td>Slug is a newspaper term. A slug is a short label for something, containing only letters, numbers, underscores or hyphens. They’re generally used in URLs.</td></tr>
    ports:
<tr><td>SmallIntegerField </td><td>It is like an IntegerField, but only allows values under a certain (database-dependent) point.</td></tr>
      - 33066:3306
<tr><td>TextField </td><td>A large text field. The default form widget for this field is a Textarea.</td></tr>
<tr><td>TimeField </td><td>A time, represented in Python by a datetime.time instance.</td></tr>
<tr><td>URLField </td><td>A CharField for a URL, validated by URLValidator.</td></tr>
<tr><td>UUIDField </td><td>A field for storing universally unique identifiers. Uses Python’s UUID class. When used on PostgreSQL, this stores in a uuid datatype, otherwise in a char(32).</td></tr>
<tr><td>
</table>
==Model API (Repository)==
There are lots of operation you can do on Models. Here are some common ones
<syntaxhighlight lang="python">
# Must return 1
Game.objects.get(pk=5)
# Returns all rows
Game.objects.all()
# Returns matching objects
Game.objects.filter(status = 'A')
</syntaxhighlight>
==Model Query Sets==
Our models inherit from models.Mdoel however there is a models.QuerySet which allows queries across models. Within the models we can construct our own views of the data. In the code we want to get all of the games for a user. Previously the code was written in the view to query the model twice. Once for the games as first player and once for the games as second player.
<syntaxhighlight lang="python">
def home(request)
     games_first_player = Game.objects.filter(
        first_player=request.user
        status='F'
    )
     games_second_player = Game.objects.filter(
        second_player=request.user
        status='S'
    )
     all_my_games = list(games_first_player) + \
                  list(games_second_player)
...
</syntaxhighlight>
If we use the Q function, which allows us to use an or '|' within the query we can get this data with only one query. We then override the objects in the model class to use the GamesQuerySet
<syntaxhighlight lang="python">
...
from django.db.models import Q
...
class GamesQuerySet(models.QuerySet):
     def games_for_user(self, user):
        return self.filter(
            Q(first_player=user) | Q(second_player=user)
        )
 
class Game(models.Model):
...
     objects = GamesQuerySet.as_manager()
 
     def __str__(self):
        return "{0} vs {1}".format(
            self.first_player, self.second_player)
 
</syntaxhighlight>
 
=Migrations=
Django can generate scripts to recreate the database. We can manage this with
<syntaxhighlight lang="bash">
# Make
python manage.py makemigrations
# Show
python manage.py showmigrations
# Run
python manage.py migrate
</syntaxhighlight>
=Admin Site=
This allow you to
*Register Models
*UI
*Create Super User
==Create Super User==
We can create the superuser account in the root manage.py with
<syntaxhighlight lang="bash">
python3 manage.py createsuperuser
</syntaxhighlight>
==Register Models==
We can register the Models in admin.py with
<syntaxhighlight lang="python">
from .models import Game, Move
from django.contrib import admin
 
# Register your models here.
admin.site.register(Game)
admin.site.register(Move)
</syntaxhighlight>
</syntaxhighlight>
==User Interface==
Amending the admin site is well documented. I not sure they screens are great and wonder how manual entry is a good thing but I guess setting up test data would be easier. Here is an example.
Previously is was
[[File:Django admin1.png|600px]]
<br>
<br>
Create a requirements.txt document
By amending as follows
<syntaxhighlight lang="python">
from .models import Game, Move
from django.contrib import admin
 
# Register your models here.
 
# admin.site.register(Game)
@admin.register(Game)
class GameAdmin(admin.ModelAdmin):
    list_display = ('id', 'first_player', 'second_player', 'status')
 
admin.site.register(Move)
</syntaxhighlight>
We get
[[File:Django admin2.png|600px]]
=Templates=
So templates provide the HTML for the pages. I suspect that React or Angular will replace this going forward but here goes.
==Setup a new App==
===Create a new app===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
Django==3.1.5
python3 manage.py startapp player
djangorestframework==3.12.2
</syntaxhighlight>
mysqlclient==2.0.3
===Create a view===
django-mysql==3.10.0
<syntaxhighlight lang="python">
django-cors-headers==3.6.0
from django.shortcuts import render
pika==1.1.0
from django.http import HttpResponse
 
# Create your views here.
def player(request):
    return render(request, "player/player.html")
</syntaxhighlight>
 
===Add to Settings===
<syntaxhighlight lang="python">
# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
...
    'gameplay',
    'player',
]
</syntaxhighlight>
===Create a route===
In the base route
<syntaxhighlight lang="python">
...
urlpatterns = [
    path('admin/', admin.site.urls),
    path('welcome/', welcome),
    path('player/', include('player.urls')),
]
</syntaxhighlight>
Then create the apps urls.py
<syntaxhighlight lang="python">
from django.urls import path
 
from .views import player
 
urlpatterns = [
    path('', player),
]
</syntaxhighlight>
===Create the Template===
It is best practice to create the template in a directory templates/<app> because of naming clashing. In this case it is just html
<syntaxhighlight lang="html">
<HTML>
    <body>
        <p>Fred was ere</p>
    </body>
</HTML>
</syntaxhighlight>
==Example Template==
Like Angular we can use special tags for various data. The data is store in a context as a dicitionary.
<syntaxhighlight lang="python">
from django.shortcuts import render
from gameplay.models import Game
 
def home(request):
    my_games = Game.objects.games_for_user(request.user)
    active_games = my_games.active()
 
    return render(request, "player/home.html",
                  {'games': active_games})
</syntaxhighlight>
<br>
And here is the template to support this.
<syntaxhighlight lang="html+django">
{% extends "base.html" %}
 
{% block title %}
Home: {{ user.username }}
{% endblock %}
 
{% block content %}
    <h1>Welcome, {{ user.username }}</h1>
    These are your active games:
 
    <ul>
    {% for g in games %}
        <li>Game {{ g.id }}: {{ g.first_player }} vs {{ g.second_player }} </li>
    {% endfor %}
    </ul>
{% endblock %}
</syntaxhighlight>
==Static Files e.g. CSS, Fonts and Javascript==
===Introduction===
We can of course use static files. Like templates we place the static html in a special folder for best practice. We use static/<app> The example below should this and the special tag required for CSS.
<syntaxhighlight lang="html+django">
{% load staticfiles %}
<!DOCTYPE html>
<html lang="en">
<head>
    <link href="{% static 'player/style.css' %}" rel="stylesheet">
</head>
<body>
....
</body>
</html>
</syntaxhighlight>
===Using 3rd Party===
In the demos they went to http://www.initializr.com/ (no https ooo). They selected the bootstrap, removed options and downloaded the resulting zip file.
==Base Templates==
Like Angular the templates can be broken down into pieces. You may have noticed the words base.html in the example template. This is because we can derive our html from a base html template.
Things to make sure you do in the base template
*Add "load staticfiles" at the top to make sure the fonts, css etc are loaded
*Add the "static" around all of the css, fonts, etc.
*Add the {% block content %} and {% endblock %}
<syntaxhighlight lang="html+django">
{% load staticfiles %}
 
<!doctype html>
<html class="no-js" lang="">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title>{% block title %}{% endblock %}</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">
 
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
    <style>
        body {
            padding-top: 70px;
            padding-bottom: 20px;
        }
    </style>
    <link rel="stylesheet" href="{% static 'css/bootstrap-theme.min.css' %}">
    <link rel="stylesheet" href="{% static 'css/main.css' %}">
 
    <script src="{% static 'js/vendor/modernizr-2.8.3-respond-1.4.2.min.js' %}"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
                    aria-expanded="false" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Django Fundamentals</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
            <form class="navbar-form navbar-right" role="form">
                <div class="form-group">
                    <input type="text" placeholder="Email" class="form-control">
                </div>
                <div class="form-group">
                    <input type="password" placeholder="Password" class="form-control">
                </div>
                <button type="submit" class="btn btn-success">Sign in</button>
            </form>
        </div><!--/.navbar-collapse -->
    </div>
</nav>
 
<div class="container">
 
    {% block content %}
    {% endblock %}
 
</div> <!-- /container -->
 
 
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{% static 'js/vendor/jquery-1.11.2.min.js' %}"><\/script>')</script>
 
<script src="{% static 'js/vendor/bootstrap.min.js' %}"></script>
 
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>
 
</syntaxhighlight>
==Configuration==
In the example they put the templates and static files in the root of the project and not the root of the apps so the could be shared. To do this you do need to reconfigure settings.py from the default so django can find these by add DIRS and a new STATICFILES_DIRS definition
<syntaxhighlight lang="python">
...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
...
STATICFILES_DIRS = [
os.path.join(BASE_DIR,'templates')
]
...
</syntaxhighlight>
=Authentication=
==Restricting access==
In django the user is provided but there are also functions behind this which help us with authentication. i.e. is_authenticated or login_required. We can take advantage of this. Note we are referencing a named url and not the url itself.
<syntaxhighlight lang="python">
from django.shortcuts import render, redirect
def welcome(request):
    if request.user.is_authenticated:
        return redirect('player_home')
    else:
        return render(request, 'tictactoe/welcome.html')
</syntaxhighlight>
We can protect views using the login decorator too. e.g.
<syntaxhighlight lang="python">
@login_required()
def home(request):
...
</syntaxhighlight>
==Login and Logout==
This is not a tutorial but enough information to allow further reading.
===Configuration===
Within the settings we need to configure
<syntaxhighlight lang="python">
LOGOUT_REDIRECT_URL="tictactoe_welcome"
LOGIN_REDIRECT_URL="player_home"
LOGIN_URL="player_login"
</syntaxhighlight>
===Url Definition===
Because we are going to use classes for the urlpatterns we need to us myClass.as_view() to create the instance.
<syntaxhighlight lang="python">
...
urlpatterns = [
...
    url(r'home$', home, name="player_home"),
    url(r'login$',
        LoginView.as_view(template_name="player/login_form.html"),
        name="player_login"),
    url(r'logout$',
        LogoutView.as_view(),
        name="player_logout"),
...
]
</syntaxhighlight>
===Login Form Template===
Not much here accept perhaps the csrf token which stops a csrf attack.
<syntaxhighlight lang="html+django">
{% extends "base.html" %}
{% load crispy_forms_tags %}
 
{% block title %}
    Login
{% endblock %}
 
{% block content %}
 
    <form role="form"
          action="{% url 'player_login' %}"
          method="post">
        {% csrf_token %}
 
        <p>Please login.</p>
 
        {{ form|crispy }}
 
        <button type="submit" class="btn btn-success">Sign in</button>
    </form>
{% endblock %}
</syntaxhighlight>
===Logging Outt===
In the base template we use user.is_authenticated and then call the logout function
<syntaxhighlight lang="html+django">
...
        <ul class="nav navbar-nav navbar-right">
            {% if user.is_authenticated %}
                <li><a href="{% url 'player_logout' %}">Logout</a></li>
            {% else %}
                <li><a href="{% url 'player_login' %}">Login</a></li>
            {% endif %}
        </ul>
</syntaxhighlight>
=Forms=
==Introduction==
Django can generate forms based on model data. We just need to provide the model. We can exclude fields based on their name.
<syntaxhighlight lang="python">
from django.forms import ModelForm
from .models import Invitation
class InvitationForm(ModelForm):
    class Meta:
        model = Invitation
        exclude = ('from_user', 'timestamp')
</syntaxhighlight>
==Managing the form lifecycle in the View==
Here is an example of using the form we test for POST, ie first time through and render a form with no arguments. On POST we check the form is valid and save, redirecting to the home page, if invalid we re-show the form
<syntaxhighlight lang="python">
@login_required()
def new_invitation(request):
    if request.method == "POST":
        invitation = Invitation(from_user=request.user)
        form = InvitationForm(instance=invitation, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('player_home')
    else:
        form = InvitationForm()
    return render(request, "player/new_invitation_form.html", {'form': form})
</syntaxhighlight>
==And the Template==
We just need to make sure we have the csrf token and the action to take on submit.
<syntaxhighlight lang="html+django">
{% extends 'base.html' %}
{% load crispy_forms_tags %}
 
{% block title %}
New Invitation
{% endblock %}
 
{% block content %}
    <form method="post" action="{% url 'player_new_invitation' %}">
        {{ form | crispy }}
 
        {% csrf_token %}
        <button type="submit">Send the invitation</button>
    </form>
{% endblock %}
</syntaxhighlight>
==Another Form Example==
Here is another example as this is not a tutorial. This also demonstrates a few other things.
===View===
This shows the function '''get_object_or_404''' which does what it says. It also shows the same form processing as before.
<syntaxhighlight lang="python">
@login_required()
def accept_invitation(request, id):
    invitation = get_object_or_404(Invitation, pk=id)
    if not request.user == invitation.to_user:
        raise PermissionDenied
    if request.method == 'POST':
        if "accept" in request.POST:
            game = Game.objects.create(
                first_player=invitation.to_user,
                second_player=invitation.from_user,
            )
        invitation.delete()
        return redirect('player_home')
    else:
        return render(request,
                      "player/accept_invitation_form.html",
                      {'invitation': invitation}
                      )
 
</syntaxhighlight>
</syntaxhighlight>


=Django=
===Form Template===
This is the supporting form which just has two buttons.
<syntaxhighlight lang="html+django">
{% extends "base.html" %}
 
{% block title %}
Accept Invitation
{% endblock title %}


==Create a Django Project==
{% block content %}
Follow steps on https://www.django-rest-framework.org/tutorial/quickstart/#project-setup
    <div class="well col-md-6">
        <p>User {{ invitation.from_user }} invites you to a game.
            He/she included the following message:</p>


=Create an App=
        <blockquote> <p> {{ invitation.message }} </p></blockquote>
        <form action="" method="post">
            {% csrf_token %}
            <button type="submit" name="accept" value="ok">Accept</button>
            <button type="submit" name="deny" value="no">Deny</button>
        </form>
    </div>
{% endblock content %}
</syntaxhighlight>
===Url===
In the accept invitations url we see the id passed to the class.
<syntaxhighlight lang="python">
urlpatterns = [
...
    url(r'accept_invitation/(?P<id>\d+)/$',
        accept_invitation,
        name="player_accept_invitation")
]
</syntaxhighlight>
==Generic Views==
Django provides a list of view including<br>
<br>
For Display
*TemplateView
*DetailView
*ListView
<br>
For Editing
*CreateView
*UpdateView
*DeleteView
 
=Installing Rabbit MQ=
==Installing Erlang==
Here is how to do it on 20.04
<syntaxhighlight lang="bash">
echo "deb https://packages.erlang-solutions.com/ubuntu focal contrib" | sudo tee /etc/apt/sources.list.d/rabbitmq.list
sudo apt update
sudo apt install erlang
</syntaxhighlight>
If we get and error with the keys then, if you are sure, you can add the keys.
<syntaxhighlight lang="bash">
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys XXXXX
</syntaxhighlight>
==Install RabbitMQ Repository==
<syntaxhighlight lang="bash">
sudo apt install apt-transport-https -y
wget -O- https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc | sudo apt-key add -
wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add -
echo "deb https://dl.bintray.com/rabbitmq-erlang/debian focal erlang-22.x" | sudo tee /etc/apt/sources.list.d/rabbitmq.list
sudo apt update
sudo apt install rabbitmq-server
systemctl status  rabbitmq-server.service
systemctl is-enabled rabbitmq-server.service
</syntaxhighlight>
==Install RabbitMQ Management==
<syntaxhighlight lang="bash">
sudo rabbitmq-plugins enable rabbitmq_management
# ss -tunelp | grep 15672
tcp  LISTEN  0      128                    0.0.0.0:15672 
sudo ufw allow proto tcp from any to any port 5672,15672
</syntaxhighlight>
Now create a user
<syntaxhighlight lang="bash">
rabbitmqctl add_user admin StrongPassword
rabbitmqctl set_user_tags admin administrator
</syntaxhighlight>
We can see the interface at
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
python manage.py startapp products
http://server:15672
</syntaxhighlight>
</syntaxhighlight>
=Amend the Settings on the container=
All of this was taken from the website https://computingforgeeks.com/how-to-install-latest-rabbitmq-server-on-ubuntu-linux/
We need to modify the following sections
 
=Django=
On the container you need to define the app
==Setup==
===Docker===
For the Docker Compose refer to the [[Docker]] page.
===Django App Settings===
We need to modify the following sections of settings.py the settings file for django
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
INSTALLED_APPS = [
INSTALLED_APPS = [
Line 104: Line 717:
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_ALL_ORIGINS = True
</syntaxhighlight>
</syntaxhighlight>
 
==Define the Website==
=Create Model Classes=
===Create Model Classes===
Edit the model classes under the Product app
Edit the model classes under the Product app
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
Line 119: Line 732:
     pass
     pass
</syntaxhighlight>
</syntaxhighlight>
=Run the Migrations=
 
===Run the Migrations===
This will create the tables. Log back into the container and type
This will create the tables. Log back into the container and type
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
Line 125: Line 739:
python manage.py migrate
python manage.py migrate
</syntaxhighlight>
</syntaxhighlight>
=Make Serializers=
 
===Make Serializers===
We make serializer for the product table with
We make serializer for the product table with
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
Line 137: Line 752:
         fields = '__all__'
         fields = '__all__'
</syntaxhighlight>
</syntaxhighlight>
=Make Url (Routing)=
===Make Url (Routing)===
In the main routing add the api route with an include
In the main routing add the api route with an include
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
Line 166: Line 781:
</syntaxhighlight>
</syntaxhighlight>


=Make Views (Repository or Data Layer)=
===Make Views (Repository or Data Layer)===
This is like the repository layer and provides the CRUD functions to support the routing.
This is like the repository layer and provides the CRUD functions to support the routing.
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
Line 208: Line 823:
         product.delete()
         product.delete()
         return Response(status=status.HTTP_204_NO_CONTENT)
         return Response(status=status.HTTP_204_NO_CONTENT)
class UserAPIView(APIView):
    def get(self, _):
        users = User.objects.all()
        user = random.choice(users)
        return Response({
            'id': user.id
        })       
</syntaxhighlight>
=Flask=
==Setup==
===Requirement===
<syntaxhighlight lang="bash">
Flask==1.1.2
Flask-SQLAlchemy==2.4.4
SQLAlchemy==1.3.20
Flask-Migrate==2.5.3
Flask-Script==2.0.6
Flask-Cors==3.0.9
requests==2.25.0
mysqlclient==2.0.3
pika==1.1.0
</syntaxhighlight>
===Flask Docker File Specific===
For Flask this is the start command in the container
<syntaxhighlight lang="Dockerfile">
CMD python main.py
</syntaxhighlight>
===Flask Docker Composer Specific===
In the composer file we need to make sure the port Flask uses is unique e.g.
<syntaxhighlight lang="Dockerfile">
services:
  backend:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8001:5000
    volumes:
      - .:/app
    depends_on:
      - db
</syntaxhighlight>
===Create App===
Create an empty app
<syntaxhighlight lang="python">
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
    return 'Hello'
if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')
</syntaxhighlight>
==Define App==
===Create the Model Classes===
Here we define the model
<syntaxhighlight lang="python">
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import UniqueConstraint
from flask_cors import CORS
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:root@db/main'
CORS(app)
db = SQLAlchemy(app)
class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    title = db.Column(db.String(200))
    image = db.Column(db.String(200))
class ProductUser(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer)
    product_id = db.Column(db.Integer)
    UniqueConstraint('user_id', 'product_id', name='user_product_unique')
@app.route('/')
def index():
    return 'Hello'
if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')
</syntaxhighlight>
===Run the Migrations===
We create the tables by creating a script e.g. manager.py
<syntaxhighlight lang="python">
from main import app, db
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
    manager.run()
</syntaxhighlight>
And run the script using the script
<syntaxhighlight lang="bash">
python manager.py db init
python manager.py db migrate
python manager.py db upgrade
</syntaxhighlight>
=Rabbit MQ=
==Example Consumer==
<syntaxhighlight lang="Dockerfile">
import pika
params = pika.URLParameters('amqp://test123:PASS@heaven.com:5672/%2f')
connection = pika.BlockingConnection(params)
channel = connection.channel()
channel.queue_declare(queue='admin')
def callback(ch, method, properties, body):
    print('Received in admin')
    print(body)
channel.basic_consume(queue='admin', on_message_callback=callback, auto_ack=True)
print('Started consuming')
channel.start_consuming()
channel.close()
</syntaxhighlight>
==Example Producer==
And here is a producer
<syntaxhighlight lang="Dockerfile">
import pika
params = pika.URLParameters('amqp://test:test@192.168.1.70:5672/%2f')
connection = pika.BlockingConnection(params)
channel = connection.channel()
def publish():
    pass
    channel.basic_publish(exchange='',routing_key='admin',body='hello')
</syntaxhighlight>
==Adding Queue to Container==
Simply add another service to the Django container.
<syntaxhighlight lang="Dockerfile">
  queue:
    build:
      context: .
      dockerfile: Dockerfile
    command: 'python consumer.py'
    depends_on:
      - db
</syntaxhighlight>
==Application Events==
===Publishing Event===
Next we can change the events to publish. First we change the publish function in the producer to add the method and the body to message using json.
<syntaxhighlight lang="python">
def publish(method, body):
    print('Sending from django to flask')
    properties = pika.BasicProperties(method)
    channel.basic_publish(
        exchange='',
        routing_key='main',
        body=json.dumps(body),
        properties=properties)
</syntaxhighlight>
We now need to go to the methods to produce these events.
<syntaxhighlight lang="python">
    def create(self, request): # /api/products
...
        publish('product_created',serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED)
...
    def update(self, request, pk=None): # /api/products/<str:id>
...
        publish('product_updated',serializer.data)
        return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
    def destroy(self, request, pk=None): # /api/products/<str:id>
...
        publish('product_deleted', pk)
        return Response(status=status.HTTP_204_NO_CONTENT)
</syntaxhighlight>
===Consuming Events===
In the flask app we now need to act on the messages being sent. There is one action for each of the CRUD operations.
<syntaxhighlight lang="python">
import pika, json
from main import Product, db
params = pika.URLParameters('amqp://test:test@192.168.1.70:5672/%2f')
connection = pika.BlockingConnection(params)
channel = connection.channel()
channel.queue_declare(queue='main')
def callback(ch, method, properties, body):
    print('Received in admin')
    data=json.loads(body)
    print(body)
    if properties.content_type == 'product_created':
        product = Product(id=data['id'], title=data['title'], image=data['image'])
        db.session.add(product)
        db.session.commit()
        print('product created')
    elif properties.content_type == 'product_updated':
        product = Product.query.get(data['id'])
        product.title = data['title']
        product.image = data['image']
        db.session.commit()
        print('product updated')
    elif properties.content_type == 'product_deleted':
        product = Product.query.get(data)
        db.session.delete(product)
        db.session.commit()
        print('product deleted')
channel.basic_consume(queue='main', on_message_callback=callback, auto_ack=True)
print('Started consuming')
channel.start_consuming()
channel.close()
</syntaxhighlight>
=Implementing the Likes=
==Introduction==
Within the app we are going to
*allow flask to serialize json for tables (Flask)
*post a like for a product id in flask
*call the django app for a random user, 
*notify the django app of the like for the random user
*update Django with likes
==Allow Flask to Serialize Json for Tables (Flask)==
We need to add the dataclass decorator and add the types to the class and set the end points
<syntaxhighlight lang="python">
from flask import Flask, jsonify
from dataclasses import dataclass
...
@dataclass
class Product(db.Model):
    id: int
    title: str
    image: str
    id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    title = db.Column(db.String(200))
    image = db.Column(db.String(200))
....
@app.route('/api/products')
def index():
    return jsonify(Product.query.all())
</syntaxhighlight>
==Post a Like for a Product in Flask==
We will receive the post, get a random use from the django app. We send a request to the django app to get a random user. We then write a record in the Product User in Flask app and publish the result to Rabbit for the Django app to receive.
<syntaxhighlight lang="python">
@app.route('/api/products/<int:id>/like', methods=['POST'])
def like(id):
    httpclient_logging_patch()
    try:
        req = requests.get('http://djangoapp:8000/api/user')
        json = req.json()
        app.logger.info("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ")
        app.logger.info(json)
        productUser = ProductUser(user_id=json['id'], product_id=id)
        db.session.add(productUser)
        db.session.commit()
   
        publish('product liked', id)
    except:
        abort(400, 'You already liked this product')
   
    return jsonify({
        'message' : 'success'
    })
</syntaxhighlight>
==Notify the Django App of the Like for Random User==
In the like function we call publish. This sends a message to rabbit queue for the Django app to consume
<syntaxhighlight lang="python">
import pika, json
params = pika.URLParameters('amqp://test:test@192.168.1.70:5672/%2f')
connection = pika.BlockingConnection(params)
channel = connection.channel()
def publish(method, body):
    print('Sending from django to django')
    properties = pika.BasicProperties(method)
    channel.basic_publish(
        exchange='',
        routing_key='admin',
        body=json.dumps(body),
        properties=properties)
</syntaxhighlight>
==Update Django likes==
Once we receive the message from Rabbit MQ in Django we increase the likes
<syntaxhighlight lang="python">
# Require because consumer runs outside of the app
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "admin.settings")
import django
django.setup()
# End of Require because consumer runs outside of the app
def callback(ch, method, properties, body):
    id = json.loads(body)
    product = Product.objects.get(id=id)
    product.likes = product.likes + 1
    product.save()
    print('products were increased')
channel.basic_consume(queue='admin', on_message_callback=callback, auto_ack=True)
channel.start_consuming()
print('Started consuming')
channel.close()
</syntaxhighlight>
=Useful Commands=
==Login to Container==
Login to the container with
<syntaxhighlight lang="bash">
docker-compose exec backend bash
</syntaxhighlight>
==Build Container==
Login to the container with
<syntaxhighlight lang="bash">
docker-compose up --build
</syntaxhighlight>
</syntaxhighlight>

Latest revision as of 14:21, 10 January 2021

Django

Introduction

Named after Django Reinhardt. It is a Python web framework. Packages to support Django can be found at http://awesom-django.com It includes

  • ORM (Object mapping to DB)
  • URL Mapping
  • Templates
  • Forms
  • Admin
  • Package

Some of the design principles can be found at http://goo.gl/PRrEMe. Django using an Model Template View approach which is like MVC Where the template is the view and the view is a controller.

Installation And Create am App

We can create a python environment and install with. Using and environment keeps the packages local and not available at a user level or global level

python3 -m venv django-env
. django-env/bin/activate
pip install django

We can set the environment in VS Code with
Within Python we can now create our project

django-admin startproject myapp


Key Files created are

  • manage.py (Management)
  • settings.py (Settings)
  • urls.py (Routing)
  • wsgi.py (Packaging)

We can run the development server with

python3 manage.py runserver

Creating a View

We can create view by creating a view and add a route.

from django.http import HttpResponse

def welcome(request):
    return HttpResponse("Hello World")

And adding the route to urls.py

from .views import welcome
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('welcome/', welcome),
]

Not you can use regex for urls with re_path e.g.

    re_path(r'welcome/', welcome),

Creating Apps

Within in the directory we can divide the app into apps. So go into the directory containing manage.py

python3 manage.py startapp gameplay

Within the settings file we need to add the new app, in this case gameplay.

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
...
    'gameplay'
]

Models

Introduction

We Create models using the django models class. You can see we can create foreign keys and cascade deletes which are all documented.

from django.db import models
from django.contrib.auth.models import User

class Game(models.Model):
    first_player = models.ForeignKey(
        User,
        related_name="games_first_player", 
        on_delete=models.CASCADE,)

    second_player = models.ForeignKey(
        User,
        related_name="games_second_player",
        on_delete=models.CASCADE,)

    start_time = models.DateTimeField(auto_now_add=True)
    last_active = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=1, default='F')


class Move(models.Model):
    x = models.IntegerField()
    y = models.IntegerField()
    comment = models.CharField(max_length=300, blank=True)
    by_first_player = models.BooleanField()

    game = models.ForeignKey(Game, on_delete=models.CASCADE)

Field Types

Options

  • Make Field Nullable (Default is non Null)

Model.IntegerField(null = True)

  • Allow Empty Values in Forms (Not db-related)

Model.CharField(blank = True)

  • Default Value

Model.IntegerField(default = 'F')

  • Typed-specific options

Model.DateTimeField(auto_now = 'True')

Here is a list of some of the fields provided.

Field Name Description
AutoField It An IntegerField that automatically increments.
BigAutoFieldIt is a 64-bit integer, much like an AutoField except that it is guaranteed to fit numbers from 1 to 9223372036854775807.
BigIntegerFieldIt is a 64-bit integer, much like an IntegerField except that it is guaranteed to fit numbers from -9223372036854775808 to 9223372036854775807.
BinaryField A field to store raw binary data.
BooleanField A true/false field. The default form widget for this field is a CheckboxInput.
CharField It is a date, represented in Python by a datetime.date instance.
DateField A date, represented in Python by a datetime.date instance. It is used for date and time, represented in Python by a datetime.datetime instance.
DecimalField It is a fixed-precision decimal number, represented in Python by a Decimal instance.
DurationField A field for storing periods of time.
EmailField It is a CharField that checks that the value is a valid email address.
FileField It is a file-upload field.
FloatField It is a floating-point number represented in Python by a float instance.
ImageField It inherits all attributes and methods from FileField, but also validates that the uploaded object is a valid image.
IntegerField It is an integer field. Values from -2147483648 to 2147483647 are safe in all |databases supported by Django.
GenericIPAddressField An IPv4 or IPv6 address, in string format (e.g. 192.0.2.30 or 2a02:42fe::4).
NullBooleanField Like a BooleanField, but allows NULL as one of the options.
PositiveIntegerField Like an IntegerField, but must be either positive or zero (0).
PositiveSmallIntegerField Like a PositiveIntegerField, but only allows values under a certain (database-dependent) point.
SlugField Slug is a newspaper term. A slug is a short label for something, containing only letters, numbers, underscores or hyphens. They’re generally used in URLs.
SmallIntegerField It is like an IntegerField, but only allows values under a certain (database-dependent) point.
TextField A large text field. The default form widget for this field is a Textarea.
TimeField A time, represented in Python by a datetime.time instance.
URLField A CharField for a URL, validated by URLValidator.
UUIDField A field for storing universally unique identifiers. Uses Python’s UUID class. When used on PostgreSQL, this stores in a uuid datatype, otherwise in a char(32).

Model API (Repository)

There are lots of operation you can do on Models. Here are some common ones

# Must return 1
Game.objects.get(pk=5)
# Returns all rows
Game.objects.all()
# Returns matching objects
Game.objects.filter(status = 'A')

Model Query Sets

Our models inherit from models.Mdoel however there is a models.QuerySet which allows queries across models. Within the models we can construct our own views of the data. In the code we want to get all of the games for a user. Previously the code was written in the view to query the model twice. Once for the games as first player and once for the games as second player.

def home(request)
    games_first_player = Game.objects.filter(
        first_player=request.user
        status='F'
    )
    games_second_player = Game.objects.filter(
        second_player=request.user
        status='S'
    )
    all_my_games = list(games_first_player) + \
                   list(games_second_player)
...

If we use the Q function, which allows us to use an or '|' within the query we can get this data with only one query. We then override the objects in the model class to use the GamesQuerySet

...
from django.db.models import Q
...
class GamesQuerySet(models.QuerySet):
    def games_for_user(self, user):
        return self.filter(
            Q(first_player=user) | Q(second_player=user)
        )

class Game(models.Model):
...
    objects = GamesQuerySet.as_manager()

    def __str__(self):
        return "{0} vs {1}".format(
            self.first_player, self.second_player)

Migrations

Django can generate scripts to recreate the database. We can manage this with

# Make
python manage.py makemigrations
# Show
python manage.py showmigrations
# Run
python manage.py migrate

Admin Site

This allow you to

  • Register Models
  • UI
  • Create Super User

Create Super User

We can create the superuser account in the root manage.py with

python3 manage.py createsuperuser

Register Models

We can register the Models in admin.py with

from .models import Game, Move
from django.contrib import admin

# Register your models here.
admin.site.register(Game)
admin.site.register(Move)

User Interface

Amending the admin site is well documented. I not sure they screens are great and wonder how manual entry is a good thing but I guess setting up test data would be easier. Here is an example. Previously is was
By amending as follows

from .models import Game, Move
from django.contrib import admin

# Register your models here.

# admin.site.register(Game)
@admin.register(Game)
class GameAdmin(admin.ModelAdmin):
    list_display = ('id', 'first_player', 'second_player', 'status')

admin.site.register(Move)

We get

Templates

So templates provide the HTML for the pages. I suspect that React or Angular will replace this going forward but here goes.

Setup a new App

Create a new app

python3 manage.py startapp player

Create a view

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.
def player(request):
    return render(request, "player/player.html")

Add to Settings

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
...
    'gameplay',
    'player',
]

Create a route

In the base route

...
urlpatterns = [
    path('admin/', admin.site.urls),
    path('welcome/', welcome),
    path('player/', include('player.urls')),
]

Then create the apps urls.py

from django.urls import path

from .views import player

urlpatterns = [
    path('', player),
]

Create the Template

It is best practice to create the template in a directory templates/<app> because of naming clashing. In this case it is just html

<HTML>
    <body>
        <p>Fred was ere</p>
    </body>
</HTML>

Example Template

Like Angular we can use special tags for various data. The data is store in a context as a dicitionary.

from django.shortcuts import render
from gameplay.models import Game

def home(request):
    my_games = Game.objects.games_for_user(request.user)
    active_games = my_games.active()

    return render(request, "player/home.html",
                  {'games': active_games})


And here is the template to support this.

{% extends "base.html" %}

{% block title %}
Home: {{ user.username }}
{% endblock %}

{% block content %}
    <h1>Welcome, {{ user.username }}</h1>
    These are your active games:

    <ul>
    {% for g in games %}
        <li>Game {{ g.id }}: {{ g.first_player }} vs {{ g.second_player }} </li>
    {% endfor %}
    </ul>
{% endblock %}

Static Files e.g. CSS, Fonts and Javascript

Introduction

We can of course use static files. Like templates we place the static html in a special folder for best practice. We use static/<app> The example below should this and the special tag required for CSS.

{% load staticfiles %}
<!DOCTYPE html>
<html lang="en">
<head>
    <link href="{% static 'player/style.css' %}" rel="stylesheet">
</head>
<body>
....
</body>
</html>

Using 3rd Party

In the demos they went to http://www.initializr.com/ (no https ooo). They selected the bootstrap, removed options and downloaded the resulting zip file.

Base Templates

Like Angular the templates can be broken down into pieces. You may have noticed the words base.html in the example template. This is because we can derive our html from a base html template. Things to make sure you do in the base template

  • Add "load staticfiles" at the top to make sure the fonts, css etc are loaded
  • Add the "static" around all of the css, fonts, etc.
  • Add the {% block content %} and {% endblock %}
{% load staticfiles %}

<!doctype html>
<html class="no-js" lang="">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title>{% block title %}{% endblock %}</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
    <style>
        body {
            padding-top: 70px;
            padding-bottom: 20px;
        }
    </style>
    <link rel="stylesheet" href="{% static 'css/bootstrap-theme.min.css' %}">
    <link rel="stylesheet" href="{% static 'css/main.css' %}">

    <script src="{% static 'js/vendor/modernizr-2.8.3-respond-1.4.2.min.js' %}"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
                    aria-expanded="false" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Django Fundamentals</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
            <form class="navbar-form navbar-right" role="form">
                <div class="form-group">
                    <input type="text" placeholder="Email" class="form-control">
                </div>
                <div class="form-group">
                    <input type="password" placeholder="Password" class="form-control">
                </div>
                <button type="submit" class="btn btn-success">Sign in</button>
            </form>
        </div><!--/.navbar-collapse -->
    </div>
</nav>

<div class="container">

    {% block content %}
    {% endblock %}

</div> <!-- /container -->


<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{% static 'js/vendor/jquery-1.11.2.min.js' %}"><\/script>')</script>

<script src="{% static 'js/vendor/bootstrap.min.js' %}"></script>

<script src="{% static 'js/main.js' %}"></script>
</body>
</html>

Configuration

In the example they put the templates and static files in the root of the project and not the root of the apps so the could be shared. To do this you do need to reconfigure settings.py from the default so django can find these by add DIRS and a new STATICFILES_DIRS definition

...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
...
STATICFILES_DIRS = [
os.path.join(BASE_DIR,'templates')
]
...

Authentication

Restricting access

In django the user is provided but there are also functions behind this which help us with authentication. i.e. is_authenticated or login_required. We can take advantage of this. Note we are referencing a named url and not the url itself.

from django.shortcuts import render, redirect
def welcome(request):
    if request.user.is_authenticated:
        return redirect('player_home')
    else:
        return render(request, 'tictactoe/welcome.html')

We can protect views using the login decorator too. e.g.

@login_required()
def home(request):
...

Login and Logout

This is not a tutorial but enough information to allow further reading.

Configuration

Within the settings we need to configure

LOGOUT_REDIRECT_URL="tictactoe_welcome"
LOGIN_REDIRECT_URL="player_home"
LOGIN_URL="player_login"

Url Definition

Because we are going to use classes for the urlpatterns we need to us myClass.as_view() to create the instance.

...
urlpatterns = [
...
    url(r'home$', home, name="player_home"),
    url(r'login$',
        LoginView.as_view(template_name="player/login_form.html"),
        name="player_login"),
    url(r'logout$',
        LogoutView.as_view(),
        name="player_logout"),
...
]

Login Form Template

Not much here accept perhaps the csrf token which stops a csrf attack.

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}
    Login
{% endblock %}

{% block content %}

    <form role="form"
          action="{% url 'player_login' %}"
          method="post">
        {% csrf_token %}

        <p>Please login.</p>

        {{ form|crispy }}

        <button type="submit" class="btn btn-success">Sign in</button>
    </form>
{% endblock %}

Logging Outt

In the base template we use user.is_authenticated and then call the logout function

...
        <ul class="nav navbar-nav navbar-right">
            {% if user.is_authenticated %}
                <li><a href="{% url 'player_logout' %}">Logout</a></li>
            {% else %}
                <li><a href="{% url 'player_login' %}">Login</a></li>
            {% endif %}
        </ul>

Forms

Introduction

Django can generate forms based on model data. We just need to provide the model. We can exclude fields based on their name.

from django.forms import ModelForm
from .models import Invitation
class InvitationForm(ModelForm):
    class Meta:
        model = Invitation
        exclude = ('from_user', 'timestamp')

Managing the form lifecycle in the View

Here is an example of using the form we test for POST, ie first time through and render a form with no arguments. On POST we check the form is valid and save, redirecting to the home page, if invalid we re-show the form

@login_required()
def new_invitation(request):
    if request.method == "POST":
        invitation = Invitation(from_user=request.user)
        form = InvitationForm(instance=invitation, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('player_home')
    else:
        form = InvitationForm()
    return render(request, "player/new_invitation_form.html", {'form': form})

And the Template

We just need to make sure we have the csrf token and the action to take on submit.

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}
New Invitation
{% endblock %}

{% block content %}
    <form method="post" action="{% url 'player_new_invitation' %}">
        {{ form | crispy }}

        {% csrf_token %}
        <button type="submit">Send the invitation</button>
    </form>
{% endblock %}

Another Form Example

Here is another example as this is not a tutorial. This also demonstrates a few other things.

View

This shows the function get_object_or_404 which does what it says. It also shows the same form processing as before.

@login_required()
def accept_invitation(request, id):
    invitation = get_object_or_404(Invitation, pk=id)
    if not request.user == invitation.to_user:
        raise PermissionDenied
    if request.method == 'POST':
        if "accept" in request.POST:
            game = Game.objects.create(
                first_player=invitation.to_user,
                second_player=invitation.from_user,
            )
        invitation.delete()
        return redirect('player_home')
    else:
        return render(request,
                      "player/accept_invitation_form.html",
                      {'invitation': invitation}
                      )

Form Template

This is the supporting form which just has two buttons.

{% extends "base.html" %}

{% block title %}
Accept Invitation
{% endblock title %}

{% block content %}
    <div class="well col-md-6">
        <p>User {{ invitation.from_user }} invites you to a game.
            He/she included the following message:</p>

        <blockquote> <p> {{ invitation.message }} </p></blockquote>
        <form action="" method="post">
            {% csrf_token %}
            <button type="submit" name="accept" value="ok">Accept</button>
            <button type="submit" name="deny" value="no">Deny</button>
        </form>
    </div>
{% endblock content %}

Url

In the accept invitations url we see the id passed to the class.

urlpatterns = [
...
    url(r'accept_invitation/(?P<id>\d+)/$',
        accept_invitation,
        name="player_accept_invitation")
]

Generic Views

Django provides a list of view including

For Display

  • TemplateView
  • DetailView
  • ListView


For Editing

  • CreateView
  • UpdateView
  • DeleteView

Installing Rabbit MQ

Installing Erlang

Here is how to do it on 20.04

echo "deb https://packages.erlang-solutions.com/ubuntu focal contrib" | sudo tee /etc/apt/sources.list.d/rabbitmq.list
sudo apt update
sudo apt install erlang

If we get and error with the keys then, if you are sure, you can add the keys.

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys XXXXX

Install RabbitMQ Repository

sudo apt install apt-transport-https -y
wget -O- https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc | sudo apt-key add -
wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add -
echo "deb https://dl.bintray.com/rabbitmq-erlang/debian focal erlang-22.x" | sudo tee /etc/apt/sources.list.d/rabbitmq.list
sudo apt update
sudo apt install rabbitmq-server
systemctl status  rabbitmq-server.service 
systemctl is-enabled rabbitmq-server.service

Install RabbitMQ Management

sudo rabbitmq-plugins enable rabbitmq_management
# ss -tunelp | grep 15672
tcp   LISTEN  0       128                    0.0.0.0:15672  
sudo ufw allow proto tcp from any to any port 5672,15672

Now create a user

rabbitmqctl add_user admin StrongPassword
rabbitmqctl set_user_tags admin administrator

We can see the interface at

http://server:15672

All of this was taken from the website https://computingforgeeks.com/how-to-install-latest-rabbitmq-server-on-ubuntu-linux/

Django

On the container you need to define the app

Setup

Docker

For the Docker Compose refer to the Docker page.

Django App Settings

We need to modify the following sections of settings.py the settings file for django

INSTALLED_APPS = [
...
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'products'
]

MIDDLEWARE = [
...
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
...
]
...

# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'admin',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'db',
        'PORT': '3306'

    }
}
...
CORS_ALLOW_ALL_ORIGINS = True

Define the Website

Create Model Classes

Edit the model classes under the Product app

from django.db import models

# Create your models here.
class Product(models.Model):
    title = models.CharField(max_length=200)
    image = models.CharField(max_length=200)
    likes = models.PositiveIntegerField(default=0)

class User(models.Model):
    pass

Run the Migrations

This will create the tables. Log back into the container and type

python manage.py makemigrations
python manage.py migrate

Make Serializers

We make serializer for the product table with

from django.db import models
from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = '__all__'

Make Url (Routing)

In the main routing add the api route with an include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('products.urls'))
]

In the app we can now add routes for the REST end points. Which are just maps to the views.

from django.urls import path

from .views import ProductViewSet

urlpatterns = [
    path('products', ProductViewSet.as_view({
        'get' : 'list',
        'post': 'create'
    })),
    path( 'products/<int:pk>', ProductViewSet.as_view({
        'get' : 'retreive',
        'put': 'update',
        'delete': 'destroy'
    }))
]

Make Views (Repository or Data Layer)

This is like the repository layer and provides the CRUD functions to support the routing.

from django.shortcuts import render

# Create your views here.
from rest_framework import viewsets, status
from rest_framework.response import Response
from rest_framework.serializers import Serializer

from .models import Product
from .serializer import ProductSerializer

class ProductViewSet(viewsets.ViewSet):

    def list(self, request): # /api/products/<str:id>
        products = Product.objects.all()
        serializer = ProductSerializer(products, many=True)
        return Response(serializer.data)

    def create(self, request): # /api/products
        serializer = ProductSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)

    def retreive(self, request, pk=None): # /api/products/<str:id>
        product = Product.objects.get(id=pk)
        serializer =ProductSerializer(product)
        return Response(serializer.data)

    def update(self, request, pk=None): # /api/products/<str:id>
        product = Product.objects.get(id=pk)
        serializer =ProductSerializer(instance=product, data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data, status=status.HTTP_202_ACCEPTED)

    def destroy(self, request, pk=None): # /api/products/<str:id>
        product = Product.objects.get(id=pk)
        product.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

class UserAPIView(APIView):
    def get(self, _):
        users = User.objects.all()
        user = random.choice(users)
        return Response({
            'id': user.id
        })

Flask

Setup

Requirement

Flask==1.1.2
Flask-SQLAlchemy==2.4.4
SQLAlchemy==1.3.20
Flask-Migrate==2.5.3
Flask-Script==2.0.6
Flask-Cors==3.0.9
requests==2.25.0
mysqlclient==2.0.3
pika==1.1.0

Flask Docker File Specific

For Flask this is the start command in the container

CMD python main.py

Flask Docker Composer Specific

In the composer file we need to make sure the port Flask uses is unique e.g.

services: 
  backend:
    build: 
      context: .
      dockerfile: Dockerfile
    ports: 
      - 8001:5000
    volumes: 
      - .:/app
    depends_on:
      - db

Create App

Create an empty app

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello'

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')

Define App

Create the Model Classes

Here we define the model

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import UniqueConstraint

from flask_cors import CORS

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:root@db/main'
CORS(app)

db = SQLAlchemy(app)

class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    title = db.Column(db.String(200))
    image = db.Column(db.String(200))


class ProductUser(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer)
    product_id = db.Column(db.Integer)

    UniqueConstraint('user_id', 'product_id', name='user_product_unique')

@app.route('/')
def index():
    return 'Hello'

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')

Run the Migrations

We create the tables by creating a script e.g. manager.py

from main import app, db
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager

migrate = Migrate(app, db)

manager = Manager(app)
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
    manager.run()

And run the script using the script

python manager.py db init 
python manager.py db migrate
python manager.py db upgrade

Rabbit MQ

Example Consumer

import pika

params = pika.URLParameters('amqp://test123:PASS@heaven.com:5672/%2f')

connection = pika.BlockingConnection(params)

channel = connection.channel()

channel.queue_declare(queue='admin')

def callback(ch, method, properties, body):
    print('Received in admin')
    print(body)

channel.basic_consume(queue='admin', on_message_callback=callback, auto_ack=True)

print('Started consuming')

channel.start_consuming()

channel.close()

Example Producer

And here is a producer

import pika

params = pika.URLParameters('amqp://test:test@192.168.1.70:5672/%2f')

connection = pika.BlockingConnection(params)

channel = connection.channel() 

def publish():
    pass
    channel.basic_publish(exchange='',routing_key='admin',body='hello')

Adding Queue to Container

Simply add another service to the Django container.

  queue:
    build: 
      context: .
      dockerfile: Dockerfile
    command: 'python consumer.py'
    depends_on:
      - db

Application Events

Publishing Event

Next we can change the events to publish. First we change the publish function in the producer to add the method and the body to message using json.

def publish(method, body):
    print('Sending from django to flask')
    properties = pika.BasicProperties(method)
    channel.basic_publish(
        exchange='',
        routing_key='main',
        body=json.dumps(body), 
        properties=properties)

We now need to go to the methods to produce these events.

    def create(self, request): # /api/products
...
        publish('product_created',serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED)
...
    def update(self, request, pk=None): # /api/products/<str:id>
...
        publish('product_updated',serializer.data)
        return Response(serializer.data, status=status.HTTP_202_ACCEPTED)

    def destroy(self, request, pk=None): # /api/products/<str:id>
...
        publish('product_deleted', pk)
        return Response(status=status.HTTP_204_NO_CONTENT)

Consuming Events

In the flask app we now need to act on the messages being sent. There is one action for each of the CRUD operations.

import pika, json

from main import Product, db

params = pika.URLParameters('amqp://test:test@192.168.1.70:5672/%2f')

connection = pika.BlockingConnection(params)

channel = connection.channel()

channel.queue_declare(queue='main')

def callback(ch, method, properties, body):
    print('Received in admin')
    data=json.loads(body)
    print(body)

    if properties.content_type == 'product_created':
        product = Product(id=data['id'], title=data['title'], image=data['image'])
        db.session.add(product)
        db.session.commit()
        print('product created')

    elif properties.content_type == 'product_updated':
        product = Product.query.get(data['id'])
        product.title = data['title']
        product.image = data['image']
        db.session.commit()
        print('product updated')

    elif properties.content_type == 'product_deleted':
        product = Product.query.get(data)
        db.session.delete(product)
        db.session.commit()
        print('product deleted')

channel.basic_consume(queue='main', on_message_callback=callback, auto_ack=True)

print('Started consuming')

channel.start_consuming()

channel.close()

Implementing the Likes

Introduction

Within the app we are going to

  • allow flask to serialize json for tables (Flask)
  • post a like for a product id in flask
  • call the django app for a random user,
  • notify the django app of the like for the random user
  • update Django with likes

Allow Flask to Serialize Json for Tables (Flask)

We need to add the dataclass decorator and add the types to the class and set the end points

from flask import Flask, jsonify
from dataclasses import dataclass
...
@dataclass
class Product(db.Model):
    id: int
    title: str
    image: str

    id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    title = db.Column(db.String(200))
    image = db.Column(db.String(200))
....
@app.route('/api/products')
def index():
    return jsonify(Product.query.all())

Post a Like for a Product in Flask

We will receive the post, get a random use from the django app. We send a request to the django app to get a random user. We then write a record in the Product User in Flask app and publish the result to Rabbit for the Django app to receive.

@app.route('/api/products/<int:id>/like', methods=['POST'])
def like(id):

    httpclient_logging_patch()
    try:
        req = requests.get('http://djangoapp:8000/api/user') 
        json = req.json()

        app.logger.info("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ")
        app.logger.info(json)

        productUser = ProductUser(user_id=json['id'], product_id=id)
        db.session.add(productUser)
        db.session.commit()
    
        publish('product liked', id)
    except:
        abort(400, 'You already liked this product')
    
    return jsonify({
        'message' : 'success'
    })

Notify the Django App of the Like for Random User

In the like function we call publish. This sends a message to rabbit queue for the Django app to consume

import pika, json

params = pika.URLParameters('amqp://test:test@192.168.1.70:5672/%2f')

connection = pika.BlockingConnection(params)

channel = connection.channel() 

def publish(method, body):
    print('Sending from django to django')
    properties = pika.BasicProperties(method)
    channel.basic_publish(
        exchange='',
        routing_key='admin',
        body=json.dumps(body), 
        properties=properties)

Update Django likes

Once we receive the message from Rabbit MQ in Django we increase the likes

# Require because consumer runs outside of the app
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "admin.settings")

import django
django.setup()
# End of Require because consumer runs outside of the app

def callback(ch, method, properties, body):
    id = json.loads(body)
    product = Product.objects.get(id=id)
    product.likes = product.likes + 1
    product.save()
    print('products were increased')

channel.basic_consume(queue='admin', on_message_callback=callback, auto_ack=True)

channel.start_consuming()
print('Started consuming')

channel.close()

Useful Commands

Login to Container

Login to the container with

docker-compose exec backend bash

Build Container

Login to the container with

docker-compose up --build