├── .do └── app.yaml ├── .github └── dependabot.yaml ├── .gitignore ├── LICENSE ├── README.md ├── frontpage ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── manage.py ├── mysite ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── polls ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── static │ └── polls │ │ ├── images │ │ └── background.gif │ │ └── style.css ├── templates │ └── polls │ │ ├── detail.html │ │ ├── index.html │ │ └── results.html ├── tests.py ├── urls.py └── views.py ├── requirements.txt ├── static └── README.md └── templates ├── admin └── base_site.html └── index.html /.do/app.yaml: -------------------------------------------------------------------------------- 1 | name: sample-django 2 | services: 3 | - name: server 4 | github: 5 | repo: digitalocean/sample-django 6 | branch: static 7 | deploy_on_push: true 8 | run_command: gunicorn --worker-tmp-dir /dev/shm mysite.wsgi 9 | envs: 10 | - key: DISABLE_COLLECTSTATIC 11 | value: "1" 12 | scope: BUILD_TIME 13 | - key: DATABASE_URL 14 | value: "${db.DATABASE_URL}" 15 | scope: RUN_TIME 16 | - key: DJANGO_ALLOWED_HOSTS 17 | value: "${APP_DOMAIN}" 18 | scope: RUN_TIME 19 | jobs: 20 | - name: migrate 21 | kind: PRE_DEPLOY 22 | github: 23 | repo: digitalocean/sample-django 24 | branch: static 25 | deploy_on_push: true 26 | run_command: python manage.py migrate 27 | envs: 28 | - key: DISABLE_COLLECTSTATIC 29 | value: "1" 30 | scope: BUILD_TIME 31 | - key: DATABASE_URL 32 | value: "${db.DATABASE_URL}" 33 | scope: RUN_TIME 34 | static_sites: 35 | - name: static 36 | github: 37 | repo: digitalocean/sample-django 38 | branch: static 39 | deploy_on_push: true 40 | # This happens as part of the Python/DJango buildpack: 41 | # build_command: python manage.py collectstatic --noinput 42 | output_dir: staticfiles 43 | routes: 44 | - path: /static 45 | databases: 46 | # Create a new dev DB: 47 | - name: db 48 | engine: PG 49 | version: "12" 50 | # Or bring an existing DB: 51 | # - name: db 52 | # production: true 53 | # cluster_name: mydb 54 | # engine: PG 55 | # version: "12" 56 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # borrowed from https://github.com/dependabot/dependabot-core/blob/main/.github/dependabot.yml 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "bundler" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | groups: 14 | dev-dependencies: 15 | dependency-type: "development" 16 | update-types: 17 | - "minor" 18 | - "patch" 19 | - package-ecosystem: "composer" 20 | directory: "/" 21 | schedule: 22 | interval: "daily" 23 | groups: 24 | dev-dependencies: 25 | dependency-type: "development" 26 | update-types: 27 | - "minor" 28 | - "patch" 29 | - package-ecosystem: "docker" 30 | directory: "/" 31 | schedule: 32 | interval: "daily" 33 | - package-ecosystem: "github-actions" 34 | directory: "/" 35 | schedule: 36 | interval: "daily" 37 | - package-ecosystem: "gomod" 38 | directory: "/" 39 | schedule: 40 | interval: "daily" 41 | - package-ecosystem: "mix" 42 | directory: "/" 43 | schedule: 44 | interval: "daily" 45 | - package-ecosystem: "npm" 46 | directory: "/" 47 | schedule: 48 | interval: "daily" 49 | groups: 50 | dev-dependencies: 51 | dependency-type: "development" 52 | update-types: 53 | - "minor" 54 | - "patch" 55 | ignore: 56 | - dependency-name: "npm" 57 | update-types: ["version-update:semver-major"] 58 | - package-ecosystem: "pip" 59 | directory: "/" 60 | schedule: 61 | interval: "daily" 62 | - package-ecosystem: "pub" 63 | directory: "/" 64 | schedule: 65 | interval: "daily" 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | *.pyc 3 | staticfiles/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 DigitalOcean Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ **This repository is archived and kept for reference purposes only.** 2 | > It is no longer maintained and will not receive updates or support. 3 | 4 | # Getting Started # 5 | 6 | These steps will get this sample Django application running for you using DigitalOcean. 7 | This application is the standard [Django Polls Tutorial](https://docs.djangoproject.com/en/3.1/intro/tutorial01/) with an added page at `/`. 8 | This app uses a Postgres database by default. If you want to change this to use 9 | a local sqlite db (this will not persist deployments) then you'll need to set 10 | an environment variable specified below. 11 | 12 | **Note: Following these steps will result in charges for the use of DigitalOcean services** 13 | 14 | ## Requirements 15 | 16 | * You need a DigitalOcean account. If you don't already have one, you can sign up at https://cloud.digitalocean.com/registrations/new 17 | 18 | ## Forking the Sample App Source Code 19 | 20 | To use all the features of App Platform, you need to be running against your own copy of this application. To make a copy, click the Fork button above and follow the on-screen instructions. In this case, you'll be forking this repo as a starting point for your own app (see [Github documentation](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) to learn more about forking repos. 21 | 22 | After forking the repo, you should now be viewing this README in your own github org (e.g. `https://github.com//sample-django`) 23 | 24 | ## Deploying the App ## 25 | 26 | 1. Visit https://cloud.digitalocean.com/apps (if you're not logged in, you may see an error message. Visit https://cloud.digitalocean.com/login directly and authenticate, then try again) 27 | 1. Click "Launch App" or "Create App" 28 | 1. Choose GitHub and authenticate with your GitHub credentials. 29 | 1. Under Repository, choose this repository (e.g. `/sample-django`) and click **Next**. 30 | 1. On the next screen you will be prompted for the name of your app, which region you wish to deploy to, which branch you want deployments to spin off of and whether or not you wish to autodeploy the app every time an update is made to this branch. Fill this out according to how you want your app to function and click **Next**. 31 | 1. Once you have reached the next screen you will be prompted about environment variables, build and run commands, and if you want to deploy a database. 32 | 1. Click **Edit** next to the **Environment Variables** section and add the following : 33 | 1. If you wish to launch the Django app in Debug mode, set `DEBUG` to `True` 34 | 1. Set `DJANGO_ALLOWED_HOSTS` to `${APP_DOMAIN}` to set the `allowed_hosts` setting of your app to the default domain provided by DigitalOcean. 35 | 1. Set `DATABASE_URL` to `${.DATABASE_URL}`. You will set the database name in a next step. For simplicity, just name it `db` so the example would look like `${db.DATABASE_URL}` 36 | 1. You can find more information about all the different environment variables [here](#environment-variables). 37 | 1. Modify the **Run Command** setting to point to your application. For this example my project is named `mysite`. So the modified command would be `gunicorn --worker-tmp-dir /dev/shm mysite.wsgi`. 38 | 1. There is no need to modify the **Build Command** section 39 | 1. Click **Add a Database** to add the necessary Postgres db. For simplicity sake, just name it `db`. You can also connect an existing DBaaS instance if you choose. 40 | 1. Click "Next". 41 | 1. Confirm your Plan settings and how many containers you want to launch and click **Launch Basic/Pro App**. 42 | 1. You should see a "Building..." progress indicator. And you can click "Deployments"→"Details" to see more details of the build. 43 | 1. It can currently take 5-6 minutes to build this app, so please be patient. Live build logs are coming soon to provide much more feedback during deployments. 44 | 1. Once the build completes successfully, click the "Live App" link in the header and you should see your running application in a new tab, displaying the home page. 45 | 1. You will still need to perform the following tasks from the dashboard console to finish setting up your Django application: 46 | 1. Navigate to the console tab 47 | 1. Run `python manage.py migrate` to perform the initial database migration. After setup, you may need to migrate the database again if updated. For this reason, we recommend setting up a [pre-deploy Job](https://docs.digitalocean.com/products/app-platform/how-to/manage-jobs/) to migrate the database, automated to run before each deployment. This method also ensures that the migration happens successfully before serving traffic. If it fails, the old version of the service remains active instead. 48 | 1. Run `python manage.py createsuperuser` and follow the prompts to create a super user to access at `/admin`. 49 | 50 | ## Environment Variables ## 51 | Many of the Django settings necessary to have an app run on App Platform have been exposed as environment variables. Below 52 | is a list and what they do 53 | 54 | * *DEBUG* - Set Django to debug mode. Defaults to `False`. 55 | * *DJANGO_ALLOWED_HOSTS* - The hostnames django is allowed to receive requests from. This defaults to `127.0.0.1,localhost`. Can be a comma deliminated list. For more information about `allowed_hosts` view the [django documentation](https://docs.djangoproject.com/en/3.1/ref/settings/#allowed-hosts). 56 | * *DEVELOPMENT_MODE* - This determines whether to use a Postgres db or local sqlite. Defaults to `False` therefore using the Postgres db 57 | * *DATABASE_URL* - The connection url including port, username, and password to connect to a postgres db. This is provided by App Platform. Required if *DEVELOPMENT_MODE* is `False`. 58 | 59 | ## Making Changes to Your App ## 60 | 61 | As long as you left the default Autodeploy option enabled when you first launched this app, you can now make code changes and see them automatically reflected in your live application. During these automatic deployments, your application will never pause or stop serving request because the App Platform offers zero-downtime deployments. 62 | 63 | Here's an example code change you can make for this app: 64 | 1. Edit code within the repository 65 | 1. Commit the change to master. Normally it's a better practice to create a new branch for your change and then merge that branch to master after review, but for this demo you can commit to master directly. 66 | 1. Visit https://cloud.digitalocean.com/apps and navigate to your sample-python app. 67 | 1. You should see a "Building..." progress indicator, just like above. 68 | 1. Once the build completes successfully, click the "Live App" link in the header and you should see your updated application running. You may need to force refresh the page in your browser (e.g. using Shift+Reload). 69 | 70 | ## Learn More ## 71 | 72 | You can learn more about the App Platform and how to manage and update your application at https://www.digitalocean.com/docs/apps/. 73 | 74 | 75 | ## Deleting the App # 76 | 77 | When you no longer need this sample application running live, you can delete it by following these steps: 78 | 1. Visit the Apps control panel at https://cloud.digitalocean.com/apps 79 | 1. Navigate to the sample-python app 80 | 1. Choose "Settings"->"Destroy" 81 | 82 | This will delete the app and destroy any underlying DigitalOcean resources 83 | 84 | **Note: If you don't delete your app, charges for the use of DigitalOcean services will continue to accrue.** 85 | -------------------------------------------------------------------------------- /frontpage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalocean/sample-django/3bfa0e441415b1a7940f54e77c1b362ef72c81cf/frontpage/__init__.py -------------------------------------------------------------------------------- /frontpage/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /frontpage/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FrontpageConfig(AppConfig): 5 | name = 'frontpage' 6 | -------------------------------------------------------------------------------- /frontpage/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalocean/sample-django/3bfa0e441415b1a7940f54e77c1b362ef72c81cf/frontpage/migrations/__init__.py -------------------------------------------------------------------------------- /frontpage/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /frontpage/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /frontpage/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | app_name = "frontpage" 7 | urlpatterns = [ 8 | path("", views.index, name="frontpage"), 9 | ] 10 | -------------------------------------------------------------------------------- /frontpage/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | def index(request): 5 | return render(request, "index.html", context={}) 6 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalocean/sample-django/3bfa0e441415b1a7940f54e77c1b362ef72c81cf/mysite/__init__.py -------------------------------------------------------------------------------- /mysite/asgi.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | ASGI config for rss_reader project. 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | For more information on this file, see 6 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 7 | """ 8 | 9 | import os 10 | 11 | from django.core.asgi import get_asgi_application 12 | 13 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 14 | 15 | application = get_asgi_application() 16 | -------------------------------------------------------------------------------- /mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | from pathlib import Path 13 | import os 14 | import sys 15 | import dj_database_url 16 | from urllib.parse import urlparse 17 | 18 | from django.core.management.utils import get_random_secret_key 19 | 20 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 21 | BASE_DIR = Path(__file__).resolve().parent.parent 22 | 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", get_random_secret_key()) 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = os.getenv("DEBUG", "False") == "True" 32 | 33 | ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",") 34 | 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | "polls.apps.PollsConfig", 40 | "django.contrib.admin", 41 | "django.contrib.auth", 42 | "django.contrib.contenttypes", 43 | "django.contrib.sessions", 44 | "django.contrib.messages", 45 | "django.contrib.staticfiles", 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | "django.middleware.security.SecurityMiddleware", 50 | "django.contrib.sessions.middleware.SessionMiddleware", 51 | "django.middleware.common.CommonMiddleware", 52 | "django.middleware.csrf.CsrfViewMiddleware", 53 | "django.contrib.auth.middleware.AuthenticationMiddleware", 54 | "django.contrib.messages.middleware.MessageMiddleware", 55 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 56 | "whitenoise.middleware.WhiteNoiseMiddleware", 57 | ] 58 | 59 | ROOT_URLCONF = "mysite.urls" 60 | 61 | TEMPLATES = [ 62 | { 63 | "BACKEND": "django.template.backends.django.DjangoTemplates", 64 | "DIRS": [os.path.join(BASE_DIR, "templates")], 65 | "APP_DIRS": True, 66 | "OPTIONS": { 67 | "context_processors": [ 68 | "django.template.context_processors.debug", 69 | "django.template.context_processors.request", 70 | "django.contrib.auth.context_processors.auth", 71 | "django.contrib.messages.context_processors.messages", 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = "mysite.wsgi.application" 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 82 | 83 | if os.getenv("DEVELOPMENT_MODE", "False") == "True": 84 | DATABASES = { 85 | "default": { 86 | "ENGINE": "django.db.backends.sqlite3", 87 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 88 | } 89 | } 90 | elif len(sys.argv) > 0 and sys.argv[1] != 'collectstatic': 91 | if os.getenv("DATABASE_URL", None) is None: 92 | raise Exception("DATABASE_URL environment variable not defined") 93 | DATABASES = { 94 | "default": dj_database_url.parse(os.environ.get("DATABASE_URL")), 95 | } 96 | 97 | 98 | # Password validation 99 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | { 103 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 104 | }, 105 | { 106 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 107 | }, 108 | { 109 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 110 | }, 111 | { 112 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 113 | }, 114 | ] 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 119 | 120 | LANGUAGE_CODE = "en-us" 121 | 122 | TIME_ZONE = "UTC" 123 | 124 | USE_I18N = True 125 | 126 | USE_L10N = True 127 | 128 | USE_TZ = True 129 | 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 133 | 134 | STATIC_URL = "/static/" 135 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 136 | STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) 137 | STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" 138 | -------------------------------------------------------------------------------- /mysite/urls.py: -------------------------------------------------------------------------------- 1 | """mysite URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | 19 | urlpatterns = [ 20 | path("polls/", include("polls.urls")), 21 | path("admin/", admin.site.urls), 22 | path("", include("frontpage.urls")), 23 | ] 24 | -------------------------------------------------------------------------------- /mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /polls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalocean/sample-django/3bfa0e441415b1a7940f54e77c1b362ef72c81cf/polls/__init__.py -------------------------------------------------------------------------------- /polls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Choice, Question 4 | 5 | 6 | class ChoiceInline(admin.TabularInline): 7 | model = Choice 8 | extra = 3 9 | 10 | 11 | class QuestionAdmin(admin.ModelAdmin): 12 | fieldsets = [ 13 | (None, {'fields': ['question_text']}), 14 | ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), 15 | ] 16 | inlines = [ChoiceInline] 17 | list_display = ('question_text', 'pub_date', 'was_published_recently') 18 | list_filter = ['pub_date'] 19 | search_fields = ['question_text'] 20 | 21 | admin.site.register(Question, QuestionAdmin) 22 | -------------------------------------------------------------------------------- /polls/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PollsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.AutoField' 6 | name = 'polls' 7 | -------------------------------------------------------------------------------- /polls/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-08-08 21:45 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Choice', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('choice_text', models.CharField(max_length=200)), 20 | ('votes', models.IntegerField(default=0)), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='Question', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('question_text', models.CharField(max_length=200)), 28 | ('pub_date', models.DateTimeField(verbose_name='date published')), 29 | ], 30 | ), 31 | migrations.AddField( 32 | model_name='choice', 33 | name='question', 34 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.Question'), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /polls/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalocean/sample-django/3bfa0e441415b1a7940f54e77c1b362ef72c81cf/polls/migrations/__init__.py -------------------------------------------------------------------------------- /polls/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db import models 4 | from django.utils import timezone 5 | 6 | 7 | class Question(models.Model): 8 | question_text = models.CharField(max_length=200) 9 | pub_date = models.DateTimeField('date published') 10 | 11 | def __str__(self): 12 | return self.question_text 13 | 14 | def was_published_recently(self): 15 | now = timezone.now() 16 | return now - datetime.timedelta(days=1) <= self.pub_date <= now 17 | 18 | was_published_recently.admin_order_field = 'pub_date' 19 | was_published_recently.boolean = True 20 | was_published_recently.short_description = 'Published recently?' 21 | 22 | 23 | class Choice(models.Model): 24 | question = models.ForeignKey(Question, on_delete=models.CASCADE) 25 | choice_text = models.CharField(max_length=200) 26 | votes = models.IntegerField(default=0) 27 | 28 | def __str__(self): 29 | return self.choice_text 30 | -------------------------------------------------------------------------------- /polls/static/polls/images/background.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalocean/sample-django/3bfa0e441415b1a7940f54e77c1b362ef72c81cf/polls/static/polls/images/background.gif -------------------------------------------------------------------------------- /polls/static/polls/style.css: -------------------------------------------------------------------------------- 1 | li a { 2 | color: green; 3 | } 4 | body { 5 | background: white url("images/background.gif") no-repeat; 6 | } 7 | -------------------------------------------------------------------------------- /polls/templates/polls/detail.html: -------------------------------------------------------------------------------- 1 |

