├── .devcontainer
├── Dockerfile
├── README.md
├── devcontainer.json
└── docker-compose.yml
├── .env
├── .gitattributes
├── .github
├── CODE_OF_CONDUCT.md
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yaml
└── workflows
│ ├── bicep-audit.yml
│ └── python-test.yaml
├── .gitignore
├── .vscode
├── README.md
└── launch.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── azure.yaml
├── azureproject
├── __init__.py
├── asgi.py
├── production.py
├── settings.py
├── urls.py
└── wsgi.py
├── infra
├── README.md
├── appinsights.bicep
├── main.bicep
├── main.parameters.json
└── resources.bicep
├── manage.py
├── requirements.txt
├── restaurant_review
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_alter_review_rating.py
│ └── __init__.py
├── models.py
├── templates
│ ├── 500.html
│ └── restaurant_review
│ │ ├── base.html
│ │ ├── create_restaurant.html
│ │ ├── details.html
│ │ ├── index.html
│ │ └── star_rating.html
├── templatetags
│ ├── __init__.py
│ └── restaurant_extras.py
├── tests.py
├── urls.py
└── views.py
├── screenshot_website.png
├── startup.sh
└── static
├── bootstrap
├── css
│ ├── bootstrap-grid.css
│ ├── bootstrap-grid.css.map
│ ├── bootstrap-grid.min.css
│ ├── bootstrap-grid.min.css.map
│ ├── bootstrap-grid.rtl.css
│ ├── bootstrap-grid.rtl.css.map
│ ├── bootstrap-grid.rtl.min.css
│ ├── bootstrap-grid.rtl.min.css.map
│ ├── bootstrap-reboot.css
│ ├── bootstrap-reboot.css.map
│ ├── bootstrap-reboot.min.css
│ ├── bootstrap-reboot.min.css.map
│ ├── bootstrap-reboot.rtl.css
│ ├── bootstrap-reboot.rtl.css.map
│ ├── bootstrap-reboot.rtl.min.css
│ ├── bootstrap-reboot.rtl.min.css.map
│ ├── bootstrap-utilities.css
│ ├── bootstrap-utilities.css.map
│ ├── bootstrap-utilities.min.css
│ ├── bootstrap-utilities.min.css.map
│ ├── bootstrap-utilities.rtl.css
│ ├── bootstrap-utilities.rtl.css.map
│ ├── bootstrap-utilities.rtl.min.css
│ ├── bootstrap-utilities.rtl.min.css.map
│ ├── bootstrap.css
│ ├── bootstrap.css.map
│ ├── bootstrap.min.css
│ ├── bootstrap.min.css.map
│ ├── bootstrap.rtl.css
│ ├── bootstrap.rtl.css.map
│ ├── bootstrap.rtl.min.css
│ └── bootstrap.rtl.min.css.map
└── js
│ ├── bootstrap.bundle.js
│ ├── bootstrap.bundle.js.map
│ ├── bootstrap.bundle.min.js
│ ├── bootstrap.bundle.min.js.map
│ ├── bootstrap.esm.js
│ ├── bootstrap.esm.js.map
│ ├── bootstrap.esm.min.js
│ ├── bootstrap.esm.min.js.map
│ ├── bootstrap.js
│ ├── bootstrap.js.map
│ ├── bootstrap.min.js
│ └── bootstrap.min.js.map
├── favicon.ico
├── fontawesome
├── LICENSE.txt
├── attribution.js
├── css
│ ├── all.css
│ └── all.min.css
└── webfonts
│ ├── fa-brands-400.eot
│ ├── fa-brands-400.svg
│ ├── fa-brands-400.ttf
│ ├── fa-brands-400.woff
│ ├── fa-brands-400.woff2
│ ├── fa-regular-400.eot
│ ├── fa-regular-400.svg
│ ├── fa-regular-400.ttf
│ ├── fa-regular-400.woff
│ ├── fa-regular-400.woff2
│ ├── fa-solid-900.eot
│ ├── fa-solid-900.svg
│ ├── fa-solid-900.ttf
│ ├── fa-solid-900.woff
│ └── fa-solid-900.woff2
└── images
└── azure-icon.svg
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/vscode/devcontainers/python:3.12
2 |
3 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
4 | && apt-get -y install --no-install-recommends postgresql-client \
5 | && apt-get clean -y && rm -rf /var/lib/apt/lists/*
6 |
--------------------------------------------------------------------------------
/.devcontainer/README.md:
--------------------------------------------------------------------------------
1 | # .devcontainer directory
2 |
3 | This `.devcontainer` directory contains the configuration for a [dev container](https://docs.github.com/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers) and isn't used by the sample application.
4 |
5 | The dev container configuration lets you open therepository in a [GitHub codespace](https://docs.github.com/codespaces/overview) or a dev container in Visual Studio Code. For your convenience, the dev container is configured with the following:
6 |
7 | - Python
8 | - Running `pip install -r requirements.txt` from the project at container start.
9 | - PostgreSQL
10 | - Redis
11 | - [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview) (so you can run `azd` commands directly).
12 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "python-app-service-postgresql-redis-infra",
3 | "dockerComposeFile": "docker-compose.yml",
4 | "service": "app",
5 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
6 | "features": {
7 | "ghcr.io/azure/azure-dev/azd:latest": {}
8 | },
9 | "customizations": {
10 | "vscode": {
11 | // Add the IDs of extensions you want installed when the container is created.
12 | "extensions": [
13 | "ms-azuretools.azure-dev",
14 | "ms-python.python",
15 | "ms-python.vscode-pylance",
16 | "mtxr.sqltools",
17 | "mtxr.sqltools-driver-pg",
18 | "GitHub.copilot"
19 | ],
20 | "settings": {
21 | "sqltools.connections": [
22 | {
23 | "name": "Container database",
24 | "driver": "PostgreSQL",
25 | "previewLimit": 50,
26 | "server": "localhost",
27 | "port": 5432,
28 | "database": "app",
29 | "username": "app_user",
30 | "password": "app_password"
31 | }
32 | ],
33 | "python.languageServer": "Pylance",
34 | "python.linting.enabled": true,
35 | "python.linting.mypyEnabled": true,
36 | "python.testing.pytestEnabled": true,
37 | "python.formatting.provider": "black",
38 | "python.formatting.blackArgs": [
39 | "--line-length=80"
40 | ],
41 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
42 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
43 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
44 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
45 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
46 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
47 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
48 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
49 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
50 | "python.testing.pytestPath": "/usr/local/py-utils/bin/pytest"
51 | }
52 | }
53 | },
54 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
55 | // "forwardPorts": [],
56 | // Use 'postCreateCommand' to run commands after the container is created.
57 | "postCreateCommand": "pip install -r requirements.txt",
58 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
59 | "remoteUser": "vscode"
60 | }
61 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | app:
5 | build:
6 | context: ..
7 | dockerfile: .devcontainer/Dockerfile
8 |
9 | volumes:
10 | - ..:/workspace:cached
11 |
12 | # Overrides default command so things don't shut down after the process ends.
13 | command: sleep infinity
14 |
15 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
16 | network_mode: service:db
17 |
18 | db:
19 | image: postgres:latest
20 | restart: unless-stopped
21 | volumes:
22 | - postgres-data:/var/lib/postgresql/data
23 | environment:
24 | POSTGRES_DB: app
25 | POSTGRES_USER: app_user
26 | POSTGRES_PASSWORD: app_password
27 |
28 | # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
29 | # (Adding the "ports" property to this file will not forward from a Codespace.)
30 |
31 | redis:
32 | image: redis
33 | restart: unless-stopped
34 |
35 | volumes:
36 | postgres-data:
37 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | DBNAME=app
2 | DBHOST=localhost
3 | DBUSER=app_user
4 | DBPASS=app_password
5 | CACHELOCATION=redis://redis:6379/0
6 | SECRET_KEY=secret_key
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 | *.{cmd,[cC][mM][dD]} text eol=crlf
3 | *.{bat,[bB][aA][tT]} text eol=crlf
4 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
4 | > Please provide us with the following information:
5 | > ---------------------------------------------------------------
6 |
7 | ### This issue is for a: (mark with an `x`)
8 | ```
9 | - [ ] bug report -> please search issues before submitting
10 | - [ ] feature request
11 | - [ ] documentation issue or request
12 | - [ ] regression (a behavior that used to work and stopped in a new release)
13 | ```
14 |
15 | ### Minimal steps to reproduce
16 | >
17 |
18 | ### Any log messages given by the failure
19 | >
20 |
21 | ### Expected/desired behavior
22 | >
23 |
24 | ### OS and Version?
25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?)
26 |
27 | ### Versions
28 | >
29 |
30 | ### Mention any other details that might be useful
31 |
32 | > ---------------------------------------------------------------
33 | > Thanks! We'll be in touch soon.
34 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Purpose
2 |
3 | * ...
4 |
5 | ## Does this introduce a breaking change?
6 |
7 | ```
8 | [ ] Yes
9 | [ ] No
10 | ```
11 |
12 | ## Pull Request Type
13 | What kind of change does this Pull Request introduce?
14 |
15 |
16 | ```
17 | [ ] Bugfix
18 | [ ] Feature
19 | [ ] Code style update (formatting, local variables)
20 | [ ] Refactoring (no functional changes, no api changes)
21 | [ ] Documentation content changes
22 | [ ] Other... Please describe:
23 | ```
24 |
25 | ## How to Test
26 | * Get the code
27 |
28 | ```
29 | git clone [repo-address]
30 | cd [repo-name]
31 | git checkout [branch-name]
32 | npm install
33 | ```
34 |
35 | * Test the code
36 |
37 | ```
38 | ```
39 |
40 | ## What to Check
41 | Verify that the following are valid
42 | * ...
43 |
44 | ## Other Information
45 |
--------------------------------------------------------------------------------
/.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 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 | - package-ecosystem: "pip" # See documentation for possible values
13 | directory: "/" # Location of package manifests
14 | schedule:
15 | interval: "weekly"
16 |
--------------------------------------------------------------------------------
/.github/workflows/bicep-audit.yml:
--------------------------------------------------------------------------------
1 | name: Validate AZD template
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - "infra/**"
8 | pull_request:
9 | branches:
10 | - main
11 | paths:
12 | - "infra/**"
13 | workflow_dispatch:
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 | permissions:
19 | security-events: write
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v4
23 |
24 | - name: Run Microsoft Security DevOps Analysis
25 | uses: microsoft/security-devops-action@preview
26 | id: msdo
27 | continue-on-error: true
28 | with:
29 | tools: templateanalyzer
30 |
31 | - name: Upload alerts to Security tab
32 | uses: github/codeql-action/upload-sarif@v3
33 | if: github.repository_owner == 'Azure-Samples'
34 | with:
35 | sarif_file: ${{ steps.msdo.outputs.sarifFile }}
36 |
--------------------------------------------------------------------------------
/.github/workflows/python-test.yaml:
--------------------------------------------------------------------------------
1 | name: Python check
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | test_package:
11 | name: Test ${{ matrix.os }} Python ${{ matrix.python_version }}
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | os: ["ubuntu-24.04"]
17 | python_version: ["3.10", "3.11"]
18 | services:
19 | postgres:
20 | image: postgres:17
21 | env:
22 | POSTGRES_PASSWORD: postgres
23 | ports:
24 | - 5432:5432
25 | # needed because the postgres container does not provide a healthcheck
26 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
27 | steps:
28 | - uses: actions/checkout@v4
29 | - name: Setup python
30 | uses: actions/setup-python@v5
31 | with:
32 | python-version: ${{ matrix.python_version }}
33 | architecture: x64
34 | - name: Install dependencies
35 | run: |
36 | python -m pip install --upgrade pip
37 | pip install -r requirements.txt
38 | pip install flake8
39 | - name: Look for major issues with flake8
40 | run: |
41 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
42 | - name: Make sure Python code is all compilable
43 | run: |
44 | python -m compileall azureproject -f
45 | python -m compileall restaurant_review -f
46 | - name: Run Django server
47 | run: |
48 | python manage.py migrate
49 | python manage.py test restaurant_review &&
50 | python manage.py runserver &
51 | env:
52 | DBNAME: postgres
53 | DBHOST: localhost
54 | DBUSER: postgres
55 | DBPASS: postgres
56 | SECRET_KEY: django-insecure-key-${{ github.run_id }}-${{ github.run_attempt }}
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | .azure
--------------------------------------------------------------------------------
/.vscode/README.md:
--------------------------------------------------------------------------------
1 | # .vscode directory
2 |
3 | This `.vscode` directory contains configuration that lets you launch and debug in Visual Studio Code and isn't used by the sample application.
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Run Server",
6 | "type": "python",
7 | "request": "launch",
8 | "program": "${workspaceFolder}/manage.py",
9 | "args": [
10 | "runserver",
11 | ],
12 | "cwd": "${workspaceFolder}",
13 | "django": true
14 | },
15 | {
16 | "name": "Migrate",
17 | "type": "python",
18 | "request": "launch",
19 | "program": "${workspaceFolder}/manage.py",
20 | "args": [
21 | "migrate"
22 | ],
23 | "django": true
24 | },
25 | {
26 | "name": "Super",
27 | "type": "python",
28 | "request": "launch",
29 | "program": "${workspaceFolder}/manage.py",
30 | "args": [
31 | "createsuperuser"
32 | ],
33 | "django": true
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [project-title] Changelog
2 |
3 |
4 | # x.y.z (yyyy-mm-dd)
5 |
6 | *Features*
7 | * ...
8 |
9 | *Bug Fixes*
10 | * ...
11 |
12 | *Breaking Changes*
13 | * ...
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to [project-title]
2 |
3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
6 |
7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
9 | provided by the bot. You will only need to do this once across all repos using our CLA.
10 |
11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
14 |
15 | - [Code of Conduct](#coc)
16 | - [Issues and Bugs](#issue)
17 | - [Feature Requests](#feature)
18 | - [Submission Guidelines](#submit)
19 |
20 | ## Code of Conduct
21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
22 |
23 | ## Found an Issue?
24 | If you find a bug in the source code or a mistake in the documentation, you can help us by
25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can
26 | [submit a Pull Request](#submit-pr) with a fix.
27 |
28 | ## Want a Feature?
29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub
30 | Repository. If you would like to *implement* a new feature, please submit an issue with
31 | a proposal for your work first, to be sure that we can use it.
32 |
33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
34 |
35 | ## Submission Guidelines
36 |
37 | ### Submitting an Issue
38 | Before you submit an issue, search the archive, maybe your question was already answered.
39 |
40 | If your issue appears to be a bug, and hasn't been reported, open a new issue.
41 | Help us to maximize the effort we can spend fixing issues and adding new
42 | features, by not reporting duplicate issues. Providing the following information will increase the
43 | chances of your issue being dealt with quickly:
44 |
45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
46 | * **Version** - what version is affected (e.g. 0.1.2)
47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you
48 | * **Browsers and Operating System** - is this a problem with all browsers?
49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps
50 | * **Related Issues** - has a similar issue been reported before?
51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
52 | causing the problem (line of code or commit)
53 |
54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new].
55 |
56 | ### Submitting a Pull Request (PR)
57 | Before you submit your Pull Request (PR) consider the following guidelines:
58 |
59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR
60 | that relates to your submission. You don't want to duplicate effort.
61 |
62 | * Make your changes in a new git fork:
63 |
64 | * Commit your changes using a descriptive commit message
65 | * Push your fork to GitHub:
66 | * In GitHub, create a pull request
67 | * If we suggest changes then:
68 | * Make the required updates.
69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request):
70 |
71 | ```shell
72 | git rebase master -i
73 | git push -f
74 | ```
75 |
76 | That's it! Thank you for your contribution!
77 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
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
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | page_type: sample
3 | languages:
4 | - azdeveloper
5 | - python
6 | - bicep
7 | - html
8 | products:
9 | - azure
10 | - azure-app-service
11 | - azure-database-postgresql
12 | - azure-virtual-network
13 | urlFragment: msdocs-django-postgresql-sample-app
14 | name: Deploy a Python (Django) web app with PostgreSQL in Azure
15 | description: This is a Python web app using the Django framework and the Azure Database for PostgreSQL relational database service.
16 | ---
17 |
18 |
19 | # Deploy a Python (Django) web app with PostgreSQL in Azure
20 |
21 | This is a Python web app using the Django framework and the Azure Database for PostgreSQL relational database service. The Django app is hosted in a fully managed Azure App Service. This app is designed to be be run locally and then deployed to Azure. You can either deploy this project by following the tutorial [*Deploy a Python (Django or Flask) web app with PostgreSQL in Azure*](https://docs.microsoft.com/azure/app-service/tutorial-python-postgresql-app) or by using the [Azure Developer CLI (azd)](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview) according to the instructions below.
22 |
23 | Additionally, the sample application demonstrates Azure Redis Cache access by caching the restaurant details page for 60 seconds. You can add the Azure Redis Cache integration in the [secure-by-default web app + database creation wizard](https://portal.azure.com/?feature.customportal=false#create/Microsoft.AppServiceWebAppDatabaseV3), and it's also included in the [AZD template](https://github.com/Azure-Samples/python-app-service-postgresql-infra).
24 |
25 | ## Requirements
26 |
27 | The [requirements.txt](./requirements.txt) has the following packages, all used by a typical data-driven Django application:
28 |
29 | | Package | Description |
30 | | ------- | ----------- |
31 | | [Django](https://pypi.org/project/Django/) | Web application framework. |
32 | | [pyscopg2-binary](https://pypi.org/project/psycopg-binary/) | PostgreSQL database adapter for Python. |
33 | | [python-dotenv](https://pypi.org/project/python-dotenv/) | Read key-value pairs from .env file and set them as environment variables. In this sample app, those variables describe how to connect to the database locally.
This package is used in the [manage.py](./manage.py) file to load environment variables. |
34 | | [whitenoise](https://pypi.org/project/whitenoise/) | Static file serving for WSGI applications, used in the deployed app.
This package is used in the [azureproject/production.py](./azureproject/production.py) file, which configures production settings. |
35 | | [django-redis](https://pypi.org/project/django-redis/) | Redis cache backend for Django. |
36 |
37 | ## Run the sample
38 |
39 | This project has a [dev container configuration](.devcontainer/), which makes it easier to develop apps locally, deploy them to Azure, and monitor them. The easiest way to run this sample application is inside a GitHub codespace. Follow these steps:
40 |
41 | 1. Fork this repository to your account. For instructions, see [Fork a repo](https://docs.github.com/get-started/quickstart/fork-a-repo).
42 |
43 | 1. From the repository root of your fork, select **Code** > **Codespaces** > **+**.
44 |
45 | 1. In the codespace terminal, run the following commands:
46 |
47 | ```shell
48 | # Run database migrations
49 | python3 manage.py migrate
50 | # Start the development server
51 | python3 manage.py runserver
52 | ```
53 |
54 | 1. When you see the message `Your application running on port 8000 is available.`, click **Open in Browser**.
55 |
56 | ### Quick deploy
57 |
58 | This project is designed to work well with the [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview), which makes it easier to develop apps locally, deploy them to Azure, and monitor them.
59 |
60 | 🎥 Watch a deployment of the code in this [screencast](https://www.youtube.com/watch?v=JDlZ4TgPKYc).
61 | > Learn more about developing and deploying Django apps to Azure from Microsoft's comprehensive beginner series:
62 | > [Django for Beginners](https://www.youtube.com/playlist?list=PLlrxD0HtieHjHCQ0JB_RrhbQp_9ccJztr).
63 |
64 | Steps for deployment:
65 |
66 | 1. Sign up for a [free Azure account](https://azure.microsoft.com/free/)
67 | 2. Install the [Azure Dev CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd). (If you opened this repository in a Dev Container, it's already installed for you.)
68 | 3. Initialize a new `azd` environment:
69 |
70 | ```shell
71 | azd init
72 | ```
73 |
74 | It will prompt you to provide a name (like "django-app"), which will later be used in the name of the deployed resources.
75 |
76 | 4. Provision and deploy all the resources:
77 |
78 | ```shell
79 | azd up
80 | ```
81 |
82 | It will prompt you to login, pick a subscription, and provide a location (like "eastus"). Then it will provision the resources in your account and deploy the latest code. If you get an error with deployment, changing the location (like to "centralus") can help, as there may be availability constraints for some of the resources.
83 |
84 | 5. When `azd` has finished deploying, you'll see an endpoint URI in the command output. Visit that URI, and you should see the front page of the restaurant review app! 🎉 If you see an error, open the Azure Portal from the URL in the command output, navigate to the App Service, select Logstream, and check the logs for any errors.
85 |
86 | 
87 |
88 | 6. If you'd like to access `/admin`, you'll need a Django superuser. Navigate to the Azure Portal for the App Service, select SSH, and run this command:
89 |
90 | ```shell
91 | python3 manage.py createsuperuser
92 | ```
93 |
94 | 7. When you've made any changes to the app code, you can just run:
95 |
96 | ```shell
97 | azd deploy
98 | ```
99 |
100 | ## Getting help
101 |
102 | If you're working with this project and running into issues, please post in [Issues](/issues).
103 |
--------------------------------------------------------------------------------
/azure.yaml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
2 | # azure.yaml is an azd configuration file and isn't used by the sample application.
3 |
4 | name: python-app-service-postgresql-redis-infra
5 | metadata:
6 | template: python-app-service-postgresql-redis-infra@0.0.1-beta
7 | services:
8 | web:
9 | project: .
10 | language: py
11 | host: appservice
12 | hooks:
13 | postprovision:
14 | posix:
15 | shell: sh
16 | run: printf '\nApp Service app has the following connection settings:\n' && printf "$CONNECTION_SETTINGS" | jq -r '.[]' | sed 's/\(.*\)/\t- \1/' && printf "\nSee the settings in the portal:\033[1;36m $WEB_APP_CONFIG\n"
17 | interactive: true
18 | continueOnError: true
19 | windows:
20 | shell: pwsh
21 | run: Write-Host "`n`nApp Service app has the following connection settings:`n" $CONNECTION_SETTINGS | ConvertFrom-Json | ForEach-Object { Write-Host "\t- $_" }
22 | interactive: true
23 | continueOnError: true
24 | postdeploy:
25 | posix:
26 | shell: sh
27 | run: printf "Open SSH session to App Service container at:\033[1;36m $WEB_APP_SSH\033[0m\nStream App Service logs at:\033[1;36m $WEB_APP_LOG_STREAM\n"
28 | interactive: true
29 | continueOnError: true
30 | windows:
31 | shell: pwsh
32 | run: Write-Host "`n`nOpen SSH session to App Service container at:`n" $WEB_APP_SSH; Write-Host "Stream App Service logs at:`n" $WEB_APP_LOG_STREAM
33 | interactive: true
34 | continueOnError: true
--------------------------------------------------------------------------------
/azureproject/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/msdocs-django-postgresql-sample-app/75e65b9b94896fe6dae94d0c707b731800d17929/azureproject/__init__.py
--------------------------------------------------------------------------------
/azureproject/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for azureproject project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'azureproject.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/azureproject/production.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from .settings import * # noqa
4 | from .settings import BASE_DIR
5 |
6 | # Configure the domain name using the environment variable
7 | # that Azure automatically creates for us.
8 | ALLOWED_HOSTS = [os.environ['WEBSITE_HOSTNAME']] if 'WEBSITE_HOSTNAME' in os.environ else []
9 | CSRF_TRUSTED_ORIGINS = ['https://' + os.environ['WEBSITE_HOSTNAME']] if 'WEBSITE_HOSTNAME' in os.environ else []
10 | DEBUG = False
11 |
12 | # WhiteNoise configuration
13 | MIDDLEWARE = [
14 | 'django.middleware.security.SecurityMiddleware',
15 | # Add whitenoise middleware after the security middleware
16 | 'whitenoise.middleware.WhiteNoiseMiddleware',
17 | 'django.contrib.sessions.middleware.SessionMiddleware',
18 | 'django.middleware.common.CommonMiddleware',
19 | 'django.middleware.csrf.CsrfViewMiddleware',
20 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
21 | 'django.contrib.messages.middleware.MessageMiddleware',
22 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
23 | ]
24 |
25 | SESSION_ENGINE = "django.contrib.sessions.backends.cache"
26 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
27 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
28 |
29 | DATABASES = {
30 | 'default': {
31 | 'ENGINE': 'django.db.backends.postgresql',
32 | 'NAME': os.environ['AZURE_POSTGRESQL_NAME'],
33 | 'HOST': os.environ['AZURE_POSTGRESQL_HOST'],
34 | 'USER': os.environ['AZURE_POSTGRESQL_USER'],
35 | 'PASSWORD': os.environ['AZURE_POSTGRESQL_PASSWORD'],
36 | }
37 | }
38 |
39 | CACHES = {
40 | "default": {
41 | "BACKEND": "django_redis.cache.RedisCache",
42 | "LOCATION": os.environ['AZURE_REDIS_CONNECTIONSTRING'],
43 | "OPTIONS": {
44 | "CLIENT_CLASS": "django_redis.client.DefaultClient",
45 | "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
46 | },
47 | }
48 | }
--------------------------------------------------------------------------------
/azureproject/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for azureproject project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.0.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.0/ref/settings/
11 | """
12 |
13 | import os
14 | from pathlib import Path
15 |
16 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
17 | BASE_DIR = Path(__file__).resolve().parent.parent
18 |
19 |
20 | # Quick-start development settings - unsuitable for production
21 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
22 |
23 | # SECURITY WARNING: keep the secret key used in production secret!
24 | SECRET_KEY = os.getenv('SECRET_KEY')
25 |
26 | # SECURITY WARNING: don't run with debug turned on in production!
27 | DEBUG = True
28 |
29 | ALLOWED_HOSTS = []
30 |
31 | if 'CODESPACE_NAME' in os.environ:
32 | CSRF_TRUSTED_ORIGINS = [f'https://{os.getenv("CODESPACE_NAME")}-8000.{os.getenv("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN")}']
33 |
34 | # Application definition
35 |
36 | INSTALLED_APPS = [
37 | 'restaurant_review.apps.RestaurantReviewConfig',
38 | 'django.contrib.admin',
39 | 'django.contrib.auth',
40 | 'django.contrib.contenttypes',
41 | 'django.contrib.sessions',
42 | 'django.contrib.messages',
43 | 'django.contrib.staticfiles',
44 | ]
45 |
46 | MIDDLEWARE = [
47 | 'django.middleware.security.SecurityMiddleware',
48 | 'django.contrib.sessions.middleware.SessionMiddleware',
49 | 'django.middleware.common.CommonMiddleware',
50 | 'django.middleware.csrf.CsrfViewMiddleware',
51 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
52 | 'django.contrib.messages.middleware.MessageMiddleware',
53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
54 | ]
55 |
56 | SESSION_ENGINE = "django.contrib.sessions.backends.cache"
57 | ROOT_URLCONF = 'azureproject.urls'
58 |
59 | TEMPLATES = [
60 | {
61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
62 | 'DIRS': [],
63 | 'APP_DIRS': True,
64 | 'OPTIONS': {
65 | 'context_processors': [
66 | 'django.template.context_processors.debug',
67 | 'django.template.context_processors.request',
68 | 'django.contrib.auth.context_processors.auth',
69 | 'django.contrib.messages.context_processors.messages',
70 | ],
71 | },
72 | },
73 | ]
74 |
75 | WSGI_APPLICATION = 'azureproject.wsgi.application'
76 |
77 |
78 | # Database
79 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
80 |
81 | # To use sqllite as the database engine,
82 | # uncomment the following block and comment out the Postgres section below
83 |
84 | # DATABASES = {
85 | # 'default': {
86 | # 'ENGINE': 'django.db.backends.sqlite3',
87 | # 'NAME': BASE_DIR / 'db.sqlite3',
88 | # }
89 | # }
90 |
91 |
92 | # Configure Postgres database for local development
93 | # Set these environment variables in the .env file for this project.
94 | DATABASES = {
95 | 'default': {
96 | 'ENGINE': 'django.db.backends.postgresql',
97 | 'NAME': os.environ.get('DBNAME'),
98 | 'HOST': os.environ.get('DBHOST'),
99 | 'USER': os.environ.get('DBUSER'),
100 | 'PASSWORD': os.environ.get('DBPASS'),
101 | }
102 | }
103 |
104 |
105 | # Password validation
106 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
107 |
108 | AUTH_PASSWORD_VALIDATORS = [
109 | {
110 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
111 | },
112 | {
113 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
114 | },
115 | {
116 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
117 | },
118 | {
119 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
120 | },
121 | ]
122 |
123 | CACHES = {
124 | "default": {
125 | "BACKEND": "django_redis.cache.RedisCache",
126 | "LOCATION": os.environ.get('CACHELOCATION'),
127 | "OPTIONS": {
128 | "CLIENT_CLASS": "django_redis.client.DefaultClient",
129 | },
130 | }
131 | }
132 |
133 | # Internationalization
134 | # https://docs.djangoproject.com/en/4.0/topics/i18n/
135 |
136 | LANGUAGE_CODE = 'en-us'
137 |
138 | TIME_ZONE = 'UTC'
139 |
140 | USE_I18N = True
141 |
142 | USE_TZ = True
143 |
144 |
145 | # Static files (CSS, JavaScript, Images)
146 | # https://docs.djangoproject.com/en/4.0/howto/static-files/
147 |
148 | STATICFILES_DIRS = (str(BASE_DIR.joinpath('static')),)
149 | STATIC_URL = 'static/'
150 |
151 | # Default primary key field type
152 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
153 |
154 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
155 |
--------------------------------------------------------------------------------
/azureproject/urls.py:
--------------------------------------------------------------------------------
1 | """azureproject URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/4.0/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('', include('restaurant_review.urls')),
21 | path('admin/', admin.site.urls),
22 | ]
23 |
--------------------------------------------------------------------------------
/azureproject/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for azureproject 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/4.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | # Check for the WEBSITE_HOSTNAME environment variable to see if we are running in Azure Ap Service
15 | # If so, then load the settings from production.py
16 | settings_module = 'azureproject.production' if 'WEBSITE_HOSTNAME' in os.environ else 'azureproject.settings'
17 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
18 |
19 | application = get_wsgi_application()
20 |
--------------------------------------------------------------------------------
/infra/README.md:
--------------------------------------------------------------------------------
1 | # infra directory
2 |
3 | This `infra` directory contains azd files used for `azd provision` and isn't used by the sample application.
4 |
--------------------------------------------------------------------------------
/infra/main.bicep:
--------------------------------------------------------------------------------
1 | targetScope = 'subscription'
2 |
3 | @minLength(1)
4 | @maxLength(64)
5 | @description('Name which is used to generate a short unique hash for each resource')
6 | param name string
7 |
8 | @minLength(1)
9 | @description('Primary location for all resources')
10 | param location string
11 |
12 | @secure()
13 | @description('PostGreSQL Server administrator password')
14 | param databasePassword string
15 |
16 | @secure()
17 | @description('Django SECRET_KEY for securing signed data')
18 | param secretKey string
19 |
20 | param principalId string = ''
21 |
22 | var resourceToken = toLower(uniqueString(subscription().id, name, location))
23 | var tags = { 'azd-env-name': name }
24 |
25 | resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
26 | name: '${name}-rg'
27 | location: location
28 | tags: tags
29 | }
30 |
31 | module resources 'resources.bicep' = {
32 | name: 'resources'
33 | scope: resourceGroup
34 | params: {
35 | name: name
36 | location: location
37 | resourceToken: resourceToken
38 | tags: tags
39 | databasePassword: databasePassword
40 | principalId: principalId
41 | secretKey: secretKey
42 | }
43 | }
44 |
45 | output AZURE_LOCATION string = location
46 | output APPLICATIONINSIGHTS_CONNECTION_STRING string = resources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING
47 | output WEB_URI string = resources.outputs.WEB_URI
48 | output CONNECTION_SETTINGS array = resources.outputs.CONNECTION_SETTINGS
49 | output WEB_APP_LOG_STREAM string = resources.outputs.WEB_APP_LOG_STREAM
50 | output WEB_APP_SSH string = resources.outputs.WEB_APP_SSH
51 | output WEB_APP_CONFIG string = resources.outputs.WEB_APP_CONFIG
52 |
--------------------------------------------------------------------------------
/infra/main.parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "name": {
6 | "value": "${AZURE_ENV_NAME}"
7 | },
8 | "location": {
9 | "value": "${AZURE_LOCATION}"
10 | },
11 | "databasePassword": {
12 | "value": "$(secretOrRandomPassword)"
13 | },
14 | "principalId": {
15 | "value": "${AZURE_PRINCIPAL_ID}"
16 | },
17 | "secretKey": {
18 | "value": "$(secretOrRandomPassword)"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/infra/resources.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string
3 | param resourceToken string
4 | param principalId string
5 | param tags object
6 | @secure()
7 | param databasePassword string
8 | @secure()
9 | param secretKey string
10 | var appName = '${name}-${resourceToken}'
11 |
12 | var pgServerName = '${appName}-server'
13 |
14 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-01-01' = {
15 | name: '${appName}-vnet'
16 | location: location
17 | tags: tags
18 | properties: {
19 | addressSpace: {
20 | addressPrefixes: [
21 | '10.0.0.0/16'
22 | ]
23 | }
24 | subnets: [
25 | {
26 | name: 'database-subnet'
27 | properties: {
28 | addressPrefix: '10.0.0.0/24'
29 | delegations: [
30 | {
31 | name: '${appName}-subnet-delegation'
32 | properties: {
33 | serviceName: 'Microsoft.DBforPostgreSQL/flexibleServers'
34 | }
35 | }
36 | ]
37 | privateEndpointNetworkPolicies: 'Enabled'
38 | privateLinkServiceNetworkPolicies: 'Enabled'
39 | }
40 | }
41 | {
42 | name: 'webapp-subnet'
43 | properties: {
44 | addressPrefix: '10.0.1.0/24'
45 | delegations: [
46 | {
47 | name: 'dlg-appServices'
48 | properties: {
49 | serviceName: 'Microsoft.Web/serverFarms'
50 | }
51 | }
52 | ]
53 | }
54 | }
55 | {
56 | name: 'cache-subnet'
57 | properties:{
58 | addressPrefix: '10.0.2.0/24'
59 | privateEndpointNetworkPolicies: 'Disabled'
60 | }
61 | }
62 | {
63 | name: 'vault-subnet'
64 | properties: {
65 | addressPrefix: '10.0.3.0/24'
66 | privateEndpointNetworkPolicies: 'Disabled'
67 | }
68 | }
69 | ]
70 | }
71 | resource subnetForDb 'subnets' existing = {
72 | name: 'database-subnet'
73 | }
74 | resource subnetForVault 'subnets' existing = {
75 | name: 'vault-subnet'
76 | }
77 | resource subnetForApp 'subnets' existing = {
78 | name: 'webapp-subnet'
79 | }
80 | resource subnetForCache 'subnets' existing = {
81 | name: 'cache-subnet'
82 | }
83 | }
84 |
85 | // Resources needed to secure Key Vault behind a private endpoint
86 | resource privateDnsZoneKeyVault 'Microsoft.Network/privateDnsZones@2020-06-01' = {
87 | name: 'privatelink.vaultcore.azure.net'
88 | location: 'global'
89 | resource vnetLink 'virtualNetworkLinks@2020-06-01' = {
90 | location: 'global'
91 | name: '${appName}-vaultlink'
92 | properties: {
93 | virtualNetwork: {
94 | id: virtualNetwork.id
95 | }
96 | registrationEnabled: false
97 | }
98 | }
99 | }
100 | resource vaultPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = {
101 | name: '${appName}-vault-privateEndpoint'
102 | location: location
103 | properties: {
104 | subnet: {
105 | id: virtualNetwork::subnetForVault.id
106 | }
107 | privateLinkServiceConnections: [
108 | {
109 | name: '${appName}-vault-privateEndpoint'
110 | properties: {
111 | privateLinkServiceId: keyVault.id
112 | groupIds: ['vault']
113 | }
114 | }
115 | ]
116 | }
117 | resource privateDnsZoneGroup 'privateDnsZoneGroups@2024-01-01' = {
118 | name: 'default'
119 | properties: {
120 | privateDnsZoneConfigs: [
121 | {
122 | name: 'vault-config'
123 | properties: {
124 | privateDnsZoneId: privateDnsZoneKeyVault.id
125 | }
126 | }
127 | ]
128 | }
129 | }
130 | }
131 |
132 | resource privateDnsZoneDB 'Microsoft.Network/privateDnsZones@2024-06-01' = {
133 | name: '${pgServerName}.private.postgres.database.azure.com'
134 | location: 'global'
135 | tags: tags
136 | dependsOn: [
137 | virtualNetwork
138 | ]
139 | resource privateDnsZoneLinkDB 'virtualNetworkLinks@2024-06-01' = {
140 | name: '${appName}-dblink'
141 | location: 'global'
142 | properties: {
143 | virtualNetwork: {
144 | id: virtualNetwork.id
145 | }
146 | registrationEnabled: false
147 | }
148 | }
149 | }
150 |
151 | // Resources needed to secure Redis Cache behind a private endpoint
152 | resource cachePrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-03-01' = {
153 | name: '${appName}-cache-privateEndpoint'
154 | location: location
155 | properties: {
156 | subnet: {
157 | id: virtualNetwork::subnetForCache.id
158 | }
159 | privateLinkServiceConnections: [
160 | {
161 | name: '${appName}-cache-privateEndpoint'
162 | properties: {
163 | privateLinkServiceId: redisCache.id
164 | groupIds: ['redisCache']
165 | }
166 | }
167 | ]
168 | }
169 | resource privateDnsZoneGroup 'privateDnsZoneGroups' = {
170 | name: 'default'
171 | properties: {
172 | privateDnsZoneConfigs: [
173 | {
174 | name: 'cache-config'
175 | properties: {
176 | privateDnsZoneId: privateDnsZoneCache.id
177 | }
178 | }
179 | ]
180 | }
181 | }
182 | }
183 | resource privateDnsZoneCache 'Microsoft.Network/privateDnsZones@2024-06-01' = {
184 | name: 'privatelink.redis.cache.windows.net'
185 | location: 'global'
186 | dependsOn: [
187 | virtualNetwork
188 | ]
189 | resource privateDnsZoneLinkCache 'virtualNetworkLinks@2020-06-01' = {
190 | name: '${appName}-cachelink'
191 | location: 'global'
192 | properties: {
193 | virtualNetwork: {
194 | id: virtualNetwork.id
195 | }
196 | registrationEnabled: false
197 | }
198 | }
199 | }
200 |
201 | // The Key Vault is used to manage SQL database and redis secrets.
202 | // Current user has the admin permissions to configure key vault secrets, but by default doesn't have the permissions to read them.
203 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
204 | name: '${take(replace(appName, '-', ''), 17)}-vault'
205 | location: location
206 | properties: {
207 | enableRbacAuthorization: true
208 | tenantId: subscription().tenantId
209 | sku: { family: 'A', name: 'standard' }
210 | // Only allow requests from the private endpoint in the VNET.
211 | publicNetworkAccess: 'Disabled' // To see the secret in the portal, change to 'Enabled'
212 | networkAcls: {
213 | defaultAction: 'Deny' // To see the secret in the portal, change to 'Allow'
214 | bypass: 'None'
215 | }
216 | }
217 | }
218 |
219 | // Grant the current user with key vault secret user role permissions over the key vault. This lets you inspect the secrets, such as in the portal
220 | // If you remove this section, you can't read the key vault secrets, but the app still has access with its managed identity.
221 | resource keyVaultSecretUserRoleRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
222 | scope: subscription()
223 | name: '4633458b-17de-408a-b874-0445c86b69e6' // The built-in Key Vault Secret User role
224 | }
225 | resource keyVaultSecretUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = {
226 | scope: keyVault
227 | name: guid(resourceGroup().id, principalId, keyVaultSecretUserRoleRoleDefinition.id)
228 | properties: {
229 | roleDefinitionId: keyVaultSecretUserRoleRoleDefinition.id
230 | principalId: principalId
231 | principalType: 'User'
232 | }
233 | }
234 |
235 | resource dbserver 'Microsoft.DBforPostgreSQL/flexibleServers@2022-01-20-preview' = {
236 | location: location
237 | tags: tags
238 | name: pgServerName
239 | sku: {
240 | name: 'Standard_B1ms'
241 | tier: 'Burstable'
242 | }
243 | properties: {
244 | version: '12'
245 | administratorLogin: 'postgresadmin'
246 | administratorLoginPassword: databasePassword
247 | storage: {
248 | storageSizeGB: 128
249 | }
250 | backup: {
251 | backupRetentionDays: 7
252 | geoRedundantBackup: 'Disabled'
253 | }
254 | network: {
255 | delegatedSubnetResourceId: virtualNetwork::subnetForDb.id
256 | privateDnsZoneArmResourceId: privateDnsZoneDB.id
257 | }
258 | highAvailability: {
259 | mode: 'Disabled'
260 | }
261 | maintenanceWindow: {
262 | customWindow: 'Disabled'
263 | dayOfWeek: 0
264 | startHour: 0
265 | startMinute: 0
266 | }
267 | }
268 |
269 | resource db 'databases@2024-08-01' = {
270 | name: '${appName}-database'
271 | }
272 | dependsOn: [
273 | privateDnsZoneDB::privateDnsZoneLinkDB
274 | ]
275 | }
276 |
277 | // The Redis cache is configured to the minimum pricing tier
278 | resource redisCache 'Microsoft.Cache/redis@2024-11-01' = {
279 | name: '${appName}-cache'
280 | location: location
281 | properties: {
282 | sku: {
283 | name: 'Basic'
284 | family: 'C'
285 | capacity: 0
286 | }
287 | redisConfiguration: {}
288 | enableNonSslPort: false
289 | redisVersion: '6'
290 | publicNetworkAccess: 'Disabled'
291 | }
292 | }
293 |
294 | // The App Service plan is configured to the B1 pricing tier
295 | resource appServicePlan 'Microsoft.Web/serverfarms@2024-04-01' = {
296 | name: '${appName}-plan'
297 | location: location
298 | kind: 'linux'
299 | properties: {
300 | reserved: true
301 | }
302 | sku: {
303 | name: 'B1'
304 | }
305 | }
306 |
307 | resource web 'Microsoft.Web/sites@2024-04-01' = {
308 | name: appName
309 | location: location
310 | tags: union(tags, { 'azd-service-name': 'web' }) // Needed by AZD
311 | properties: {
312 | siteConfig: {
313 | linuxFxVersion: 'PYTHON|3.12' // Set to Python 3.12
314 | ftpsState: 'Disabled'
315 | appCommandLine: 'startup.sh'
316 | minTlsVersion: '1.2'
317 | }
318 | serverFarmId: appServicePlan.id
319 | httpsOnly: true
320 | }
321 | identity: {
322 | type: 'SystemAssigned'
323 | }
324 |
325 | // For app setting configuration see the appsettings resource
326 |
327 | // Disable basic authentication for FTP and SCM
328 | resource ftp 'basicPublishingCredentialsPolicies@2023-12-01' = {
329 | name: 'ftp'
330 | properties: {
331 | allow: false
332 | }
333 | }
334 | resource scm 'basicPublishingCredentialsPolicies@2023-12-01' = {
335 | name: 'scm'
336 | properties: {
337 | allow: false
338 | }
339 | }
340 |
341 | // Enable App Service native logs
342 | resource logs 'config' = {
343 | name: 'logs'
344 | properties: {
345 | applicationLogs: {
346 | fileSystem: {
347 | level: 'Verbose'
348 | }
349 | }
350 | detailedErrorMessages: {
351 | enabled: true
352 | }
353 | failedRequestsTracing: {
354 | enabled: true
355 | }
356 | httpLogs: {
357 | fileSystem: {
358 | enabled: true
359 | retentionInDays: 1
360 | retentionInMb: 35
361 | }
362 | }
363 | }
364 | }
365 |
366 | // Enable VNET integration
367 | resource webappVnetConfig 'networkConfig' = {
368 | name: 'virtualNetwork'
369 | properties: {
370 | subnetResourceId: virtualNetwork::subnetForApp.id
371 | }
372 | }
373 |
374 | dependsOn: [ virtualNetwork ]
375 | }
376 |
377 | // Service Connector from the app to the key vault, which generates the connection settings for the App Service app
378 | // The application code doesn't make any direct connections to the key vault, but the setup expedites the managed identity access
379 | // so that the cache connector can be configured with key vault references.
380 | resource vaultConnector 'Microsoft.ServiceLinker/linkers@2024-04-01' = {
381 | scope: web
382 | name: 'vaultConnector'
383 | properties: {
384 | clientType: 'python'
385 | targetService: {
386 | type: 'AzureResource'
387 | id: keyVault.id
388 | }
389 | authInfo: {
390 | authType: 'systemAssignedIdentity' // Use a system-assigned managed identity. No password is used.
391 | }
392 | vNetSolution: {
393 | type: 'privateLink'
394 | }
395 | }
396 | dependsOn: [
397 | vaultPrivateEndpoint
398 | ]
399 | }
400 |
401 | // Connector to the PostgreSQL database, which generates the connection string for the App Service app
402 | resource dbConnector 'Microsoft.ServiceLinker/linkers@2024-04-01' = {
403 | scope: web
404 | name: 'defaultConnector'
405 | properties: {
406 | targetService: {
407 | type: 'AzureResource'
408 | id: dbserver::db.id
409 | }
410 | authInfo: {
411 | authType: 'secret'
412 | name: 'postgresadmin'
413 | secretInfo: {
414 | secretType: 'rawValue'
415 | value: databasePassword
416 | }
417 | }
418 | secretStore: {
419 | keyVaultId: keyVault.id // Configure secrets as key vault references. No secret is exposed in App Service.
420 | }
421 | clientType: 'django'
422 | }
423 | }
424 |
425 | // Service Connector from the app to the cache, which generates an app setting for the App Service app
426 | resource cacheConnector 'Microsoft.ServiceLinker/linkers@2024-04-01' = {
427 | scope: web
428 | name: 'RedisConnector'
429 | properties: {
430 | clientType: 'python'
431 | targetService: {
432 | type: 'AzureResource'
433 | id: resourceId('Microsoft.Cache/Redis/Databases', redisCache.name, '0')
434 | }
435 | authInfo: {
436 | authType: 'accessKey'
437 | }
438 | secretStore: {
439 | keyVaultId: keyVault.id // Configure secrets as key vault references. No secret is exposed in App Service.
440 | }
441 | vNetSolution: {
442 | type: 'privateLink'
443 |
444 | }
445 | }
446 | dependsOn: [
447 | cachePrivateEndpoint
448 | ]
449 | }
450 |
451 | resource webdiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
452 | name: 'AllLogs'
453 | scope: web
454 | properties: {
455 | workspaceId: logAnalyticsWorkspace.id
456 | logs: [
457 | {
458 | category: 'AppServiceHTTPLogs'
459 | enabled: true
460 | }
461 | {
462 | category: 'AppServiceConsoleLogs'
463 | enabled: true
464 | }
465 | {
466 | category: 'AppServiceAppLogs'
467 | enabled: true
468 | }
469 | {
470 | category: 'AppServiceAuditLogs'
471 | enabled: true
472 | }
473 | {
474 | category: 'AppServiceIPSecAuditLogs'
475 | enabled: true
476 | }
477 | {
478 | category: 'AppServicePlatformLogs'
479 | enabled: true
480 | }
481 | ]
482 | metrics: [
483 | {
484 | category: 'AllMetrics'
485 | enabled: true
486 | }
487 | ]
488 | }
489 | }
490 |
491 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
492 | name: '${appName}-workspace'
493 | location: location
494 | tags: tags
495 | properties: any({
496 | retentionInDays: 30
497 | features: {
498 | searchVersion: 1
499 | }
500 | sku: {
501 | name: 'PerGB2018'
502 | }
503 | })
504 | }
505 |
506 | module applicationInsightsResources 'appinsights.bicep' = {
507 | name: 'applicationinsights-resources'
508 | params: {
509 | prefix: appName
510 | location: location
511 | tags: tags
512 | workspaceId: logAnalyticsWorkspace.id
513 | }
514 | }
515 |
516 | func checkAndFormatSecrets(config object) string => config.configType == 'KeyVaultSecret' ? '@Microsoft.KeyVault(SecretUri=${config.value})' : config.value
517 |
518 | // Add the app settings, by merging them with the ones created by the service connectors
519 | var aggregatedAppSettings = union(
520 | reduce(vaultConnector.listConfigurations().configurations, {}, (cur, next) => union(cur, { '${next.name}': checkAndFormatSecrets(next) })),
521 | reduce(dbConnector.listConfigurations().configurations, {}, (cur, next) => union(cur, { '${next.name}': checkAndFormatSecrets(next) })),
522 | reduce(cacheConnector.listConfigurations().configurations, {}, (cur, next) => union(cur, { '${next.name}': checkAndFormatSecrets(next) })),
523 | {
524 | SCM_DO_BUILD_DURING_DEPLOYMENT: 'true'
525 | FLASK_DEBUG: 'False'
526 | SECRET_KEY: secretKey
527 | // Add other app settings here, for example:
528 | // 'FOO': 'BAR'
529 | }
530 | )
531 | resource appsettings 'Microsoft.Web/sites/config@2024-04-01' = {
532 | name: 'appsettings'
533 | parent: web
534 | properties: aggregatedAppSettings
535 | }
536 | // Why is this needed?
537 | // The service connectors automatically add necessary respective app settings to the App Service app. However, if you configure a separate
538 | // set of app settings in a config/appsettings resource, expecting a cummulative effect, the app settings actually overwrite the ones
539 | // created by the service connectors, and the service connectors don't recreate the app settings after the first run. This configuration
540 | // is a workaround to ensure that the app settings are aggregated correctly and consistent across multiple deployments.
541 |
542 | output WEB_URI string = 'https://${web.properties.defaultHostName}'
543 | output CONNECTION_SETTINGS array = map(concat(dbConnector.listConfigurations().configurations, cacheConnector.listConfigurations().configurations, vaultConnector.listConfigurations().configurations), config => config.name)
544 | output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsightsResources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING
545 | output WEB_APP_LOG_STREAM string = format('https://portal.azure.com/#@/resource{0}/logStream', web.id)
546 | output WEB_APP_SSH string = format('https://{0}.scm.azurewebsites.net/webssh/host', web.name)
547 | output WEB_APP_CONFIG string = format('https://portal.azure.com/#@/resource{0}/environmentVariablesAppSettings', web.id)
548 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 | from dotenv import load_dotenv
7 |
8 |
9 | def main():
10 | """Run administrative tasks."""
11 | # If WEBSITE_HOSTNAME is defined as an environment variable, then we're running on Azure App Service
12 |
13 | # Only for Local Development - Load environment variables from the .env file
14 | if 'WEBSITE_HOSTNAME' not in os.environ:
15 | print("Loading environment variables for .env file")
16 | load_dotenv('./.env')
17 |
18 | # When running on Azure App Service you should use the production settings.
19 | settings_module = "azureproject.production" if 'WEBSITE_HOSTNAME' in os.environ else 'azureproject.settings'
20 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
21 |
22 | try:
23 | from django.core.management import execute_from_command_line
24 | except ImportError as exc:
25 | raise ImportError(
26 | "Couldn't import Django. Are you sure it's installed and "
27 | "available on your PYTHONPATH environment variable? Did you "
28 | "forget to activate a virtual environment?"
29 | ) from exc
30 | execute_from_command_line(sys.argv)
31 |
32 |
33 | if __name__ == '__main__':
34 | main()
35 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==5.1.6
2 | psycopg2-binary==2.9.10
3 | python-dotenv==1.0.1
4 | whitenoise==6.9.0
5 | django-redis==5.4.0
6 |
--------------------------------------------------------------------------------
/restaurant_review/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/msdocs-django-postgresql-sample-app/75e65b9b94896fe6dae94d0c707b731800d17929/restaurant_review/__init__.py
--------------------------------------------------------------------------------
/restaurant_review/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Restaurant, Review
4 |
5 | # Register your models here.
6 |
7 | admin.site.register(Restaurant)
8 | admin.site.register(Review)
9 |
--------------------------------------------------------------------------------
/restaurant_review/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class RestaurantReviewConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'restaurant_review'
7 |
--------------------------------------------------------------------------------
/restaurant_review/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0.2 on 2022-02-08 04:34
2 |
3 | import django.core.validators
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='Restaurant',
18 | fields=[
19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('name', models.CharField(max_length=50)),
21 | ('street_address', models.CharField(max_length=50)),
22 | ('description', models.CharField(max_length=250)),
23 | ],
24 | ),
25 | migrations.CreateModel(
26 | name='Review',
27 | fields=[
28 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29 | ('user_name', models.CharField(max_length=20)),
30 | ('rating', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
31 | ('review_text', models.CharField(max_length=500)),
32 | ('review_date', models.DateTimeField(verbose_name='review date')),
33 | ('restaurant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='restaurant_review.restaurant')),
34 | ],
35 | ),
36 | ]
37 |
--------------------------------------------------------------------------------
/restaurant_review/migrations/0002_alter_review_rating.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.3 on 2022-11-04 21:25
2 |
3 | import django.core.validators
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("restaurant_review", "0001_initial"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="review",
16 | name="rating",
17 | field=models.IntegerField(
18 | validators=[
19 | django.core.validators.MinValueValidator(1),
20 | django.core.validators.MaxValueValidator(5),
21 | ]
22 | ),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/restaurant_review/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/msdocs-django-postgresql-sample-app/75e65b9b94896fe6dae94d0c707b731800d17929/restaurant_review/migrations/__init__.py
--------------------------------------------------------------------------------
/restaurant_review/models.py:
--------------------------------------------------------------------------------
1 | from django.core.validators import MaxValueValidator, MinValueValidator
2 | from django.db import models
3 |
4 | # Create your models here.
5 |
6 | class Restaurant(models.Model):
7 | name = models.CharField(max_length=50)
8 | street_address = models.CharField(max_length=50)
9 | description = models.CharField(max_length=250)
10 |
11 | def __str__(self):
12 | return self.name
13 |
14 |
15 | class Review(models.Model):
16 | restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
17 | user_name = models.CharField(max_length=20)
18 | rating = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
19 | review_text = models.CharField(max_length=500)
20 | review_date = models.DateTimeField('review date')
21 |
22 | def __str__(self):
23 | return f"{self.restaurant.name} ({self.review_date:%x})"
--------------------------------------------------------------------------------
/restaurant_review/templates/500.html:
--------------------------------------------------------------------------------
1 |
2 |
` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `
` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`