Django

From bibbleWiki
Jump to navigation Jump to search

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. Django MTV.png

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
Django Python.png 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 Django admin1.png
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 Django admin2.png

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