{{ question.question_text }}

2 | 3 | {% if error_message %}

{{ error_message }}

{% endif %} 4 | 5 |
6 | {% csrf_token %} 7 | {% for choice in question.choice_set.all %} 8 | 9 |
10 | {% endfor %} 11 | 12 |
13 | -------------------------------------------------------------------------------- /polls/templates/polls/index.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | {% if latest_question_list %} 6 | 11 | {% else %} 12 |

No polls are available.

13 | {% endif %} 14 | -------------------------------------------------------------------------------- /polls/templates/polls/results.html: -------------------------------------------------------------------------------- 1 |

{{ question.question_text }}

2 | 3 |
    4 | {% for choice in question.choice_set.all %} 5 |
  • {{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}
  • 6 | {% endfor %} 7 |
8 | 9 | Vote again? 10 | -------------------------------------------------------------------------------- /polls/tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.test import TestCase 4 | from django.urls import reverse 5 | from django.utils import timezone 6 | 7 | from .models import Question 8 | 9 | 10 | class QuestionModelTests(TestCase): 11 | 12 | def test_was_published_recently_with_future_question(self): 13 | """ 14 | was_published_recently() returns False for questions whose pub_date 15 | is in the future. 16 | """ 17 | time = timezone.now() + datetime.timedelta(days=30) 18 | future_question = Question(pub_date=time) 19 | self.assertIs(future_question.was_published_recently(), False) 20 | 21 | 22 | def test_was_published_recently_with_old_question(self): 23 | """ 24 | was_published_recently() returns False for questions whose pub_date 25 | is older than 1 day. 26 | """ 27 | time = timezone.now() - datetime.timedelta(days=1, seconds=1) 28 | old_question = Question(pub_date=time) 29 | self.assertIs(old_question.was_published_recently(), False) 30 | 31 | def test_was_published_recently_with_recent_question(self): 32 | """ 33 | was_published_recently() returns True for questions whose pub_date 34 | is within the last day. 35 | """ 36 | time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) 37 | recent_question = Question(pub_date=time) 38 | self.assertIs(recent_question.was_published_recently(), True) 39 | 40 | 41 | def create_question(question_text, days): 42 | """ 43 | Create a question with the given `question_text` and published the 44 | given number of `days` offset to now (negative for questions published 45 | in the past, positive for questions that have yet to be published). 46 | """ 47 | time = timezone.now() + datetime.timedelta(days=days) 48 | return Question.objects.create(question_text=question_text, pub_date=time) 49 | 50 | 51 | class QuestionIndexViewTests(TestCase): 52 | def test_no_questions(self): 53 | """ 54 | If no questions exist, an appropriate message is displayed. 55 | """ 56 | response = self.client.get(reverse('polls:index')) 57 | self.assertEqual(response.status_code, 200) 58 | self.assertContains(response, "No polls are available.") 59 | self.assertQuerysetEqual(response.context['latest_question_list'], []) 60 | 61 | def test_past_question(self): 62 | """ 63 | Questions with a pub_date in the past are displayed on the 64 | index page. 65 | """ 66 | create_question(question_text="Past question.", days=-30) 67 | response = self.client.get(reverse('polls:index')) 68 | self.assertQuerysetEqual( 69 | response.context['latest_question_list'], 70 | [''] 71 | ) 72 | 73 | def test_future_question(self): 74 | """ 75 | Questions with a pub_date in the future aren't displayed on 76 | the index page. 77 | """ 78 | create_question(question_text="Future question.", days=30) 79 | response = self.client.get(reverse('polls:index')) 80 | self.assertContains(response, "No polls are available.") 81 | self.assertQuerysetEqual(response.context['latest_question_list'], []) 82 | 83 | def test_future_question_and_past_question(self): 84 | """ 85 | Even if both past and future questions exist, only past questions 86 | are displayed. 87 | """ 88 | create_question(question_text="Past question.", days=-30) 89 | create_question(question_text="Future question.", days=30) 90 | response = self.client.get(reverse('polls:index')) 91 | self.assertQuerysetEqual( 92 | response.context['latest_question_list'], 93 | [''] 94 | ) 95 | 96 | def test_two_past_questions(self): 97 | """ 98 | The questions index page may display multiple questions. 99 | """ 100 | create_question(question_text="Past question 1.", days=-30) 101 | create_question(question_text="Past question 2.", days=-5) 102 | response = self.client.get(reverse('polls:index')) 103 | self.assertQuerysetEqual( 104 | response.context['latest_question_list'], 105 | ['', ''] 106 | ) 107 | 108 | 109 | class QuestionDetailViewTests(TestCase): 110 | def test_future_question(self): 111 | """ 112 | The detail view of a question with a pub_date in the future 113 | returns a 404 not found. 114 | """ 115 | future_question = create_question(question_text='Future question.', days=5) 116 | url = reverse('polls:detail', args=(future_question.id,)) 117 | response = self.client.get(url) 118 | self.assertEqual(response.status_code, 404) 119 | 120 | def test_past_question(self): 121 | """ 122 | The detail view of a question with a pub_date in the past 123 | displays the question's text. 124 | """ 125 | past_question = create_question(question_text='Past Question.', days=-5) 126 | url = reverse('polls:detail', args=(past_question.id,)) 127 | response = self.client.get(url) 128 | self.assertContains(response, past_question.question_text) 129 | -------------------------------------------------------------------------------- /polls/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | app_name = 'polls' 7 | urlpatterns = [ 8 | path('', views.IndexView.as_view(), name='index'), 9 | path('/', views.DetailView.as_view(), name='detail'), 10 | path('/results/', views.ResultsView.as_view(), name='results'), 11 | path('/vote/', views.vote, name='vote'), 12 | ] 13 | -------------------------------------------------------------------------------- /polls/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.shortcuts import get_object_or_404, render 3 | from django.urls import reverse 4 | from django.utils import timezone 5 | from django.views import generic 6 | 7 | from .models import Choice, Question 8 | 9 | 10 | class IndexView(generic.ListView): 11 | template_name = 'polls/index.html' 12 | context_object_name = 'latest_question_list' 13 | 14 | def get_queryset(self): 15 | """ 16 | Return the last five published questions (not including those set to be 17 | published in the future). 18 | """ 19 | return Question.objects.filter( 20 | pub_date__lte=timezone.now() 21 | ).order_by('-pub_date')[:5] 22 | 23 | 24 | class DetailView(generic.DetailView): 25 | model = Question 26 | template_name = 'polls/detail.html' 27 | 28 | def get_queryset(self): 29 | """ 30 | Excludes any questions that aren't published yet. 31 | """ 32 | return Question.objects.filter(pub_date__lte=timezone.now()) 33 | 34 | 35 | class ResultsView(generic.DetailView): 36 | model = Question 37 | template_name = 'polls/results.html' 38 | 39 | 40 | def vote(request, question_id): 41 | question = get_object_or_404(Question, pk=question_id) 42 | try: 43 | selected_choice = question.choice_set.get(pk=request.POST['choice']) 44 | except (KeyError, Choice.DoesNotExist): 45 | return render(request, 'polls/detail.html', { 46 | 'question': question, 47 | 'error_message': "You didn't select a choice.", 48 | }) 49 | else: 50 | selected_choice.votes += 1 51 | selected_choice.save() 52 | return HttpResponseRedirect(reverse('polls:results', args=(question.id,))) 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.5.0 2 | backports.zoneinfo==0.2.1;python_version<"3.9" 3 | dj-database-url==0.5.0 4 | Django==4.0.3 5 | gunicorn==20.1.0 6 | psycopg2-binary==2.9.3 7 | sqlparse==0.4.2 8 | whitenoise==6.0.0 9 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | Since Git doesn't support empty directories we have to precreate this directory for static files 2 | -------------------------------------------------------------------------------- /templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} 4 | 5 | {% block branding %} 6 |

Polls Administration

7 | {% endblock %} 8 | 9 | {% block nav-global %}{% endblock %} 10 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Your New Django App 8 | 9 | 11 | 12 | 13 | 14 |
15 |
16 | DigitalOcean Logo 17 |

Welcome to your new Django App!

18 | 19 |

20 | This application is an example of how to deploy a Django Application, specifically the Django Polls App to DigitalOcean's App 22 | Platform. Check out their documentation to 23 | learn about the app. 24 |

25 | 26 |

27 | To finish setup, perform the following tasks: 28 |

    29 |
  1. Navigate to the Console tab in your app dashboard
  2. 30 |
  3. In the console, run the command python manage.py migrate to perform the inital database migrations 31 |
  4. 32 |
  5. In the console, run the command python manage.py createsuperuser and follow the prompt to create 33 | your super user
  6. 34 |
35 |

36 |
37 | 38 | 53 |
54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 |
62 | 63 | 265 | 266 | 267 | --------------------------------------------------------------------------------