Django: Difference between revisions
(42 intermediate revisions by the same user not shown) | |||
Line 144: | Line 144: | ||
<tr><td> | <tr><td> | ||
</table> | </table> | ||
==Model API== | ==Model API (Repository)== | ||
There are lots of operation you can do on Models. Here are some common ones | There are lots of operation you can do on Models. Here are some common ones | ||
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
Line 153: | Line 153: | ||
# Returns matching objects | # Returns matching objects | ||
Game.objects.filter(status = 'A') | 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> | </syntaxhighlight> | ||
Line 208: | Line 244: | ||
=Templates= | =Templates= | ||
So templates provide the HTML for the pages. I suspect that React or Angular will replace this going forward but here goes. | So templates provide the HTML for the pages. I suspect that React or Angular will replace this going forward but here goes. | ||
==Create a new app== | ==Setup a new App== | ||
===Create a new app=== | |||
<syntaxhighlight lang="bash"> | <syntaxhighlight lang="bash"> | ||
python3 manage.py startapp player | python3 manage.py startapp player | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Create a view== | ===Create a view=== | ||
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
from django.shortcuts import render | from django.shortcuts import render | ||
Line 219: | Line 256: | ||
# Create your views here. | # Create your views here. | ||
def player(request): | def player(request): | ||
return | return render(request, "player/player.html") | ||
</syntaxhighlight> | |||
===Add to Settings=== | |||
<syntaxhighlight lang="python"> | |||
# Application definition | |||
INSTALLED_APPS = [ | |||
'django.contrib.admin', | |||
... | |||
'gameplay', | |||
'player', | |||
] | |||
</syntaxhighlight> | </syntaxhighlight> | ||
==Create a route== | ===Create a route=== | ||
In the base route | In the base route | ||
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
Line 241: | Line 289: | ||
] | ] | ||
</syntaxhighlight> | </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> | |||
===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 %} | |||
{% 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 %} | |||
</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 Rabbit MQ= | ||
Line 282: | Line 675: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
All of this was taken from the website https://computingforgeeks.com/how-to-install-latest-rabbitmq-server-on-ubuntu-linux/ | All of this was taken from the website https://computingforgeeks.com/how-to-install-latest-rabbitmq-server-on-ubuntu-linux/ | ||
=Django= | =Django= | ||
On the container you need to define the app | On the container you need to define the app | ||
==Setup== | ==Setup== | ||
=== | ===Docker=== | ||
For the Docker Compose refer to the [[Docker]] page. | |||
===Django App Settings=== | ===Django App Settings=== | ||
We need to modify the following sections of settings.py the settings file for django | We need to modify the following sections of settings.py the settings file for django |
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. |
BigAutoField | It is a 64-bit integer, much like an AutoField except that it is guaranteed to fit numbers from 1 to 9223372036854775807. |
BigIntegerField | It 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)
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