One place for hosting & domains

      Django

      How To Deploy a Django App on App Platform


      The author selected the Diversity in Tech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      Django is a powerful web framework that allows you to deploy your Python applications or websites. Django includes many features such as authentication, a custom database ORM (Object-Relational Mapper), and an extensible plugin architecture. Django simplifies the complexities of web development, allowing you to focus on writing code.

      In this tutorial, you’ll configure a Django project and deploy it to DigitalOcean’s App Platform using GitHub.

      Prerequisites

      To complete this tutorial, you’ll need:

      Step 1 — Creating a Python Virtual Environment for your Project

      Before you get started, you need to set up our Python developer environment. You will install your Python requirements within a virtual environment for easier management.

      First, create a directory in your home directory that you can use to store all of your virtual environments:

      Now create your virtual environment using Python:

      • python3 -m venv ~/.venvs/django

      This will create a directory called django within your .venvs directory. Inside, it will install a local version of Python and a local version of pip. You can use this to install and configure an isolated Python environment for your project.

      Before you install your project’s Python requirements, you need to activate the virtual environment. You can do that by typing:

      • source ~.venvs/django/bin/activate

      Your prompt should change to indicate that you are now operating within a Python virtual environment. It will look something like this: (django)user@host:~$.

      With your virtual environment active, install Django, Gunicorn, whitenoise, dj-database-url, and the psycopg2 PostgreSQL adaptor with the local instance of pip:

      • pip install django gunicorn psycopg2-binary dj-database-url

      Note: When the virtual environment is activated (when your prompt has (django) preceding it), use pip instead of pip3, even if you are using Python 3. The virtual environment’s copy of the tool is always named pip, regardless of the Python version.

      These packages do the following:

      • django - Installs the Django framework and libraries
      • gunicorn - A tool for deploying Django with a WSGI
      • dj-database-url - A Django tool for parsing a database URL
      • psycopg2 - A PostgreSQL adapter that allows Django to connect to a PostgreSQL database

      Now that you have these packages installed, you will need to save these requirements and their dependencies so App Platform can install them later. You can do this using pip and saving the information to a requirements.txt file:

      • pip freeze > requirements.txt

      You should now have all of the software needed to start a Django project. You are almost ready to deploy.

      Step 2 — Creating the Django Project

      Create your project using the django-admin tool that was installed when you installed Django:

      • django-admin startproject django_app

      At this point, your current directory (django_app in your case) will have the following content:

      • manage.py: A Django project management script.
      • django_app/: The Django project package. This should contain the __init__.py, settings.py, urls.py, asgi.py, and wsgi.py files.

      This directory will be the root directory of your project and will be what we upload to GitHub. Navigate into this directory with the command:

      Let’s adjust some settings before deployment.

      Adjusting the Project Settings

      Now that you’ve created a Django project, you’ll need to modify the settings to ensure it will run properly in App Platform. Open the settings file in your text editor:

      • nano django_app/settings.py

      Let’s examine our configuration one step at a time.

      Reading Environment Variables

      First, you need to add the os import statement to be able to read environment variables:

      django_app/settings.py

      import os
      

      Setting the Secret Key

      Next, you need to modify the SECRET_KEY directive. This is set by Django on the initial project creation and will have a randomly generated default value. It is unsafe to keep this hardcoded value in the code once it’s pushed to GitHub, so you should either read this from an environment variable or generate it when the application is started. To do this, add the following import statement at the top of the settings file:

      django_app/settings.py

      from django.core.management.utils import get_random_secret_key
      

      Now modify the SECRET_KEY directive to read in the value from the environment variable DJANGO_SECRET_KEY or generate the key if it does not find said environment variable:

      django_app/settings.py

      SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", get_random_secret_key())
      

      Warning: If you don’t set this environment variable, then every time the app is re-deployed, this key will change. This can have adverse effects on cookies and will require users to log in again every time this key changes. You can generate a key using an online password generator.

      Setting Allowed Hosts

      Now locate the ALLOWED_HOSTS directive. This defines a list of the server’s addresses or domain names that may be used to connect to the Django instance. Any incoming request with a Host header that is not in this list will raise an exception. Django requires that you set this to prevent a certain class of security vulnerability.

      In the square brackets, list the IP addresses or domain names that are associated with your Django server. Each item should be listed in quotations with entries separated by a comma. If you wish requests for an entire domain and any subdomains, prepend a period to the beginning of the entry.

      App Platform supplies you with a custom URL as a default and then allows you to set a custom domain after you have deployed the application. Since you won’t know this custom URL until you have deployed the application, you should attempt to read the ALLOWED_HOSTS from an environment variable, so App Platform can inject this into your app when it launches.

      We’ll cover this process more in-depth in a later section. But for now, modify your ALLOWED_HOSTS directive to attempt to read the hosts from an environment variable. The environment variable can be set to either a single host or a comma-delimited list:

      django_app/settings.py

      ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",")
      

      Setting the Debug Directive

      Next you should modify the DEBUG directive so that you can toggle this by setting an environment variable:

      django_app/settings.py

      DEBUG = os.getenv("DEBUG", "False") == "True"
      

      Here you used the getenv method to check for an environment variable named DEBUG. If this variable isn’t found, we should default to False for safety. Since environment variables will be read in as strings from App Platform, be sure to make a comparison to ensure that your variable is evaluated correctly.

      Setting the Development Mode

      Now create a new directive named DEVELOPMENT_MODE that will also be set as an environment variable. This is a helper variable that you will use to determine when to connect to your Postgres database and when to connect to a local SQLite database for testing. You’ll use this variable later when setting up the database connection:

      django_app/settings.py

      DEVELOPMENT_MODE = os.getenv("DEVELOPMENT_MODE", "False") == "True"
      

      Configuring Database Access

      Next, find the section that configures database access. It will start with DATABASES. The configuration in the file is for a SQLite database. App Platform allows you to create a PostgreSQL database for our project, so you need to adjust the settings to be able to connect to it.

      Warning: If you don’t change these settings and continue with the SQLite DB, your database will be erased after every new deployment. App Platform doesn’t maintain the disk when re-deploying applications, and your data will be lost.

      Change the settings with your PostgreSQL database information. You’ll read in the database connection information and credentials from an environment variable, DATABASE_URL, that will be provided by App Platform. Use the psycopg2 adaptor we installed with pip to have Django access a PostgreSQL database. You’ll use the dj-database-url package that was installed to get all of the necessary information from the database connection URL.

      To facilitate with development of your application locally, you’ll also use an if statement here to determine if DEVELOPMENT_MODE is set to True and which database should be accessed. By default, this will be set to False, and it will attempt to connect to a PostgreSQL database. You also don’t want Django attempting to make a database connection to the PostgreSQL database when attempting to collect the static files, so you’ll write an if statement to examine the command that was executed and not connect to a database if you determine that the command given was collectstatic. App Platform will automatically collect static files when the app is deployed.

      First, install the sys library so you can determine the command that was passed to manage.py and the dj_database_url library to be able to parse the URL passed in:

      django_app/settings.py

      . . .
      import os
      import sys
      import dj_database_url
      

      Next remove the current DATABASE directive block and replace it with this:

      django_app/settings.py

      if DEVELOPMENT_MODE is True:
          DATABASES = {
              "default": {
                  "ENGINE": "django.db.backends.sqlite3",
                  "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
              }
          }
      elif len(sys.argv) > 0 and sys.argv[1] != 'collectstatic':
          if os.getenv("DATABASE_URL", None) is None:
              raise Exception("DATABASE_URL environment variable not defined")
          DATABASES = {
              "default": dj_database_url.parse(os.environ.get("DATABASE_URL")),
          }
      
      

      Next, move down to the bottom of the file and add a setting indicating where the static files should be placed. When your Django app is deployed to App Platform, python manage.py collectstatic will be run automatically. Set the route to match the STATIC_URL directive in the settings file:

      django_app/settings.py

      . . .
      STATIC_URL = "/static/"
      STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
      

      Note: If you plan on storing static files in other locations outside of your individual Django-app static files, you will need to add an additional directive to your settings file. This directive will specify where to find these files. Be aware that these directories cannot share the same name as your STATIC_ROOT:

      django_app/settings.py

      . . .
      STATIC_URL = "/static/"
      STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
      STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"))
      

      Reviewing the Completed settings.py File

      Your completed file will look like this:

      from django.core.management.utils import get_random_secret_key
      from pathlib import Path
      import os
      import sys
      import dj_database_url
      
      # Build paths inside the project like this: BASE_DIR / 'subdir'.
      BASE_DIR = Path(__file__).resolve().parent.parent
      
      
      # Quick-start development settings - unsuitable for production
      # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
      
      # SECURITY WARNING: keep the secret key used in production secret!
      SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", get_random_secret_key())
      
      # SECURITY WARNING: don't run with debug turned on in production!
      DEBUG = os.getenv("DEBUG", "False") == "True"
      
      ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",")
      
      
      # Application definition
      
      INSTALLED_APPS = [
          'django.contrib.admin',
          'django.contrib.auth',
          'django.contrib.contenttypes',
          'django.contrib.sessions',
          'django.contrib.messages',
          'django.contrib.staticfiles',
      ]
      
      MIDDLEWARE = [
          'django.middleware.security.SecurityMiddleware',
          'django.contrib.sessions.middleware.SessionMiddleware',
          'django.middleware.common.CommonMiddleware',
          'django.middleware.csrf.CsrfViewMiddleware',
          'django.contrib.auth.middleware.AuthenticationMiddleware',
          'django.contrib.messages.middleware.MessageMiddleware',
          'django.middleware.clickjacking.XFrameOptionsMiddleware',
          "whitenoise.middleware.WhiteNoiseMiddleware",
      ]
      
      ROOT_URLCONF = 'masons_django_app.urls'
      
      TEMPLATES = [
          {
              'BACKEND': 'django.template.backends.django.DjangoTemplates',
              'DIRS': [],
              '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',
                  ],
              },
          },
      ]
      
      WSGI_APPLICATION = 'masons_django_app.wsgi.application'
      
      
      # Database
      # https://docs.djangoproject.com/en/3.1/ref/settings/#databases
      DEVELOPMENT_MODE = os.getenv("DEVELOPMENT_MODE", "False") == "True"
      
      if DEVELOPMENT_MODE is True:
          DATABASES = {
              "default": {
                  "ENGINE": "django.db.backends.sqlite3",
                  "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
              }
          }
      elif len(sys.argv) > 0 and sys.argv[1] != 'collectstatic':
          if os.getenv("DATABASE_URL", None) is None:
              raise Exception("DATABASE_URL environment variable not defined")
          DATABASES = {
              "default": dj_database_url.parse(os.environ.get("DATABASE_URL")),
          }
      
      
      # Password validation
      # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
      
      AUTH_PASSWORD_VALIDATORS = [
          {
              'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
          },
          {
              'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
          },
          {
              'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
          },
          {
              'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
          },
      ]
      
      
      # Internationalization
      # https://docs.djangoproject.com/en/3.1/topics/i18n/
      
      LANGUAGE_CODE = 'en-us'
      
      TIME_ZONE = 'UTC'
      
      USE_I18N = True
      
      USE_L10N = True
      
      USE_TZ = True
      
      
      # Static files (CSS, JavaScript, Images)
      # https://docs.djangoproject.com/en/3.1/howto/static-files/
      
      STATIC_URL = "/static/"
      STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
      STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
      STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
      

      Save and close settings.py.

      You’ve now finished configuring your Django app to run on App Platform. Next, you’ll push the app to GitHub and deploy it to App Platform.

      Step 3 — Pushing the Site to GitHub

      DigitalOcean App Platform deploys your code from GitHub repositories, so the first thing you’ll need to do is get your site in a git repository and then push that repository to GitHub.

      First, initialize your Django project as a git repository:

      When you work on your Django app locally, certain files get added that are unnecessary for deployment. Let’s exclude that directory by adding it to Git’s ignore list. Create a new file called .gitignore:

      Now add the following code to the file:

      .gitignore

      db.sqlite3
      *.pyc
      

      Save and close the file.

      Now execute the following command to add files to your repository:

      • git add django_app/ manage.py requirements.txt static/

      Make your initial commit:

      • git commit -m "Initial Django App"

      Your files will commit:

      Output

      [master (root-commit) eda5d36] Initial Django App 8 files changed, 238 insertions(+) create mode 100644 django_app/__init__.py create mode 100644 django_app/asgi.py create mode 100644 django_app/settings.py create mode 100644 django_app/urls.py create mode 100644 django_app/wsgi.py create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 static/README.md

      Open your browser and navigate to GitHub, log in with your profile, and create a new repository called django-app. Create an empty repository without a README or license file.

      Once you’ve created the repository, return to the command line to push your local files to GitHub.

      First, add GitHub as a remote repository:

      • git remote add origin https://github.com/your_username/django-app

      Next, rename the default branch main, to match what GitHub expects:

      Finally, push your main branch to GitHub’s main branch:

      Your files will transfer:

      Output

      Enumerating objects: 12, done. Counting objects: 100% (12/12), done. Delta compression using up to 8 threads Compressing objects: 100% (9/9), done. Writing objects: 100% (12/12), 3.98 KiB | 150.00 KiB/s, done. Total 12 (delta 1), reused 0 (delta 0) remote: Resolving deltas: 100% (1/1), done. To github.com:yourUsername/django-app.git * [new branch] main -> main Branch 'main' set up to track remote branch 'main' from 'origin'.

      Enter your GitHub credentials when prompted to push your code.

      Your code is now on GitHub and accessible through a web browser. Now you will deploy your site to DigitalOcean’s App Platform.

      Step 4 — Deploying to DigitalOcean with App Platform

      Once the code is pushed, visit https://cloud.digitalocean.com/apps and click Launch Your App. You’ll be prompted to connect your GitHub account:

      Connect GitHub account

      Connect your account and allow DigitalOcean to access your repositories. You can choose to let DigitalOcean have access to all of your repositories or just to the ones you wish to deploy.

      Allow repository access

      Click Install and Authorize. You’ll be returned to your DigitalOcean dashboard to continue creating your app.

      Once your GitHub account is connected, select the your_account/django-app repository and click Next.

      Choose your repository

      Next, provide your app’s name, choose a region, and ensure the main branch is selected. Then ensure that Autodeploy code changes is checked. Click Next to continue.

      Choose a name, region, and branch

      DigitalOcean will detect that your project is a Python app and will automatically populate a partial run command.

      Python Application detected and partial run command populated

      Click the Edit link next to the Build and Run commands to complete the build command. Your completed build command needs to reference your project’s WSGI file. In this example, this is at django_app.wsgi. Your completed run command should be gunicorn --worker-tmp-dir /dev/shm django_app.wsgi.

      Completing the run command

      Next, you need to define the environment variables you declared in your project’s settings. App Platform has a concept of App-Wide Variables, which are environment variables that are provided by App Platform, such as APP_URL and APP_DOMAIN. The platform also maintains Component-Specific Variables, which are variables that are exported from your components. These will be useful for determining your APP_DOMAIN beforehand so you can properly set DJANGO_ALLOWED_HOSTS. You will also use these variables to copy configuration settings from your database.

      To read more about these different variables, consult the App Platform Environment Variable Documetation

      For your Django app to function, you need to set the following environment variables like so:

      • DJANGO_ALLOWED_HOSTS -> ${APP_DOMAIN}
        • This allows us to know the randomly generated URL that App Platform provides and provide it to our app
      • DATABASE_URL -> ${<NAME_OF_YOUR_DATABASE>.DATABASE_URL}
        • In this case, we’ll name our database db in the next step, so this should be ${db.DATABASE_URL}
      • DEBUG -> True
        • Set this to True for now to verify your app is functioning and set to False when it’s time for this app to be in production
      • DJANGO_SECRET_KEY -> <A RANDOM SECRET KEY>
        • You can either allow your app to generate this at every launch or pick a strong password with at least 32 characters to use as this key. Using a secure password generator is a good option for this
        • Don’t forget to click the Encrypt check box to ensure that your credentials are encrypted for safety

      Set environment variables

      To set up your database, click the Add a Database button. You will be presented with the option of selecting a small development database or integrating with a managed database elsewhere. For this deployment, select the development database and ensure that the name of the database is db. Once you’ve verified this, click the Add Database button.

      Add a database

      Click Next, and you’ll be directed to the Finalize and Launch screen where you’ll choose your plan. Be sure to select the appropriate plan to fit your needs, whether in Basic App or Professional App and then click Launch App at the bottom. Your app will build and deploy:

      App building and deploying

      Once the build process completes, the interface will show you a healthy site. Now you need to access your app’s console through the Console tab and perform the Django first launch tasks by running the following commands:

      • python manage.py migrate - This will perform your initial database migrations.
      • python manage.py createsuperuser - This will prompt you for some information to create an administrative user

      Perform initial Django tasks

      Once you are done with that, click on the link to your app provided by App Platform:

      Click on app link

      This link should take you to the standard initial Django page.

      Initial Django Page

      And now you have a Django app deployed to App Platform. Any changes that you make and push to GitHub will be automatically deployed.

      Step 5 — Deploying Your Static Files

      Now that you’ve deployed your app, you may notice that your static files aren’t being loaded if you have DEBUG set to False. Django doesn’t serve static files in production and instead wants you to deploy them using a web server or CDN. Luckily, App Platform can do just that. App Platform provides free static asset serving if you are running a service alongside it, as you are doing with your app. So you’re going to deploy your same Django app but as a static site this time.

      Once your app is deployed, add a static site component from the Components tab in your app.

      Add Static Site

      Select the same GitHub repository as your deployed Django service. Click Next to continue.

      Select GitHub Repo

      Next, provide your app’s name and ensure the main branch is selected. Click Next to continue.

      Set Static Site Name and Branch

      Your component will be detected as a Service, so you’ll want to change the type to Static Site. Essentially we’ll have Django gather our static files and serve them. Set the route to what you set your STATIC_URL directive in your settings file. We set our directive to /static/ so set the route to /static. Finally, your static files will be collected into Output Directory in your app to match your STATIC_ROOT setting in settings.py. Yours is set to staticfiles, so set Output Directory to that.

      Static Site Settings

      Click Next, and you’ll be directed to the Finalize and Launch screen. When static files are paired with a service, it is free, so you won’t see any change in your bill. Click Launch App and deploy your static files. Now, if you have Debug set to False, you’ll see your static files properly displayed. Note that this tutorial’s only static asset is a README.md file. You will likely have more than this.

      Conclusion

      In this tutorial, you set up a Django project and deployed it using DigitalOcean’s App Platform. Any changes you commit and push to your repository will be re-deployed, so you can now expand your application. You can find the example code for this tutorial in the DigitalOcean Sample Images Repository

      The example in this tutorial is a minimal Django project. Your app might have more applications and features, but the deployment process will be the same.



      Source link

      How To Deploy a Scalable and Secure Django Application with Kubernetes


      Introduction

      In this tutorial you’ll deploy a containerized Django polls application into a Kubernetes cluster.

      Django is a powerful web framework that can help you get your Python application off the ground quickly. It includes several convenient features like an object-relational mapper, user authentication, and a customizable administrative interface for your application. It also includes a caching framework and encourages clean app design through its URL Dispatcher and Template system.

      In How to Build a Django and Gunicorn Application with Docker, the Django Tutorial Polls application was modified according to the Twelve-Factor methodology for building scalable, cloud-native web apps. This containerized setup was scaled and secured with an Nginx reverse-proxy and Let’s Encrypt-signed TLS certificates in How To Scale and Secure a Django Application with Docker, Nginx, and Let’s Encrypt. In this final tutorial in the From Containers to Kubernetes with Django series, the modernized Django polls application will be deployed into a Kubernetes cluster.

      Kubernetes is a powerful open-source container orchestrator that automates the deployment, scaling and management of containerized applications. Kubernetes objects like ConfigMaps and Secrets allow you to centralize and decouple configuration from your containers, while controllers like Deployments automatically restart failed containers and enable quick scaling of container replicas. TLS encryption is enabled with an Ingress object and the ingress-nginx open-source Ingress Controller. The cert-manager Kubernetes add-on renews and issues certificates using the free Let’s Encrypt certificate authority.

      Prerequisites

      To follow this tutorial, you will need:

      • A Kubernetes 1.15+ cluster with role-based access control (RBAC) enabled. This setup will use a DigitalOcean Kubernetes cluster, but you are free to create a cluster using another method.
      • The kubectl command-line tool installed on your local machine and configured to connect to your cluster. You can read more about installing kubectl in the official documentation. If you are using a DigitalOcean Kubernetes cluster, please refer to How to Connect to a DigitalOcean Kubernetes Cluster to learn how to connect to your cluster using kubectl.
      • A registered domain name. This tutorial will use your_domain.com throughout. You can get one for free at Freenom, or use the domain registrar of your choice.
      • An ingress-nginx Ingress Controller and the cert-manager TLS certificate manager installed into your cluster and configured to issue TLS certificates. To learn how to install and configure an Ingress with cert-manager, please consult How to Set Up an Nginx Ingress with Cert-Manager on DigitalOcean Kubernetes.
      • An A DNS record with your_domain.com pointing to the Ingress Load Balancer’s public IP address. If you are using DigitalOcean to manage your domain’s DNS records, consult How to Manage DNS Records to learn how to create A records
      • An S3 object storage bucket such as a DigitalOcean Space to store your Django project’s static files and a set of Access Keys for this Space. To learn how to create a Space, consult the How to Create Spaces product documentation. To learn how to create Access Keys for Spaces, consult Sharing Access to Spaces with Access Keys. With minor changes, you can use any object storage service that the django-storages plugin supports.
      • A PostgreSQL server instance, database, and user for your Django app. With minor changes, you can use any database that Django supports.
      • A Docker Hub account and public repository. For more information on creating these, please see Repositories from the Docker documentation.
      • The Docker engine installed on your local machine. Please see How to Install and Use Docker on Ubuntu 18.04 to learn more.

      Once you have these components set up, you’re ready to begin with this guide.

      Step 1 — Cloning and Configuring the Application

      In this step we’ll clone the application code from GitHub and configure settings like database credentials and object storage keys.

      The application code and Dockerfile can be found in the polls-docker branch of the Django Tutorial Polls App GitHub repository. This repo contains code for the Django documentation’s sample Polls application, which teaches you how to build a polling application from scratch.

      The polls-docker branch contains a Dockerized version of this Polls app. To learn how the Polls app was modified to work effectively in a containerized environment, please see How to Build a Django and Gunicorn Application with Docker.

      Begin by using git to clone the polls-docker branch of the Django Tutorial Polls App GitHub repository to your local machine:

      • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

      Navigate into the django-polls directory:

      This directory contains the Django application Python code, a Dockerfile that Docker will use to build the container image, as well as an env file that contains a list of environment variables to be passed into the container’s running environment. Inspect the Dockerfile:

      Output

      FROM python:3.7.4-alpine3.10 ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex && apk add --no-cache --virtual .build-deps postgresql-dev build-base && python -m venv /env && /env/bin/pip install --upgrade pip && /env/bin/pip install --no-cache-dir -r /app/requirements.txt && runDeps="$(scanelf --needed --nobanner --recursive /env | awk '{ gsub(/,/, "nso:", $2); print "so:" $2 }' | sort -u | xargs -r apk info --installed | sort -u)" && apk add --virtual rundeps $runDeps && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"]

      This Dockerfile uses the official Python 3.7.4 Docker image as a base, and installs Django and Gunicorn’s Python package requirements, as defined in the django-polls/requirements.txt file. It then removes some unnecessary build files, copies the application code into the image, and sets the execution PATH. Finally, it declares that port 8000 will be used to accept incoming container connections, and runs gunicorn with 3 workers, listening on port 8000.

      To learn more about each of the steps in this Dockerfile, please see Step 6 of How to Build a Django and Gunicorn Application with Docker.

      Now, build the image using docker build:

      We name the image polls using the -t flag and pass in the current directory as a build context, the set of files to reference when constructing the image.

      After Docker builds and tags the image, list available images using docker images:

      You should see the polls image listed:

      OutputREPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
      polls               latest              80ec4f33aae1        2 weeks ago         197MB
      python              3.7.4-alpine3.10    f309434dea3a        8 months ago        98.7MB
      

      Before we run the Django container, we need to configure its running environment using the env file present in the current directory. This file will be passed into the docker run command used to run the container, and Docker will inject the configured environment variables into the container’s running environment.

      Open the env file with nano or your favorite editor:

      django-polls/env

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Fill in missing values for the following keys:

      • DJANGO_SECRET_KEY: Set this to a unique, unpredictable value, as detailed in the Django docs. One method of generating this key is provided in Adjusting the App Settings of the Scalable Django App tutorial.
      • DJANGO_ALLOWED_HOSTS: This variable secures the app and prevents HTTP Host header attacks. For testing purposes, set this to *, a wildcard that will match all hosts. In production you should set this to your_domain.com. To learn more about this Django setting, consult Core Settings from the Django docs.
      • DATABASE_USERNAME: Set this to the PostgreSQL database user created in the prerequisite steps.
      • DATABASE_NAME: Set this to polls or the name of the PostgreSQL database created in the prerequisite steps.
      • DATABASE_PASSWORD: Set this to the PostgreSQL user password created in the prerequisite steps.
      • DATABASE_HOST: Set this to your database’s hostname.
      • DATABASE_PORT: Set this to your database’s port.
      • STATIC_ACCESS_KEY_ID: Set this to your Space or object storage’s access key.
      • STATIC_SECRET_KEY: Set this to your Space or object storage’s access key Secret.
      • STATIC_BUCKET_NAME: Set this to your Space name or object storage bucket.
      • STATIC_ENDPOINT_URL: Set this to the appropriate Spaces or object storage endpoint URL, for example https://your_space_name.nyc3.digitaloceanspaces.com if your Space is located in the nyc3 region.

      Once you’ve finished editing, save and close the file.

      In the next step we’ll run the configured container locally and create the database schema. We’ll also upload static assets like stylesheets and images to object storage.

      Step 2 — Creating the Database Schema and Uploading Assets to Object Storage

      With the container built and configured, use docker run to override the CMD set in the Dockerfile and create the database schema using the manage.py makemigrations and manage.py migrate commands:

      • docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"

      We run the polls:latest container image, pass in the environment variable file we just modified, and override the Dockerfile command with sh -c "python manage.py makemigrations && python manage.py migrate", which will create the database schema defined by the app code.

      If you’re running this for the first time you should see:

      Output

      No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK

      This indicates that the database schema has successfully been created.

      If you’re running migrate a subsequent time, Django will perform a no-op unless the database schema has changed.

      Next, we’ll run another instance of the app container and use an interactive shell inside of it to create an administrative user for the Django project.

      • docker run -i -t --env-file env polls sh

      This will provide you with a shell prompt inside of the running container which you can use to create the Django user:

      • python manage.py createsuperuser

      Enter a username, email address, and password for your user, and after creating the user, hit CTRL+D to quit the container and kill it.

      Finally, we’ll generate the static files for the app and upload them to the DigitalOcean Space using collectstatic. Note that this may take a bit of time to complete.

      • docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"

      After these files are generated and uploaded, you’ll receive the following output.

      Output

      121 static files copied.

      We can now run the app:

      • docker run --env-file env -p 80:8000 polls

      Output

      [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9

      Here, we run the default command defined in the Dockerfile, gunicorn --bind :8000 --workers 3 mysite.wsgi:application, and expose container port 8000 so that port 80 on your local machine gets mapped to port 8000 of the polls container.

      You should now be able to navigate to the polls app using your web browser by typing http://localhost in the URL bar. Since there is no route defined for the / path, you’ll likely receive a 404 Page Not Found error, which is expected.

      Navigate to http://localhost/polls to see the Polls app interface:

      Polls Apps Interface

      To view the administrative interface, visit http://localhost/admin. You should see the Polls app admin authentication window:

      Polls Admin Auth Page

      Enter the administrative username and password you created with the createsuperuser command.

      After authenticating, you can access the Polls app’s administrative interface:

      Polls Admin Main Interface

      Note that static assets for the admin and polls apps are being delivered directly from object storage. To confirm this, consult Testing Spaces Static File Delivery.

      When you are finished exploring, hit CTRL+C in the terminal window running the Docker container to kill the container.

      With the Django app Docker image tested, static assets uploaded to object storage, and database schema configured and ready for use with your app, you’re ready to upload your Django app image to an image registry like Docker Hub.

      Step 3 — Pushing the Django App Image to Docker Hub

      To roll your app out on Kubernetes, your app image must be uploaded to a registry like Docker Hub. Kubernetes will pull the app image from its repository and then deploy it to your cluster.

      You can use a private Docker registry, like DigitalOcean Container Registry, currently free in Early Access, or a public Docker registry like Docker Hub. Docker Hub also allows you to create private Docker repositories. A public repository allows anyone to see and pull the container images, while a private repository allows you to restrict access to you and your team members.

      In this tutorial we’ll push the Django image to the public Docker Hub repository created in the prerequisites. You can also push your image to a private repository, but pulling images from a private repository is beyond the scope of this article. To learn more about authenticating Kubernetes with Docker Hub and pulling private images, please see Pull an Image from a Private Registry from the Kubernetes docs.

      Begin by logging in to Docker Hub on your local machine:

      Output

      Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username:

      Enter your Docker Hub username and password to login.

      The Django image currently has the polls:latest tag. To push it to your Docker Hub repo, re-tag the image with your Docker Hub username and repo name:

      • docker tag polls:latest your_dockerhub_username/your_dockerhub_repo_name:latest

      Push the image to the repo:

      • docker push sammy/sammy-django:latest

      In this tutorial the Docker Hub username is sammy and the repo name is sammy-django. You should replace these values with your own Docker Hub username and repo name.

      You’ll see some output that updates as image layers are pushed to Docker Hub.

      Now that your image is available to Kubernetes on Docker Hub, you can begin rolling it out in your cluster.

      Step 4 — Setting Up the ConfigMap

      When we ran the Django container locally, we passed the env file into docker run to inject configuration variables into the runtime environment. On Kubernetes, configuration variables can be injected using ConfigMaps and Secrets.

      ConfigMaps should be used to store non-confidential configuration information like app settings, and Secrets should be used for sensitive information like API keys and database credentials. They are both injected into containers in a similar fashion, but Secrets have additional access control and security features like encryption at rest. Secrets also store data in base64, while ConfigMaps store data in plain text.

      To begin, create a directory called yaml in which we’ll store our Kubernetes manifests. Navigate into the directory.

      Open a file called polls-configmap.yaml in nano or your preferred text editor:

      • nano polls-configmap.yaml

      Paste in the following ConfigMap manifest:

      polls-configmap.yaml

      apiVersion: v1
      kind: ConfigMap
      metadata:
        name: polls-config
      data:
        DJANGO_ALLOWED_HOSTS: "*"
        STATIC_ENDPOINT_URL: "https://your_space_name.space_region.digitaloceanspaces.com"
        STATIC_BUCKET_NAME: "your_space_name"
        DJANGO_LOGLEVEL: "info"
        DEBUG: "True"
        DATABASE_ENGINE: "postgresql_psycopg2"
      

      We’ve extracted the non-sensitive configuration from the env file modified in Step 1 and pasted it into a ConfigMap manifest. The ConfigMap object is called polls-config. Copy in the same values entered into the env file in the previous step.

      For testing purposes leave DJANGO_ALLOWED_HOSTS as * to disable Host header-based filtering. In a production environment you should set this to your app’s domain.

      When you’re done editing the file, save and close it.

      Create the ConfigMap in your cluster using kubectl apply:

      • kubectl apply -f polls-configmap.yaml

      Output

      configmap/polls-config created

      With the ConfigMap created, we’ll create the Secret used by our app in the next step.

      Step 5 — Setting Up the Secret

      Secret values must be base64-encoded, which means creating Secret objects in your cluster is slightly more involved than creating ConfigMaps. You can repeat the process from the previous step, manually base64-encoding Secret values and pasting them into a manifest file. You can also create them using an environment variable file, kubectl create, and the --from-env-file flag, which we’ll do in this step.

      We’ll once again use the env file from Step 1, removing variables inserted into the ConfigMap. Make a copy of the env file called polls-secrets in the yaml directory:

      • cp ../env ./polls-secrets

      Edit the file in your preferred editor:

      polls-secrets

      DJANGO_SECRET_KEY=
      DEBUG=True
      DJANGO_ALLOWED_HOSTS=
      DATABASE_ENGINE=postgresql_psycopg2
      DATABASE_NAME=polls
      DATABASE_USERNAME=
      DATABASE_PASSWORD=
      DATABASE_HOST=
      DATABASE_PORT=
      STATIC_ACCESS_KEY_ID=
      STATIC_SECRET_KEY=
      STATIC_BUCKET_NAME=
      STATIC_ENDPOINT_URL=
      DJANGO_LOGLEVEL=info
      

      Delete all the variables inserted into the ConfigMap manifest. When you’re done, it should look like this:

      polls-secrets

      DJANGO_SECRET_KEY=your_secret_key
      DATABASE_NAME=polls
      DATABASE_USERNAME=your_django_db_user
      DATABASE_PASSWORD=your_django_db_user_password
      DATABASE_HOST=your_db_host
      DATABASE_PORT=your_db_port
      STATIC_ACCESS_KEY_ID=your_space_access_key
      STATIC_SECRET_KEY=your_space_access_key_secret
      

      Be sure to use the same values used in Step 1. When you’re done, save and close the file.

      Create the Secret in your cluster using kubectl create secret:

      • kubectl create secret generic polls-secret --from-env-file=poll-secrets

      Output

      secret/polls-secret created

      Here we create a Secret object called polls-secret and pass in the secrets file we just created.

      You can inspect the Secret using kubectl describe:

      • kubectl describe secret polls-secret

      Output

      Name: polls-secret Namespace: default Labels: <none> Annotations: <none> Type: Opaque Data ==== DATABASE_PASSWORD: 8 bytes DATABASE_PORT: 5 bytes DATABASE_USERNAME: 5 bytes DJANGO_SECRET_KEY: 14 bytes STATIC_ACCESS_KEY_ID: 20 bytes STATIC_SECRET_KEY: 43 bytes DATABASE_HOST: 47 bytes DATABASE_NAME: 5 bytes

      At this point you’ve stored your app’s configuration in your Kubernetes cluster using the Secret and ConfigMap object types. We’re now ready to deploy the app into the cluster.

      Step 6 — Rolling Out the Django App Using a Deployment

      In this step you’ll create a Deployment for your Django app. A Kubernetes Deployment is a controller that can be used to manage stateless applications in your cluster. A controller is a control loop that regulates workloads by scaling them up or down. Controllers also restart and clear out failed containers.

      Deployments control one or more Pods, the smallest deployable unit in a Kubernetes cluster. Pods enclose one or more containers. To learn more about the different types of workloads you can launch, please review An Introduction to Kubernetes.

      Begin by opening a file called polls-deployment.yaml in your favorite editor:

      • nano polls-deployment.yaml

      Paste in the following Deployment manifest:

      polls-deployment.yaml

      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: polls-app
        labels:
          app: polls
      spec:
          replicas: 2
        selector:
          matchLabels:
            app: polls
        template:
          metadata:
            labels:
              app: polls
          spec:
            containers:
              - image: your_dockerhub_username/app_repo_name:latest
                name: polls
                envFrom:
                - secretRef:
                    name: polls-secret
                - configMapRef:
                    name: polls-config
                ports:
                  - containerPort: 8000
                    name: gunicorn
      

      Fill in the appropriate container image name, referencing the Django Polls image you pushed to Docker Hub in Step 2.

      Here we define a Kubernetes Deployment called polls-app and label it with the key-value pair app: polls. We specify that we’d like to run two replicas of the Pod defined below the template field.

      Using envFrom with secretRef and configMapRef, we specify that all the data from the polls-secret Secret and polls-config ConfigMap should be injected into the containers as environment variables. The ConfigMap and Secret keys become the environment variable names.

      Finally, we expose containerPort 8000 and name it gunicorn.

      To learn more about configuring Kubernetes Deployments, please consult Deployments from the Kubernetes documentation.

      When you’re done editing the file, save and close it.

      Create the Deployment in your cluster using kubectl apply -f:

      • kubectl apply -f polls-deployment.yaml
      • deployment.apps/polls-app created

      Check that the Deployment rolled out correctly using kubectl get:

      • kubectl get deploy polls-app

      Output

      NAME READY UP-TO-DATE AVAILABLE AGE polls-app 2/2 2 2 6m38s

      If you encounter an error or something isn’t quite working, you can use kubectl describe to inspect the failed Deployment:

      You can inspect the two Pods using kubectl get pod:

      Output

      NAME READY STATUS RESTARTS AGE polls-app-847f8ccbf4-2stf7 1/1 Running 0 6m42s polls-app-847f8ccbf4-tqpwm 1/1 Running 0 6m57s

      Two replicas of your Django app are now up and running in the cluster. To access the app, you need to create a Kubernetes Service, which we’ll do next.

      Step 7 — Allowing External Access using a Service

      In this step, you’ll create a Service for your Django app. A Kubernetes Service is an abstraction that allows you to expose a set of running Pods as a network service. Using a Service you can create a stable endpoint for your app that does not change as Pods die and are recreated.

      There are multiple Service types, including ClusterIP Services, which expose the Service on a cluster-internal IP, NodePort Services, which expose the Service on each Node at a static port called the NodePort, and LoadBalancer Services, which provision a cloud load balancer to direct external traffic to the Pods in your cluster (via NodePorts, which it creates automatically). To learn more about these, please see Service from the Kubernetes docs.

      In our final setup we’ll use a ClusterIP Service that is exposed using an Ingress and the Ingress Controller set up in the prerequisites for this guide. For now, to test that everything is functioning correctly, we’ll create a temporary NodePort Service to access the Django app.

      Begin by creating a file called polls-svc.yaml using your favorite editor:

      Paste in the following Service manifest:

      polls-svc.yaml

      apiVersion: v1
      kind: Service
      metadata:
        name: polls
        labels:
          app: polls
      spec:
        type: NodePort
        selector:
          app: polls
        ports:
          - port: 8000
            targetPort: 8000
      

      Here we create a NodePort Service called polls and give it the app: polls label. We then select backend Pods with the app: polls label and target their 8000 ports.

      When you’re done editing the file, save and close it.

      Roll out the Service using kubectl apply:

      • kubectl apply -f polls-svc.yaml

      Output

      service/polls created

      Confirm that your Service was created using kubectl get svc:

      Output

      NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polls NodePort 10.245.197.189 <none> 8000:32654/TCP 59s

      This output shows the Service’s cluster-internal IP and NodePort (32654). To connect to the service, we need the external IP addresses for our cluster nodes:

      Output

      NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME pool-7no0qd9e0-364fd Ready <none> 27h v1.18.8 10.118.0.5 203.0.113.1 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9 pool-7no0qd9e0-364fi Ready <none> 27h v1.18.8 10.118.0.4 203.0.113.2 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9 pool-7no0qd9e0-364fv Ready <none> 27h v1.18.8 10.118.0.3 203.0.113.3 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9

      In your web browser, visit your Polls app using any Node’s external IP address and the NodePort. Given the output above, the app’s URL would be: http://203.0.113.1:32654/polls.

      You should see the same Polls app interface that you accessed locally in Step 1:

      Polls Apps Interface

      You can repeat the same test using the /admin route: http://203.0.113.1:32654/admin. You should see the same Admin interface as before:

      Polls Admin Auth Page

      At this stage, you’ve rolled out two replicas of the Django Polls app container using a Deployment. You’ve also created a stable network endpoint for these two replicas, and made it externally accessible using a NodePort Service.

      The final step in this tutorial is to secure external traffic to your app using HTTPS. To do this we’ll use the ingress-nginx Ingress Controller installed in the prerequisites, and create an Ingress object to route external traffic to the polls Kubernetes Service.

      Step 8 — Configuring HTTPS Using Nginx Ingress and cert-manager

      Kubernetes Ingresses allow you to flexibly route traffic from outside your Kubernetes cluster to Services inside of your cluster. This is accomplished using Ingress objects, which define rules for routing HTTP and HTTPS traffic to Kubernetes Services, and Ingress Controllers, which implement the rules by load balancing traffic and routing it to the appropriate backend Services.

      In the prerequisites you installed the ingress-nginx Ingress Controller and cert-manager TLS certificate automation add-on. You also set up staging and production ClusterIssuers for your domain using the Let’s Encrypt certificate authority, and created an Ingress to test certificate issuance and TLS encryption to two dummy backend Services. Before continuing with this step, you should delete the echo-ingress Ingress created in the prerequisite tutorial:

      • kubectl delete ingress echo-ingress

      If you’d like you can also delete the dummy Services and Deployments using kubectl delete svc and kubectl delete deploy, but this is not essential to complete this tutorial.

      You should also have created a DNS A record with your_domain.com pointing to the Ingress Load Balancer’s public IP address. If you’re using a DigitalOcean Load Balancer, you can find this IP address in the Load Balancers section of the Control Panel. If you are also using DigitalOcean to manage your domain’s DNS records, consult How to Manage DNS Records to learn how to create A records.

      If you’re using DigitalOcean Kubernetes, also ensure that you’ve implemented the workaround described in Step 5 of How to Set Up an Nginx Ingress with Cert-Manager on DigitalOcean Kubernetes.

      Once you have an A record pointing to the Ingress Controller Load Balancer, you can create an Ingress for your_domain.com and the polls Service.

      Open a file called polls-ingress.yaml using your favorite editor:

      Paste in the following Ingress manifest:

      [polls-ingress.yaml]
      apiVersion: networking.k8s.io/v1beta1
      kind: Ingress
      metadata:
        name: polls-ingress
        annotations:
          kubernetes.io/ingress.class: "nginx"
          cert-manager.io/cluster-issuer: "letsencrypt-staging"
      spec:
        tls:
        - hosts:
          - your_domain.com
          secretName: polls-tls
        rules:
        - host: your_domain.com
          http:
            paths:
            - backend:
                serviceName: polls
                servicePort: 8000
      

      We create an Ingress object called polls-ingress and annotate it to instruct the control plane to use the ingress-nginx Ingress Controller and staging ClusterIssuer. We also enable TLS for your_domain.com and store the certificate and private key in a secret called polls-tls. Finally, we define a rule to route traffic for the your_domain.com host to the polls Service on port 8000.

      When you’re done editing the file, save and close it.

      Create the Ingress in your cluster using kubectl apply:

      • kubectl apply -f polls-ingress.yaml

      Output

      ingress.networking.k8s.io/polls-ingress created

      You can use kubectl describe to track the state of the Ingress you just created:

      • kubectl describe ingress polls-ingress

      Output

      Name: polls-ingress Namespace: default Address: workaround.your_domain.com Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) TLS: polls-tls terminates your_domain.com Rules: Host Path Backends ---- ---- -------- your_domain.com polls:8000 (10.244.0.207:8000,10.244.0.53:8000) Annotations: cert-manager.io/cluster-issuer: letsencrypt-staging kubernetes.io/ingress.class: nginx Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CREATE 51s nginx-ingress-controller Ingress default/polls-ingress Normal CreateCertificate 51s cert-manager Successfully created Certificate "polls-tls" Normal UPDATE 25s nginx-ingress-controller Ingress default/polls-ingress

      You can also run a describe on the polls-tls Certificate to further confirm its successful creation:

      • kubectl describe certificate polls-tls

      Output

      . . . Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Issuing 3m33s cert-manager Issuing certificate as Secret does not exist Normal Generated 3m32s cert-manager Stored new private key in temporary Secret resource "polls-tls-v9lv9" Normal Requested 3m32s cert-manager Created new CertificateRequest resource "polls-tls-drx9c" Normal Issuing 2m58s cert-manager The certificate has been successfully issued

      This confirms that the TLS certificate was successfully issued and HTTPS encryption is now active for your_domain.com.

      Given that we used the staging ClusterIssuer, most web browsers won’t trust the fake Let’s Encrypt certificate that it issued, so navigating to your_domain.com will bring you to an error page.

      To send a test request, we’ll use wget from the command-line:

      • wget -O - http://your_domain.com/polls

      Output

      . . . ERROR: cannot verify your_domain.com's certificate, issued by ‘CN=Fake LE Intermediate X1’: Unable to locally verify the issuer's authority. To connect to your_domain.com insecurely, use `--no-check-certificate'.

      We’ll use the suggested --no-check-certificate flag to bypass certificate validation:

      • wget --no-check-certificate -q -O - http://your_domain.com/polls

      Output

      <link rel="stylesheet" type="text/css" href="https://your_space.nyc3.digitaloceanspaces.com/django-polls/static/polls/style.css"> <p>No polls are available.</p>

      This output shows the HTML for the /polls interface page, also confirming that the stylesheet is being served from object storage.

      Now that you’ve successfully tested certificate issuance using the staging ClusterIssuer, you can modify the Ingress to use the production ClusterIssuer.

      Open polls-ingress.yaml for editing once again:

      Modify the cluster-issuer annotation:

      [polls-ingress.yaml]
      apiVersion: networking.k8s.io/v1beta1
      kind: Ingress
      metadata:
        name: polls-ingress
        annotations:
          kubernetes.io/ingress.class: "nginx"
          cert-manager.io/cluster-issuer: "letsencrypt-prod"
      spec:
        tls:
        - hosts:
          - your_domain.com
          secretName: polls-tls
        rules:
        - host: your_domain.com
          http:
            paths:
            - backend:
                serviceName: polls
                servicePort: 8000
      

      When you’re done, save and close the file. Update the Ingress using kubectl apply:

      • kubectl apply -f polls-ingress.yaml

      Output

      ingress.networking.k8s.io/polls-ingress configured

      You can use kubectl describe certificate polls-tls and kubectl describe ingress polls-ingress to track the certificate issuance status:

      • kubectl describe ingress polls-ingress

      Output

      . . . Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CREATE 23m nginx-ingress-controller Ingress default/polls-ingress Normal CreateCertificate 23m cert-manager Successfully created Certificate "polls-tls" Normal UPDATE 76s (x2 over 22m) nginx-ingress-controller Ingress default/polls-ingress Normal UpdateCertificate 76s cert-manager Successfully updated Certificate "polls-tls"

      The above output confirms that the new production certificate was successfully issued and stored in the polls-tls Secret.

      Navigate to your_domain.com/polls in your web browser to confirm that HTTPS encryption is enabled and everything is working as expected. You should see the Polls app interface:

      Polls Apps Interface

      Verify that HTTPS encryption is active in your web browser. If you’re using Google Chrome, arriving at the above page without any errors confirms that everything is working correctly. In addition, you should see a padlock in the URL bar. Clicking on the padlock will allow you to inspect the Let’s Encrypt certificate details.

      As a final cleanup task, you can optionally switch the polls Service type from NodePort to the internal-only ClusterIP type.

      Modify polls-svc.yaml using your editor:

      Change the type from NodePort to ClusterIP:

      polls-svc.yaml

      apiVersion: v1
      kind: Service
      metadata:
        name: polls
        labels:
          app: polls
      spec:
        type: ClusterIP
        selector:
          app: polls
        ports:
          - port: 8000
            targetPort: 8000
      

      When you’re done editing the file, save and close it.

      Roll out the changes using kubectl apply:

      • kubectl apply -f polls-svc.yaml --force

      Output

      service/polls configured

      Confirm that your Service was modified using kubectl get svc:

      Output

      NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polls ClusterIP 10.245.203.186 <none> 8000/TCP 22s

      This output shows that the Service type is now ClusterIP. The only way to access it is via your domain and the Ingress created in this step.

      Conclusion

      In this tutorial you deployed a scalable, HTTPS-secured Django app into a Kubernetes cluster. Static content is served directly from object storage, and the number of running Pods can be quickly scaled up or down using the replicas field in the polls-app Deployment manifest.

      If you’re using a DigitalOcean Space, you can also enable delivery of static assets via a content delivery network and create a custom subdomain for your Space. Please consult Enabling CDN from How to Set Up a Scalable Django App with DigitalOcean Managed Databases and Spaces to learn more.

      To review the rest of the series, please visit our From Containers to Kubernetes with Django series page.



      Source link

      Build a To-Do application Using Django and React


      Introduction

      In this tutorial, we build a Todo application using Django and React.

      React is a JS framework that is great for developing SPAs (single page applications) and it has solid documentation and a vibrant ecosystem around it.

      Django is a Python web framework that simplifies common practices in web development. Django has been around for a while, meaning most gotcha’s and problems have been solved, and there’s a set of stable libraries supporting common development needs.

      For this application, React serves as the front-end or client side framework, handling UI and getting and setting data via requests to the Django back-end, which is an API built using the Django REST framework (DRF).

      At the end of this tutorial, we will have the final application that looks like this:

      The source code for this tutorial is available here on GitHub.

      Prerequisites

      To follow along with this tutorial, you will need to:

      1. Install and set up a local programming environment for Python 3
      2. Install Node.js and Create a Local Development Environment

      Setting up the Backend

      In this section, we will set up the backend and create all the folders that we need to get things up and running, so launch a new instance of a terminal and create the project’s directory by running this command:

      $ mkdir django-todo-react
      

      Next, we will navigate into the directory:

       $ cd django-todo-react
      

      Now we will install Pipenv using pip and activate a new virtual environment:

      $ pip install pipenv
      $ pipenv shell
      

      Note: You should skip the first command if you already have Pipenv installed.

      Let’s install Django using Pipenv then create a new project called backend:

      $ pipenv install django
      $ django-admin startproject backend
      

      Next, we will navigate into the newly created backend folder and start a new application called todo. We will also run migrations and start up the server:

      $ cd backend
      $ python manage.py startapp todo
      $ python manage.py migrate
      $ python manage.py runserver
      

      At this point, if all the commands were entered correctly, we should see an instance of a Django application running on this address — http://localhost:8000

      Registering the Todo application

      We are done with the basic setup for the backend, let’s start with the more advanced things like registering the todo application as an installed app so that Django can recognise it. Open the backend/settings.py file and update the INSTALLED_APPS section as so:

          # backend/settings.py
      
          # Application definition
          INSTALLED_APPS = [
              'django.contrib.admin',
              'django.contrib.auth',
              'django.contrib.contenttypes',
              'django.contrib.sessions',
              'django.contrib.messages',
              'django.contrib.staticfiles',
              'todo' # add this 
            ]
      

      Defining the Todo model

      Let’s create a model to define how the Todo items should be stored in the database, open the todo/models.py file and update it with this snippet:

          # todo/models.py
      
          from django.db import models
          # Create your models here.
      
          # add this
          class Todo(models.Model):
            title = models.CharField(max_length=120)
            description = models.TextField()
            completed = models.BooleanField(default=False)
      
            def _str_(self):
              return self.title
      

      The code snippet above describes three properties on the Todo model:

      • Title

      • Description

      • Completed

      The completed property is the status of a task; a task will either be completed or not completed at any time. Because we have created a Todo model, we need to create a migration file and apply the changes to the database, so let’s run these commands:

      $ python manage.py makemigrations todo
      $ python manage.py migrate todo
      

      We can test to see that CRUD operations work on the Todo model we created using the admin interface that Django provides out of the box, but first, we will do a little configuration.

      Open the todo/admin.py file and update it accordingly:

          # todo/admin.py
      
          from django.contrib import admin
          from .models import Todo # add this
      
          class TodoAdmin(admin.ModelAdmin):  # add this
            list_display = ('title', 'description', 'completed') # add this
      
          # Register your models here.
          admin.site.register(Todo, TodoAdmin) # add this
      

      We will create a superuser account to access the admin interface with this command:

      $ python manage.py createsuperuser
      

      You will be prompted to enter a username, email and password for the superuser. Be sure to enter details that you can remember because you will need them to log in to the admin dashboard shortly.

      Let’s start the server once more and log in on the address — http://localhost:8000/admin:

      $ python manage.py runserver
      

      We can create, edit and delete Todo items using this interface. Let’s go ahead and create some:

      Awesome work so far, be proud of what you’ve done! In the next section, we will see how we can create the API using the Django REST framework.

      Setting up the APIs

      Now, we will quit the server (CONTROL-C) then install the djangorestframework and django-cors-headers using Pipenv:

      $ pipenv install djangorestframework django-cors-headers
      

      We need to add rest_framework and corsheaders to the list of installed applications, so open the backend/settings.py file and update the INSTALLED_APPS and MIDDLEWARE sections accordingly:

          # backend/settings.py
      
          # Application definition
          INSTALLED_APPS = [
              'django.contrib.admin',
              'django.contrib.auth',
              'django.contrib.contenttypes',
              'django.contrib.sessions',
              'django.contrib.messages',
              'django.contrib.staticfiles',
              'corsheaders',            # add this
              'rest_framework',         # add this 
              'todo',
            ]
      
          MIDDLEWARE = [
              'corsheaders.middleware.CorsMiddleware',    # add this
              'django.middleware.security.SecurityMiddleware',
              'django.contrib.sessions.middleware.SessionMiddleware',
              'django.middleware.common.CommonMiddleware',
              'django.middleware.csrf.CsrfViewMiddleware',
              'django.contrib.auth.middleware.AuthenticationMiddleware',
              'django.contrib.messages.middleware.MessageMiddleware',
              'django.middleware.clickjacking.XFrameOptionsMiddleware',
          ]
      

      Add this code snippet to the bottom of the backend/settings.py file:

          # we whitelist localhost:3000 because that's where frontend will be served
          CORS_ORIGIN_WHITELIST = (
               'localhost:3000/'
           )
      

      Django-cors-headers is a python library that will help in preventing the errors that we would normally get due to CORS. rules. In the CORS_ORIGIN_WHITELIST snippet, we whitelisted localhost:3000 because we want the frontend (which will be served on that port) of the application to interact with the API.

      Creating serializers for the Todo model

      We need serializers to convert model instances to JSON so that the frontend can work with the received data easily. We will create a todo/serializers.py file:

      $ touch todo/serializers.py
      

      Open the serializers.py file and update it with the following code.

          # todo/serializers.py
      
          from rest_framework import serializers
          from .models import Todo
      
          class TodoSerializer(serializers.ModelSerializer):
            class Meta:
              model = Todo
              fields = ('id', 'title', 'description', 'completed')
      

      In the code snippet above, we specified the model to work with and the fields we want to be converted to JSON.

      Creating the View

      We will create a TodoView class in the todo/views.py file, so update it with the following code:

          # todo/views.py
      
          from django.shortcuts import render
          from rest_framework import viewsets          # add this
          from .serializers import TodoSerializer      # add this
          from .models import Todo                     # add this
      
          class TodoView(viewsets.ModelViewSet):       # add this
            serializer_class = TodoSerializer          # add this
            queryset = Todo.objects.all()              # add this
      

      The viewsets base class provides the implementation for CRUD operations by default, what we had to do was specify the serializer class and the query set.

      Head over to the backend/urls.py file and completely replace it with the code below. This code specifies the URL path for the API:

          # backend/urls.py
      
          from django.contrib import admin
          from django.urls import path, include                 # add this
          from rest_framework import routers                    # add this
          from todo import views                            # add this
      
          router = routers.DefaultRouter()                      # add this
          router.register(r'todos', views.TodoView, 'todo')     # add this
      
          urlpatterns = [
              path('admin/', admin.site.urls),         path('api/', include(router.urls))                # add this
          ]
      

      This is the final step that completes the building of the API, we can now perform CRUD operations on the Todo model. The router class allows us to make the following queries:

      • /todos/ – This returns a list of all the Todo items (Create and Read operations can be done here).

      • /todos/id – this returns a single Todo item using the id primary key (Update and Delete operations can be done here).

      Let’s restart the server and visit this address — http://localhost:8000/api/todos:

      $ python manage.py runserver
      

      We can create a new todo item using the interface:

      If the Todo item is created successfully, you will see a screen like this:

      We can also perform DELETE and UPDATE operations on specific Todo items using their id primary keys. To do this, we will visit an address with this structure /api/todos/id. Let’s try with this address — http://localhost:8000/api/todos/1:

      That’s all for the backend of the application, now we can move on to fleshing out the frontend.

      Setting up the frontend

      We have our backend running as it should, now we will create our frontend and make it communicate with the backend over the interface that we created.

      Since we are building our frontend using React, we want to use the create-react-app CLI tool because it registers optimal settings and several benefits such as Hot reloading and Service workers. We will install the create-react-app CLI (command line interface) tool globally with this command:

      $ npm install -g create-react-app
      

      Let’s navigate back into the parent working directory — django-todo-react — of our application and create a new React application called frontend:

      $ create-react-app frontend
      

      It will probably take a while for all of the dependencies to be installed, once it’s over, your terminal should look something like this:

      Run the following commands to navigate into the working directory and start the frontend server

      $ cd frontend
      $ yarn start
      

      Note: If you don’t have Yarn installed, you can find installation instructions here.

      We can now visit this address — http://localhost:3000 — and we will see the default React screen:

      We will pull in bootstrap and reactstrap to spice the UI up a bit:

      $ yarn add bootstrap reactstrap
      

      Let’s open the src/index.css file and replace the styles there with this one:

        /__ frontend/src/index.css  __/
      
          body {
            margin: 0;
            padding: 0;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
              "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
              sans-serif;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
            background-color: #282c34;
          }
          .todo-title {
            cursor: pointer;
          }
          .completed-todo {
            text-decoration: line-through;
          }
          .tab-list > span {
            padding: 5px 8px;
            border: 1px solid #282c34;
            border-radius: 10px;
            margin-right: 5px;
            cursor: pointer;
          }
          .tab-list > span.active {
            background-color: #282c34;
            color: #ffffff;
          }
      

      We will import Bootstrap’s stylesheet in src/index.js so that we can use Bootstrap’s classes:

      
            // frontend/src/index.js
      
            import React from 'react';
            import ReactDOM from 'react-dom';
            import 'bootstrap/dist/css/bootstrap.min.css';       // add this
            import './index.css';
            import App from './App';
            import * as serviceWorker from './serviceWorker';
      
            ReactDOM.render(<App />, document.getElementById('root'));
            // If you want your app to work offline and load faster, you can change
            // unregister() to register() below. Note this comes with some pitfalls.
            // Learn more about service workers: http://bit.ly/CRA-PWA
            serviceWorker.unregister();
      

      Let’s replace the code in src/App.js with this one:

        // frontend/src/App.js
      
          import React, { Component } from "react";
          const todoItems = [
            {
              id: 1,
              title: "Go to Market",
              description: "Buy ingredients to prepare dinner",
              completed: true
            },
            {
              id: 2,
              title: "Study",
              description: "Read Algebra and History textbook for upcoming test",
              completed: false
            },
            {
              id: 3,
              title: "Sally's books",
              description: "Go to library to rent sally's books",
              completed: true
            },
            {
              id: 4,
              title: "Article",
              description: "Write article on how to use django with react",
              completed: false
            }
          ];
          class App extends Component {
            constructor(props) {
              super(props);
              this.state = {
                viewCompleted: false,
                todoList: todoItems
              };
            }
            displayCompleted = status => {
              if (status) {
                return this.setState({ viewCompleted: true });
              }
              return this.setState({ viewCompleted: false });
            };
            renderTabList = () => {
              return (
                <div className="my-5 tab-list">
                  <span
                    onClick={() => this.displayCompleted(true)}
                    className={this.state.viewCompleted ? "active" : ""}
                  >
                    complete
                  </span>
                  <span
                    onClick={() => this.displayCompleted(false)}
                    className={this.state.viewCompleted ? "" : "active"}
                  >
                    Incomplete
                  </span>
                </div>
              );
            };
            renderItems = () => {
              const { viewCompleted } = this.state;
              const newItems = this.state.todoList.filter(
                item => item.completed == viewCompleted
              );
              return newItems.map(item => (
                <li
                  key={item.id}
                  className="list-group-item d-flex justify-content-between align-items-center"
                >
                  <span
                    className={`todo-title mr-2 ${
                      this.state.viewCompleted ? "completed-todo" : ""
                    }`}
                    title={item.description}
                  >
                    {item.title}
                  </span>
                  <span>
                    <button className="btn btn-secondary mr-2"> Edit </button>
                    <button className="btn btn-danger">Delete </button>
                  </span>
                </li>
              ));
            };
            render() {
              return (
                <main className="content">
                  <h1 className="text-white text-uppercase text-center my-4">Todo app</h1>
                  <div className="row ">
                    <div className="col-md-6 col-sm-10 mx-auto p-0">
                      <div className="card p-3">
                        <div className="">
                          <button className="btn btn-primary">Add task</button>
                        </div>
                        {this.renderTabList()}
                        <ul className="list-group list-group-flush">
                          {this.renderItems()}
                        </ul>
                      </div>
                    </div>
                  </div>
                </main>
              );
            }
          }
          export default App;
      

      Okay, that’s a lot of code ?, but there’s no need to be afraid now, we haven’t started interacting with the backend API, so we included default values to populate the Todo list. The `renderTabList()` function renders two spans which help control which set of items are displayed i.e clicking on the completed tab shows completed tasks and the same for the incomplete tab.

      If we visit the React frontend application now, it will look like this:

      To handle actions such as adding and editing tasks, we will use a modal, so let’s create a Modal component in a components folder.

      Create a components folder in the src directory:

      $ mkdir src/components
      

      Create a Modal.js file in the components folder:

      $ touch src/components/Modal.js
      

      Open the Modal.js file and populate it with the code snippet below:

       // frontend/src/components/Modal.js
      
          import React, { Component } from "react";
          import {
            Button,
            Modal,
            ModalHeader,
            ModalBody,
            ModalFooter,
            Form,
            FormGroup,
            Input,
            Label
          } from "reactstrap";
      
          export default class CustomModal extends Component {
            constructor(props) {
              super(props);
              this.state = {
                activeItem: this.props.activeItem
              };
            }
            handleChange = e => {
              let { name, value } = e.target;
              if (e.target.type === "checkbox") {
                value = e.target.checked;
              }
              const activeItem = { ...this.state.activeItem, [name]: value };
              this.setState({ activeItem });
            };
            render() {
              const { toggle, onSave } = this.props;
              return (
                <Modal isOpen={true} toggle={toggle}>
                  <ModalHeader toggle={toggle}> Todo Item </ModalHeader>
                  <ModalBody>
                    <Form>
                      <FormGroup>
                        <Label for="title">Title</Label>
                        <Input
                          type="text"
                          name="title"
                          value={this.state.activeItem.title}
                          onChange={this.handleChange}
                          placeholder="Enter Todo Title"
                        />
                      </FormGroup>
                      <FormGroup>
                        <Label for="description">Description</Label>
                        <Input
                          type="text"
                          name="description"
                          value={this.state.activeItem.description}
                          onChange={this.handleChange}
                          placeholder="Enter Todo description"
                        />
                      </FormGroup>
                      <FormGroup check>
                        <Label for="completed">
                          <Input
                            type="checkbox"
                            name="completed"
                            checked={this.state.activeItem.completed}
                            onChange={this.handleChange}
                          />
                          Completed
                        </Label>
                      </FormGroup>
                    </Form>
                  </ModalBody>
                  <ModalFooter>
                    <Button color="success" onClick={() => onSave(this.state.activeItem)}>
                      Save
                    </Button>
                  </ModalFooter>
                </Modal>
              );
            }
          }
      

      We created a CustomModal class and it nests the Modal component that is derived from the reactstrap library. We also defined three fields in the form:

      • Title

      • Description

      • Completed

      These are the same fields that we defined as properties on the Todo model in the backend.

      Here’s how the CustomModal works, it receives activeItem, toggle and onSave as props.

      1. activeItem represents the Todo item to be edited.
      2. toggle is a function used to control the Modal’s state i.e open or close the modal.
      3. onSave is a function that is called to save the edited values of the Todo item.

      Next, we will import the CustomModal component into the App.js file. Head over to the src/App.js file and replace it completely with this updated version:

        // frontend/src/App.js
      
          import React, { Component } from "react";
          import Modal from "./components/Modal";
      
          const todoItems = [
            {
              id: 1,
              title: "Go to Market",
              description: "Buy ingredients to prepare dinner",
              completed: true
            },
            {
              id: 2,
              title: "Study",
              description: "Read Algebra and History textbook for upcoming test",
              completed: false
            },
            {
              id: 3,
              title: "Sally's books",
              description: "Go to library to rent sally's books",
              completed: true
            },
            {
              id: 4,
              title: "Article",
              description: "Write article on how to use django with react",
              completed: false
            }
          ];
          class App extends Component {
            constructor(props) {
              super(props);
              this.state = {
                modal: false,
                viewCompleted: false,
                activeItem: {
                  title: "",
                  description: "",
                  completed: false
                },
                todoList: todoItems
              };
            }
            toggle = () => {
              this.setState({ modal: !this.state.modal });
            };
            handleSubmit = item => {
              this.toggle();
              alert("save" + JSON.stringify(item));
            };
            handleDelete = item => {
              alert("delete" + JSON.stringify(item));
            };
            createItem = () => {
              const item = { title: "", description: "", completed: false };
              this.setState({ activeItem: item, modal: !this.state.modal });
            };
            editItem = item => {
              this.setState({ activeItem: item, modal: !this.state.modal });
            };
            displayCompleted = status => {
              if (status) {
                return this.setState({ viewCompleted: true });
              }
              return this.setState({ viewCompleted: false });
            };
            renderTabList = () => {
              return (
                <div className="my-5 tab-list">
                  <span
                    onClick={() => this.displayCompleted(true)}
                    className={this.state.viewCompleted ? "active" : ""}
                  >
                    complete
                  </span>
                  <span
                    onClick={() => this.displayCompleted(false)}
                    className={this.state.viewCompleted ? "" : "active"}
                  >
                    Incomplete
                  </span>
                </div>
              );
            };
            renderItems = () => {
              const { viewCompleted } = this.state;
              const newItems = this.state.todoList.filter(
                item => item.completed === viewCompleted
              );
              return newItems.map(item => (
                <li
                  key={item.id}
                  className="list-group-item d-flex justify-content-between align-items-center"
                >
                  <span
                    className={`todo-title mr-2 ${
                      this.state.viewCompleted ? "completed-todo" : ""
                    }`}
                    title={item.description}
                  >
                    {item.title}
                  </span>
                  <span>
                    <button
                      onClick={() => this.editItem(item)}
                      className="btn btn-secondary mr-2"
                    >
                      Edit
                    </button>
                    <button
                      onClick={() => this.handleDelete(item)}
                      className="btn btn-danger"
                    >
                      Delete
                    </button>
                  </span>
                </li>
              ));
            };
            render() {
              return (
                <main className="content">
                  <h1 className="text-white text-uppercase text-center my-4">Todo app</h1>
                  <div className="row ">
                    <div className="col-md-6 col-sm-10 mx-auto p-0">
                      <div className="card p-3">
                        <div className="">
                          <button onClick={this.createItem} className="btn btn-primary">
                            Add task
                          </button>
                        </div>
                        {this.renderTabList()}
                        <ul className="list-group list-group-flush">
                          {this.renderItems()}
                        </ul>
                      </div>
                    </div>
                  </div>
                  {this.state.modal ? (
                    <Modal
                      activeItem={this.state.activeItem}
                      toggle={this.toggle}
                      onSave={this.handleSubmit}
                    />
                  ) : null}
                </main>
              );
            }
          }
          export default App;
      

      We can now revisit the React frontend, this is what the application should resemble at this point:

      If we attempt to edit and save a Todo item, we will get an alert showing the Todo item’s object. Clicking on save, and delete will perform the fitting actions on the Todo item.

      We will now modify the application so that it interacts with the Django API we built in the previous section. Let’s start by starting up the backend server (on a different instance of the terminal) if it isn’t already running:

      $ python manage.py runserver
      

      Note: This command has to be run in the `backend` directory in a virtual Pipenv shell.

      For us to make requests to the API endpoints on the backend server, we will install a JavaScript library called axios. Let’s pull in `axios using Yarn:

      $ yarn add axios
      

      Once axios is successfully installed, head over to the frontend/package.json file and add a proxy like so:

            // frontend/package.json
      
            [...]       "name": "frontend",
            "version": "0.1.0",
            "private": true,
            "proxy": "http://localhost:8000",
            "dependencies": {
              "axios": "^0.18.0",
              "bootstrap": "^4.1.3",
              "react": "^16.5.2",
              "react-dom": "^16.5.2",
              "react-scripts": "2.0.5",
              "reactstrap": "^6.5.0"
            },
            [...]
      

      The proxy will help in tunnelling API requests to http://localhost:8000 where the Django application will handle them, so we can write the requests like this in the frontend:

      axios.get("/api/todos/")
      

      Instead of this:

      axios.get("http://localhost:8000/api/todos/")
      

      Note: You might need to restart the development server for the proxy to register with the application.

      We will modify the frontend/src/App.js one last time so that it doesn’t use the hardcoded items from the array anymore, but requests data from the backend server and lists them instead. We want to also ensure that all CRUD operations send requests to the backend server instead of interacting with the dummy data.

      Open the file and replace it with this final version:

       // frontend/src/App.js
      
          import React, { Component } from "react";
          import Modal from "./components/Modal";
          import axios from "axios";
      
          class App extends Component {
            constructor(props) {
              super(props);
              this.state = {
                viewCompleted: false,
                activeItem: {
                  title: "",
                  description: "",
                  completed: false
                },
                todoList: []
              };
            }
            componentDidMount() {
              this.refreshList();
            }
            refreshList = () => {
              axios
                .get("http://localhost:8000/api/todos/")
                .then(res => this.setState({ todoList: res.data }))
                .catch(err => console.log(err));
            };
            displayCompleted = status => {
              if (status) {
                return this.setState({ viewCompleted: true });
              }
              return this.setState({ viewCompleted: false });
            };
            renderTabList = () => {
              return (
                <div className="my-5 tab-list">
                  <span
                    onClick={() => this.displayCompleted(true)}
                    className={this.state.viewCompleted ? "active" : ""}
                  >
                    complete
                  </span>
                  <span
                    onClick={() => this.displayCompleted(false)}
                    className={this.state.viewCompleted ? "" : "active"}
                  >
                    Incomplete
                  </span>
                </div>
              );
            };
            renderItems = () => {
              const { viewCompleted } = this.state;
              const newItems = this.state.todoList.filter(
                item => item.completed === viewCompleted
              );
              return newItems.map(item => (
                <li
                  key={item.id}
                  className="list-group-item d-flex justify-content-between align-items-center"
                >
                  <span
                    className={`todo-title mr-2 ${
                      this.state.viewCompleted ? "completed-todo" : ""
                    }`}
                    title={item.description}
                  >
                    {item.title}
                  </span>
                  <span>
                    <button
                      onClick={() => this.editItem(item)}
                      className="btn btn-secondary mr-2"
                    >
                      {" "}
                      Edit{" "}
                    </button>
                    <button
                      onClick={() => this.handleDelete(item)}
                      className="btn btn-danger"
                    >
                      Delete{" "}
                    </button>
                  </span>
                </li>
              ));
            };
            toggle = () => {
              this.setState({ modal: !this.state.modal });
            };
            handleSubmit = item => {
              this.toggle();
              if (item.id) {
                axios
                  .put(`http://localhost:8000/api/todos/${item.id}/`, item)
                  .then(res => this.refreshList());
                return;
              }
              axios
                .post("http://localhost:8000/api/todos/", item)
                .then(res => this.refreshList());
            };
            handleDelete = item => {
              axios
                .delete(`http://localhost:8000/api/todos/${item.id}`)
                .then(res => this.refreshList());
            };
            createItem = () => {
              const item = { title: "", description: "", completed: false };
              this.setState({ activeItem: item, modal: !this.state.modal });
            };
            editItem = item => {
              this.setState({ activeItem: item, modal: !this.state.modal });
            };
            render() {
              return (
                <main className="content">
                  <h1 className="text-white text-uppercase text-center my-4">Todo app</h1>
                  <div className="row ">
                    <div className="col-md-6 col-sm-10 mx-auto p-0">
                      <div className="card p-3">
                        <div className="">
                          <button onClick={this.createItem} className="btn btn-primary">
                            Add task
                          </button>
                        </div>
                        {this.renderTabList()}
                        <ul className="list-group list-group-flush">
                          {this.renderItems()}
                        </ul>
                      </div>
                    </div>
                  </div>
                  {this.state.modal ? (
                    <Modal
                      activeItem={this.state.activeItem}
                      toggle={this.toggle}
                      onSave={this.handleSubmit}
                    />
                  ) : null}
                </main>
              );
            }
          }
          export default App;
      

      The refreshList() function is reusable that is called each time an API request is completed. It updates the Todo list to display the most recent list of added items.

      The handleSubmit() function takes care of both the create and update operations. If the item passed as the parameter doesn’t have an id, then it has probably not been created, so the function creates it.

      Congratulations! We have just built the fontend successfully.

      Testing the application

      Let’s start the backend server on a terminal instance that’s sourced into the Pipenv virtual shell and pointed to the backend directory:

      $ python manage.py runserver
      

      We also need to start the frontend development server:

      $ yarn start
      

      We can visit the application on this address — http://localhost:3000 — to see that it works:

      We’ve come to the end of this tutorial and learnt how to configure Django and React to interact correctly with each other. We also saw some of the benefits that come with bootstrapping a React application using the create-react-app tool, such as Hot-reloading which is basically the feature that makes it possible for the web app to reload on its own whenever a change is detected.

      The source code for this tutorial is available here on GitHub.



      Source link