Django Crash Course
Django Crash Course
In this guide, we are going to build a web app for a weed business. This web app will include
an ecommerce section (which is what we are going to be focusing on).
Ok so now the project is created, but we still have to create a Django app.
Projects vs Apps
An app is a web application for a particular purpose e.g. we are going to create and
ecommerce app. We can create as many apps as we need in our projects but it is
important that the apps be independent of each other and serve distinct roles.
A project is a collection of configurations and apps for a particular website. It can
contain multiple apps.
Creating an app
1. We will create the ecommerce app in the iDeal directory (the one that has the
manage.py file)
cd iDeal
python manage.py startapp ecommerce
2. After creating the app, we have to add it to the INSTALLED_APPS list in our
iDeal/settings.py file
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'ecommerce' # <--
]
Models
Creating models
Models are Python classes represent the tables in our database. They allow us to
manipulate data from our database without writing SQL queries for the specific DBMS.
Django uses an SQLite database by default, we will cover using a PostgreSQL database
later.
Open the ecommerce/models.py file in a text editor and paste the following code into it
class Collection(models.Model):
time_created = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Collection"
verbose_name_plural = "Collections"
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.FloatField(default=0)
cover_image = models.FileField(null=True)
quantity = models.IntegerField(default=0)
# validate the price and quantity using the clean method and write a
test
class CollectionProduct(models.Model):
class Variant(models.Model):
name = models.CharField(max_length=30)
class Image(models.Model):
file = models.FileField()
class Order(models.Model):
time_made = models.DateTimeField(auto_now_add=True)
class OrderProduct(models.Model):
quantity = models.IntegerField()
After creating our models, to add these tables to our database we have to:
Write tests for a particular behavior, these tests fail the first time they are run
We then go and code out the required behavior that is supposed to make the tests pass
We run the tests again, this time they should pass. If the tests still fail then we should
go back to the previous step.
We observed in the previous section that we could create a product with negative prices and
quantities which doesn't make sense.
We want the models to raise a ValidationError if the price or quantity is less than 0.
Let's go and write out the tests to check the price and quantity
Open the ecommerce/tests.py file and paste the following code into it
class ProductModelTests(TestCase):
def test_price_less_than_0_raises_error(self):
"""
checks whether the save method raises a ValidationError when the
price < 0
"""
def test_quantity_less_than_0_raises_error(self):
"""
"""
def test_quantity_and_price_less_than_0_raises_error(self):
"""
"""
We now have to rewrite our Product model in such a way that it passes these tests i.e.
raises a ValidationError if the quantity < 0 or the product < 0.
We will do this by overriding the clean and save methods of our model. Copy the code below
and paste it into the Product class under the comment
# validate the price and quantity using the clean method and write a
test
if self.quantity < 0:
if self.price < 0:
return super().clean()
self.clean()
To write tests for another model, simply create a new class that inherits from TestCase and
define your test methods inside that class.
In the ecommerce/tests.py file, add this line to the top where the imports are: from
django.db.utils import IntegrityError
Paste the following code into the ecommerce/tests.py file under the ProductModelTests
class.
class VariantModelTests(TestCase):
def test_product_and_variant_name_unique_together(self):
"""
Create another variant with the same product and name as the first
"""
v1 = Variant.objects.create(name='Testv', product=product)
v2 = Variant(name='Testv', product=product)
unique_together = [
["product", "name"]
Make migrations, apply the migrations and run the tests again. This time, they should all
pass.
Here are some more tests that you can write:
In the CollectionProduct model, test whether the collection and product attributes are
unique together
In the Order model, check that the discount cannot be < 0 and > 1
In the OrderProduct model, check
that the quantity > 0
that the quantity is less than or equal to the quantity of the Product
that the Product's quantity is reduced after the OrderProduct is saved
To access the admin site we need a superuser account. We can create one using the
python manage.py createsuperuser command.
Just run the command, fill in your username and password (you can leave the Email address
empty).
Once you've created the user, run the server and navigate to <SERVER_IP>:<PORT>/admin/
e.g. (localhost:5000/admin)[localhost:5000/admin].
The interface should look like this when you've logged in:
Registering models
Django doesn't know which models you want to access from the admin interface. To make a
model accessible from the admin interface, we have to register it.
We register the models for the different apps we have, in our project we have just 1 app and
we want to access all of its models from the admin site.
Paste the following code into ecommerce/admin.py
admin.site.register(Collection)
admin.site.register(Product)
admin.site.register(CollectionProduct)
admin.site.register(Variant)
admin.site.register(Image)
admin.site.register(Order)
admin.site.register(OrderProduct)
Saving the file and refreshing (or rerunning if you stopped the server) the admin page should
show this output.
Before going to the next step, try adding a Product, an Image, a Collection Product and an
Order (with corresponding Order products).
If you're up for it, try modifying any of the records you created above using the admin
interface.
Customizing the admin site
The admin site works alright but it's not very user-friendly. Here are some issues that one
notices when interacting with it:
Our records don't have explicit names e.g. the Products are called Product object
(x) where x is a number. This doesn't make much sense so we have to modify it.
To add an Image to a Product, we need to first add the Product before adding the
image. Ideally we would like to be able to add images while creating the product
To add a Variant, we need to create a product first and then add the variant to the
Product. To add multiple variants, we need to do so one by one which can be time-
consuming.
To add a Collection product, we need to create a collection, a product and then the
collection product. Bear in mind that a product can belong to more than one collection.
So with the current user interface, we need to add each of these Collection Products
one by one which is not a very good user experience.
To add an Order Product, we would have to first add the order and then manually add
the Order products to the order. Ideally we want to be able to add Order products while
creating the order.
Save the file and look at the list of products in the admin page, the string displayed should
be updated too.
ImageInline that subclasses the admin.StackedInline class. This class will enable
us to add images inline with a product.
ProductAdmin that subclasses the admin.ModelAdmin class. This class will define how
our Product model will be displayed and manipulated on the admin site.
Paste the code below into the ecommerce/admin.py (Replace the old code with this
one)
class ImageInline(admin.StackedInline):
model = Image
extra = 3
class ProductAdmin(admin.ModelAdmin):
inlines = [ImageInline]
list_display = ("name", "price", "quantity")
admin.site.register(Collection)
admin.site.register(Product, ProductAdmin)
admin.site.register(CollectionProduct)
admin.site.register(Variant)
admin.site.register(Order)
admin.site.register(OrderProduct)
Ok, so what have we done?
This is what the Add product page now looks like, we can specify 0 or more images to
add alongside the Product.
Apply the same technique to solve the issue of the variants, collectionproducts and orders:
class ImageInline(admin.StackedInline):
model = Image
extra = 3
class VariantInline(admin.StackedInline):
model = Variant
extra = 3
class CollectionProductInline(admin.StackedInline):
model = CollectionProduct
extra = 3
class OrderProductInline(admin.StackedInline):
model = OrderProduct
extra = 3
class ProductAdmin(admin.ModelAdmin):
class OrderAdmin(admin.ModelAdmin):
inlines = [OrderProductInline]
admin.site.register(Collection)
admin.site.register(Product, ProductAdmin)
admin.site.register(Order, OrderAdmin)
Create a templates directory in the same location where your manage.py file is found.
Open your iDeal/settings.py file and add a DIRS option to the TEMPLATES setting.
Replace the TEMPLATES setting with this code:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"], # <--
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
Now create an admin directory in the templates directory you created earlier (this is where
we are going to store our custom admin templates).
Copy the template admin/base_site.html from within the default Django admin template
directory in the source code of Django itself (django/contrib/admin/templates) into that
directory.
If you have difficulty locating the Django source files, run the following command python -c
"import django; print(django.__path__)" . Running it on my machine gave me this
output (I'm using a virtual environment for this)
['D:\\IAI Teaching Documents\\Level 3\\Django Crash Course HTML, CSS &
JS\\crash_course\\environment\\lib\\site-packages\\django']
Replace the code in the admin/base_site.html file that you copied to your directory with
the one below
{% extends "admin/base.html" %}
{% block branding %}
{% if user.is_anonymous %}
{% include "admin/color_theme_toggle.html" %}
{% endif %}
{% endblock %}
Users
We already created one superuser before and we can create more users with less
permissions.
On the admin site, there's a section for 'Authentication and Authorization' that contains
Groups and Users.
Go to the Users page to see the list of users and click the 'Add User' button. This will take
you to a page where you have to fill the username and password. Put whatever you want
here.
You can also edit the user after creating to put in more details such as the first and last name
as well as the permissions.
Notice how this user can only view the models that he has permissions on.
Groups
So we created a user and assigned permissions above, but if we had to create many users
and assign them the same permissions, it gets pretty tiresome.
So instead, we would create a Group with permissions and then assign users to that group.
Each user in a group has all the permissions that the group has. A user can belong to more
than one group.
To create a group, we go to the Add Group page and define the group name, as well as the
group's permissions (similar to how we defined them in the Users) and save.
A view function, or simply a view, is a Python function or method that receives a web request
and returns a web response. These views are responsible for processing user input,
interacting with the model, and returning the appropriate response, often rendered using a
template (HTML file).
Views are connected to specific URLS via the URLconf (URL configuration). This is typically
defined in the urls.py file.
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('ecommerce.urls'))
]
This tells Django to check in the ecommerce/urls.py file for any URL that does not start
with admin.
Create a function, index in the ecommerce/views file that simply returns a Hello
message. Replace the code in the ecommerce/views.py file with the one below:
def index(request):
urlpatterns = [
Add a new folder to your project directory called uploads . You might want to add this
folder to the .gitignore as it can get pretty large.
Go to the iDeal/settings.py and add these 2 lines at the end:
MEDIA_URL = '/media/'
Go to the iDeal/urls.py file and replace the urlpatterns list with this one
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('ecommerce.urls'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Ok, almost done. Now we need to specify in the models where the uploaded files
should be saved. Go to your ecommerce/models.py file and in each FileField pass
another parameter called upload_to e.g. in the Product class, cover_image =
models.FileField(null=True, upload_to='uploads') . Do the same for the Image
model
Now if you try to upload from the admin, it gets saved to the uploads folder.