Beginner's Guide to Django Web Apps

June 9, 2020, 1:49 p.m.

Django Beginners · 16 min read

Beginner's Guide to Django Web Apps

Last Modified: Oct. 14, 2020, 12:50 p.m.

Django is a free and open-source web framework designed to quickly develop web apps.

Now you may be wondering what is a web app? Well, a web application is a website with added interactive features and functionality for site users.

 

What is the difference between a web app and a website?

Whereas a website only displays information in HTML, CSS, and JavaScript, a web app allows for dialog between the site and the user using programming languages like Python and Java. 

Features such as notifications after user submission, auto-generated feed content, and map widgets are all examples of web app functionality based on user interaction.

All of this sounds great, but how do you actually implement these features?

 

How does Django help make a web app?

Django, the self-designated "web framework for perfectionists with deadlines", has many of these web app features already built-in, they just need to be imported into your project.

With Django, you get all of the functionality of a web app within a matter of hours. 

Web development can seem complicated, but with the proper tool kit and set of instructions, you'll learn to build your web app extremely quickly.

 

(1) Python virtual environments

Django is built on Python and is installed on our computer via the Python package management system. This means before anything else, you need to download and install the Python programming language on your computer.

Virtual environments are folders created on your computer that prevent project files and packages from overriding one another. You must create a virtual environment before you begin downloading project packages. If you forget to create a virtual environment, the packages will be installed on top of one another and override any previous versions.

macOS Terminal

User-Macbook:~ user$ cd desktop

User-Macbook:desktop user$ python3 -m venv env

User-Macbook:desktop user$ cd env

User-Macbook:env user$ source bin/activate

(env)User-Macbook:env user$

Windows Command Prompt

C:\Users\Owner> cd desktop

C:\Users\Owner\desktop> py -m venv env

C:\Users\Owner\desktop> cd env

C:\Users\Owner\desktop\env> Scripts\activate

(env)C:\Users\Owner\desktop\env>

For this tutorial, we will create a virtual environment in the Desktop folder. Open you Command Line Interface, enter into your Desktop folder, then run the command py -m venv env or python3 -m venv env, depending on your device. 

When the command line prompt reappears, enter into your new virtual environment, env and activate it. Once the virtual environment is activated, you will see (env) at the start of every command line prompt.

For more information see: Creating a Python Virtual Environment 

 

(2) Django installation

To use the Django web framework you need to install Django, create a project, then create an app within this project before being able to run the server and see your app in the browser.

macOS Terminal

(env)User-Macbook:env user$ pip install django==2.1.15

(env)User-Macbook:env user$ django-admin startproject mysite

(env)User-Macbook:env user$ cd mysite

(env)User-Macbook:mysite user$ python3 manage.py startapp main

Windows Command Prompt

(env)C:\Users\Owner\desktop\env> pip install django==2.1.15

(env)C:\Users\Owner\desktop\env> django-admin startproject mysite

(env)C:\Users\Owner\desktop\env> cd mysite

(env)C:\Users\Owner\desktop\env\mysite> py manage.py startapp main

With your virtual environment activated, pip install django==2.1.15.

We are specifying the Django version number as the latest version of 2.1 because the AWS Elastic Beanstalk Python 3.6 platform currently does not support Django 2.2. If you are not deploying using AWS EB, you can install the newest version of Django.

Django may take a few minutes to install. When the command line prompt reappears, Django is installed and you can create a project called mysite using the django-admin startproject command. You can name the project whatever you'd like but for the sake of clarity in the tutorial, it will always be referred to as mysite

Next, we need to create a Django app or application. To do this, enter into the project then run the command to create an app named main. Again for the sake of clarity, the app will always be referred to as main but it can be named something else. 

 

macOS Terminal

(env)User-Macbook:mysite user$ python3 manage.py runserver 
Performing system checks...

System check identified no issues (0 silenced).

You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
February 26, 2019 - 09:54:26
Django version 3.0.6, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-C.

Windows Command Prompt

(env) C:\Users\Owner\Desktop\Code\env\mysite>py manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).

You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
February 26, 2019 - 09:54:26
Django version 3.0.6, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-C.

Finally, it's time for the exciting part! With everything installed and created, we can run the server to display the app in the browser. Run the command py or python3 manage.py runserver then copy and paste the address http://127.0.0.1:8000/ in a browser window to see a rocket ship stating your installation was successful. 

Django installation successful

For more information see: Quick Start to Django Installation

 

 

(3) Django configuration

We will now be working will HTML and Python files so you need a code editor, such as Sublime Text, moving forward.

First, open your entire project in Sublime. This means the env virtual environment folder. 

 

env > mysite > main > (New Folder) templates  > (New Folder) main >  (New File) home.html 

<p>Hello world!</p>

Create a templates > main folder structure in the app folder main then add an HTML file named home.html.

 

env > mysite > mysite > settings.py

INSTALLED_APPS = [
    'main.apps.MainConfig',
    ...
    ...
]

In settings.py add your app to the list of installed apps.

 

env > mysite > main > (New File) urls.py

from django.urls import path
from . import views

app_name = "main"   


urlpatterns = [
    path("", views.homepage, name="homepage"),
]

Create a new Python file that holds all of the URLs associated with your app main. In this case, it's only has a URL path to the homepage.

 

env > mysite > main > views.py

from django.shortcuts import render

# Create your views here.
def homepage(request):
	return render(request = request, template_name="main/home.html"	)

Now add a homepage view function that connects the URL path to the template home.html.

 

env > mysite > mysite > urls.py

"""mysite URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include  #add include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include ('main.urls')),   #add this
]

The last edit we need to make to a file is to add the main app URLs to the mysite project URLs.

 

macOS Terminal

(env)User-Macbook:mysite user$ python3 manage.py migrate

(env)User-Macbook:mysite user$ python3 manage.py runserver

Windows Command Prompt

(env)C:\Users\Owner\desktop\env\mysite> py manage.py migrate

(env)C:\Users\Owner\desktop\env\mysite> py manage.py runserver

Return to the CLI and run migrations to apply the preset configurations to your project. Then run the server again. You should be met by your home.html template.

Django configrations

For more information see: Quick Start to Django Configuration

 

(4) Django extends tag and block content

Moving forward, instead of having to add separate document types and html, head, and body HTML elements to every new template we can use the Django tag extends to connect each template to a header.html.

env > mysite > main >templates  > main >  (New File) header.html 

<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Title</title>
  </head>
 <body>

    {% block content %}

    {% endblock %} 
      
  </body>
</html>

Create the header template and nest block content tags in the body element.

 

env > mysite > main > templates  >main >  (New File) header.html 

<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Title</title>
    <!--Bootstrap CSS-->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>
  <body>

    {% block content %}

    {% endblock %}

    <!-- Optional Javascript -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

Here is the header.html with the Bootstrap CDNs. Bootstrap is a CSS framework that uses custom class attributes to style HTML elements. You will see this framework briefly mentioned throughout this guide. If you would like to use it, you need to add the CDNs above.

 

env > mysite > main > templates  > main >  home.html 

{% extends 'main/header.html' %}

  {% block content %}

  <!--CTA-->
  <h1>Hello world!</h1>

  {% endblock %}

Then go to one of your templates and extend the header.html at the top of the page and place all of the page's content within the same block content tags seen in the header. The page will render as normal in the browser if done correctly.

For more information see: How to use the Extends and Include Django Template Tags

 

(5) Django models

Moving on to Django models. A model is stored in a single database table and contains information and behaviors that can be called upon in your project files. 

env > mysite > main > models.py

from django.db import models

# Create your models here.

class Book(models.Model):
	book_title = models.CharField(max_length=150)
	publication_year = models.IntegerField()
	author = models.CharField(max_length=100)
	plot = models.TextField()
	

	def __str__(self):
		return self.book_title

Find models.py in your app folder and add the model Book.

 

macOS Terminal

(env)User-Macbook:mysite user$ python3 manage.py makemigrations

(env)User-Macbook:mysite user$ python3 manage.py migrate

Windows Command Prompt

(env) C:\Users\Owner\Desktop\Code\env\mysite>py manage.py makemigrations

(env)C:\Users\Owner\desktop\env\mysite> py manage.py migrate

Then you will need to makemigrations and migrate the model to the database.

 

env > mysite > main > admin.py

from django.contrib import admin
from .models import Book

# Register your models here.

admin.site.register(Book)

Add the model to the admin so it is accessible in the Django Administration site.

macOS Terminal

(env)User-Macbook:mysite user$ python3 manage.py createsuperuser
Username (leave blank to use 'owner'): admin
Email address:
Password: *****
Password (again): *****
Superuser created successfully.

(env)User-Macbook:mysite user$ python3 manage.py runserver

Windows Command Prompt

(env) C:\Users\Owner\Desktop\Code\env\mysite>py manage.py createsuperuser
Username (leave blank to use 'owner'): admin
Email address: 
Password: *****
Password (again): *****
Superuser created successfully.

(env) C:\Users\Owner\Desktop\Code\env\mysite>py manage.py runserver

Create a Django superuser in the CLI so you can log in to the Django admin site. Then run the server again and go to http://127.0.0.1:8000/admin/ in your browser window. Log in and add as many model objects as you'd like to the Books model.

 

env > mysite > main > views.py

from django.shortcuts import render
from .models import Book

# Create your views here.
def homepage(request):
	books = Book.objects.all() #queryset containing all books we just created
	return render(request=request, template_name="main/home.html", context={'books':books})

To render your model, add a queryset containing all of the model objects then return it as context for the template.

 

env > mysite > main > templates  >main >  home.html 

{% extends 'main/header.html' %}

  {% block content %}


   <div class="container p-4">
    <div class="row">
        {% for b in books %}
        <div class="col-lg-4 col-md-6 col-sm-12 pb-4">
          <div class="card h-100 p-4">
            <h4>{{b.book_title}}</h4>
            <p class="text-muted">{{b.publication_year}} | {{b.author}}</p>
            <p>{{b.plot}}</p>
          </div>
        </div>
        {% endfor %}
    </div>
   </div>


  {% endblock %}

Go to the template specified in the views function and use the Django Template Language to call on each model field. Note, we are using Bootstrap to help style this template.

For more information see: How to use Django Models

 

(6) Django pagination

Let's say you plan on adding dozens if not hundreds of model objects that will render in your template. To divide the content, you can use Django pagination. 

env > mysite > main > views.py

from django.shortcuts import render
from .models import Book
from django.core.paginator import Paginator #import Paginator

# Create your views here.
def homepage(request):
	books = Book.objects.all() #queryset containing all books we just created
	paginator = Paginator(books, 6)
	page_number = request.GET.get('page')
	page_obj = paginator.get_page(page_number)
	return render(request=request, template_name="main/home.html", context={'books':page_obj})

Go to the view function with your model and add three new variables that connect your model to the Paginator class. Don't forget to import Paginator at the top of the file or to add the proper context in the return. 

 

env > mysite > main >templates  > main >  home.html 

{% extends 'main/header.html' %}

  {% block content %}


   <div class="container p-4">
    <div class="row">
        {% for b in books %}
        <div class="col-lg-4 col-md-6 col-sm-12 pb-4">
          <div class="card h-100 p-4">
            <h4>{{b.book_title}}</h4>
            <p class="text-muted">{{b.publication_year}} | {{b.author}}</p>
            <p>{{b.plot}}</p>
          </div>
        </div>
        {% endfor %}
    </div>
   </div>

    <!--Pagination-->
      <div class="container p-4">
        <div class="pagination justify-content-center">
            <span class="step-links">
              {% if books.has_previous %}
                  <a href="?page=1">&laquo; first</a>
                  <a href="?page={{ books.previous_page_number }}">previous</a>
              {% endif %}

                <span class="current">
                    Page {{ books.number }} of {{ books.paginator.num_pages }}
                </span>

              {% if books.has_next %}
                  <a href="?page={{ books.next_page_number }}">next</a>
                  <a href="?page={{ books.paginator.num_pages }}">last &raquo;</a>
              {% endif %}
            </span>
          </div>
        </div>
      <!--end of Pagination-->


  {% endblock %}

Return to the HTML template connected to the view function and add the pagination code to the bottom of the page. With the default code added, you can refresh your browser page and view pagination at the bottom but there are different ways of displaying and customizing the Django pagination.

For more information see: Using Django Pagination

 

(7) Django forms

Django forms are a simple way of adding user input forms such as a contact form. To make a form that sends an inquiry email, we need to create the form fields, view function, and HTML template.

macOS Terminal

(env)User-Macbook:mysite user$ pip install django-crispy-forms

Windows Command Prompt

(env) C:\Users\Owner\desktop\code\env\mysite>pip install django-crispy-forms

Django forms have a lot of built-in functionality but they lack CSS. We will quickly take care of this will the Python package django-crispy-forms

 

env > mysite > mysite > settings.py

INSTALLED_APPS = [
    'main.apps.MainConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'crispy_forms',                #add crispy_forms to apps
]

CRISPY_TEMPLATE_PACK = 'bootstrap4' #add the Bootstrap template pack

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' #add the email backend

For this example, we are using django-crispy-forms with Bootstrap CSS which needs to be specified in the settings. Also, add the Django email backend so the emails we send will output in the CLI during development. In production, you will need to use an email sending service. 

 

env > mysite > main > (New File) forms.py

from django import forms

# Create your forms here.

class ContactForm(forms.Form):
	first_name = forms.CharField(max_length = 50)
	last_name = forms.CharField(max_length = 50)
	email_address = forms.EmailField(max_length = 150)
	message = forms.CharField(widget = forms.Textarea, max_length = 2000)

Django does not come with a forms.py file created so you need to make one and add the following to the file. If you notice, it is similar to the models.py file and format. 

 

env > mysite > main > (New File) urls.py

from django.urls import path
from . import views

app_name = "main"   


urlpatterns = [
    path("", views.homepage, name="homepage"),
    path("/contact", views.context, name="contact"),
]

You can create a separate URL pattern for the contact page.

 

env > mysite > main > views.py

from django.shortcuts import render, redirect
from .forms import ContactForm
from django.core.mail import send_mail, BadHeaderError
from django.http import HttpResponse


# Create your views here.
def contact(request):
	if request.method == 'POST':
		form = ContactForm(request.POST)
		if form.is_valid():
			subject = "Website Inquiry" 
		    body = {
			'first_name': form.cleaned_data['first_name'], 
			'last_name': form.cleaned_data['last_name'], 
			'email': form.cleaned_data['email_address'], 
			'message':form.cleaned_data['message'], 
			}
			message = "\n".join(body.values())

			try:
				send_mail(subject, message, 'admin@example.com', ['admin@example.com']) 
			except BadHeaderError:
				return HttpResponse('Invalid header found.')
			return redirect ("main:homepage")
      
	form = ContactForm()
	return render(request, "main/contact.html", {'form':form})

Then create a view function for the contact form. The view function formats the form into an email and sends it to the address specified if the fields are all valid. 

 

env > mysite > main > templates  >main >  (New File) contact.html 

{% extends 'main/header.html' %}

  {% block content %}

		{% load crispy_forms_tags %}	

		<!--Contact form-->
		<div class="container p-5">
			<h1>Contact</h1>
			<h4>Contact us directly if you have any questions</h4>
			<p>
				Please write your name, email address and a message below if you have any questions.
				One of our staff members will be happy to contact you directly and answer your questions as soon as possible. 
			</p>
			<form method="post">
	        {% csrf_token %}
	            {{form|crispy}}
	            <button class="btn btn-primary" type="submit">Submit</button>
	        </form>
	    </div>



  {% endblock %}

To use django-crispy-forms in a template you need to load the tags in at the top of the page. Then add the CSRF token and use the Django template language to call on the form with the added crispy filter. 

 

CLI

_____________________________________________________________________
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Website Inquiry
From: admin@example.com
To: admin@example.com
Date: Wed, 05 Feb 2020 00:04:43 -0000
Message-ID: [123456789@DESKTOP]

John
Smith
john@gmail.com
Hello, I have a question...

_________________________________________________________________________

Open your contact form in the browser and draft an email. When you send it, check the CLI for the email. Check out setting up an AWS SES Email Backend when you are ready for production.

For more information see: Build a Django Contact Form with Email Backend

 

(8) Django messages framework

Now you need to notify users that they have successfully sent their contact email. 

env > mysite > mysite > settings.py

INSTALLED_APPS = [
    ...
    'django.contrib.messages',
    ...
]

MIDDLEWARE = [
        ...
        'django.contrib.sessions.middleware.SessionMiddleware',
        ...
        'django.contrib.messages.middleware.MessageMiddleware',
        ...
    ]

TEMPLATES = [
			...
            'context_processors': [
								...
                'django.contrib.messages.context_processors.messages',
            ],
       ...

Open your settings.py and make sure these three lines of information are present. These allow the Django messages framework to function correctly. 

 

env > mysite > mysite > settings.py

from django.contrib.messages import constants as messages


MESSAGE_TAGS = {
        messages.DEBUG: 'alert-secondary',
        messages.INFO: 'alert-info',
        messages.SUCCESS: 'alert-success',
        messages.WARNING: 'alert-warning',
        messages.ERROR: 'alert-danger',
 }

Then import messages at the top of the page and connect the Django message tags with the Bootstrap alert messages. 

 

env > mysite > main > templates > main > (New Folder)  includes > (New File) messages.html

{% for message in messages %}
<div class="container-fluid p-0">
  <div class="alert {{ message.tags }} alert-dismissible" role="alert" >
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
      <span aria-hidden="true">&times;</span>
    </button>
    {{ message }}
  </div>
</div>
{% endfor %}

We will create a new template that holds the HTML code for the messages.

 

env > mysite > main >templates  >main >  header.html 

<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Title</title>
    <!--Bootstrap CSS-->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>
  <body>

    {% include 'main/includes/messages.html' %}

    {% block content %}

    {% endblock %}

    <!-- Optional Javascript -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

Now include messages.html in the header. If you are unclear about include, learn to use the Django include tag.

 

env > mysite > main > views.py

from django.shortcuts import render, redirect
from .forms import ContactForm
from django.core.mail import send_mail, BadHeaderError
from django.http import HttpResponse
from django.contrib import messages #import messages


# Create your views here.
def contact(request):
	if request.method == 'POST':
		form = ContactForm(request.POST)
		if form.is_valid():
			subject = "Website Inquiry" 
		    body = {
			'first_name': form.cleaned_data['first_name'], 
			'last_name': form.cleaned_data['last_name'], 
			'email': form.cleaned_data['email_address'], 
			'message':form.cleaned_data['message'], 
			}
			message = "\n".join(body.values())

			try:
				send_mail(subject, message, 'admin@example.com', ['admin@example.com']) 
			except BadHeaderError:
				return HttpResponse('Invalid header found.')
			messages.success(request, "Message sent." )
			return redirect ("main:homepage")
		messages.error(request, "Error. Message not sent.")
      
	form = ContactForm()
	return render(request, "main/contact.html", {'form':form})

We can then go back to the contact view function and add the messages. Be sure to import messages at the top of the file. 

For more information see: How to use Django Messages Framework

 

(9) Django variable security

The last, and possibly most important thing to know about your Django project is how to secure sensitive information, like settings variables and keys. 

macOS Terminal

(env)User-Macbook:mysite user$ pip install python-decouple

Windows Command Prompt

(env) C:\Users\Owner\desktop\code\env\mysite>pip install python-decouple

This can be done easily with the Python package Python Decouple. <

 

env > mysite > mysite > settings.py

from decouple import config

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', cast=bool)

env > mysite > (New File) .env

SECRET_KEY=sdjioerb43buobnodhioh4i34hgip
DEBUG=True

You will need to copy and paste all of the information you want to be hidden in the .env file then import config in settings.py and connect it back to the .env file.

For more information see: Secure Sensitive Django Variables using Python Decouple

 


 

If you are interested in building and deploying an entire Django web app, check out out the free course Building a Django Web App.


0
Subscribe now

Subscribe to stay current on our latest articles and promos





Post a Comment
Join the community

0 Comments