Repository: cookiecutter/cookiecutter-django

Issue:
VS Code Dev Container setup
## Description

Hi,

I spent some time today setting up vs code with "dev container" to work with cookiecutter-django. At the end the dev experience is pretty good as you can code seemlessly directly from within the "django" container with the python interpreter linting and everything coming from the container.

I will share here my setup but it can definetly be improved and added to `cookiecutter-django`. I did the following things:
- install git, zsh and ohmyzsh inside the container when vs code is attaching to it.
- add python, and intellicode extensions when vs code attach to the container.
- setup linting, formatting and testing to work out of the box when attaching to the container.
- override the docker-compose to add a custom .zshrc volume which export the missing ENV variable.

## Rationale

Better DX on VS Code using dev container.

## Use case(s) / visualization(s)

Once the project created, I just added a `.devcontainer` folder at the root of my project with the following files:

A `devcontainer.json` to set what happens when vs code is attaching to the container:
```javscript
// devcontainer.json
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.112.0/containers/docker-existing-docker-compose
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
{
  "name": "Existing Docker Compose (Extend)",
  // Update the 'dockerComposeFile' list if you have more compose files or use different names.
  // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
  "dockerComposeFile": ["../local.yml", "./docker-compose.override.yml"],
  // The 'service' property is the name of the service for the container that VS Code should
  // use. Update this value and .devcontainer/docker-compose.yml to the real service name.
  "service": "django",
  // The optional 'workspaceFolder' property is the path VS Code should open by default when
  // connected. This is typically a file mount in .devcontainer/docker-compose.yml
  "workspaceFolder": "/app",
  // Set *default* container specific settings.json values on container create.
  "settings": {
    "terminal.integrated.shell.linux": "/bin/zsh",
    "[python]": {
      "editor.rulers": [120]
    },
    "editor.formatOnSave": true,
    "python.pythonPath": "/usr/local/bin/python",
    "python.linting.pylintEnabled": true,
    "python.formatting.provider": "black",
    "python.linting.flake8Enabled": true,
    "python.linting.mypyEnabled": true
  },
  // Add the IDs of extensions you want installed when the container is created.
  "extensions": [
    "ms-python.python",
    "visualstudioexptteam.vscodeintellicode",
    "esbenp.prettier-vscode"
  ],
  // Uncomment the next line if you want start specific services in your Docker Compose config.
  // "runServices": [],
  // Uncomment the next line if you want to keep your containers running after VS Code shuts down.
  // "shutdownAction": "none",
  // Uncomment the next line to run commands after the container is created - for example installing git.
  "postCreateCommand": "apt-get update && apt-get install -y git zsh wget && wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true"
  // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
  // "remoteUser": "vscode"
}
```
a new `.zshrc` for adding the missing env variable to the shell:

```zsh
# If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:/usr/local/bin:$PATH

# Path to your oh-my-zsh installation.
export ZSH="/root/.oh-my-zsh"

# Set name of the theme to load --- if set to "random", it will
# load a random theme each time oh-my-zsh is loaded, in which case,
# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
ZSH_THEME="robbyrussell"

# Set list of themes to pick from when loading at random
# Setting this variable when ZSH_THEME=random will cause zsh to load
# a theme from this variable instead of looking in ~/.oh-my-zsh/themes/
# If set to an empty array, this variable will have no effect.
# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )

# Uncomment the following line to use case-sensitive completion.
# CASE_SENSITIVE="true"

# Uncomment the following line to use hyphen-insensitive completion.
# Case-sensitive completion must be off. _ and - will be interchangeable.
# HYPHEN_INSENSITIVE="true"

# Uncomment the following line to disable bi-weekly auto-update checks.
# DISABLE_AUTO_UPDATE="true"

# Uncomment the following line to automatically update without prompting.
# DISABLE_UPDATE_PROMPT="true"

# Uncomment the following line to change how often to auto-update (in days).
# export UPDATE_ZSH_DAYS=13

# Uncomment the following line if pasting URLs and other text is messed up.
# DISABLE_MAGIC_FUNCTIONS=true

# Uncomment the following line to disable colors in ls.
# DISABLE_LS_COLORS="true"

# Uncomment the following line to disable auto-setting terminal title.
# DISABLE_AUTO_TITLE="false"

# Uncomment the following line to enable command auto-correction.
# ENABLE_CORRECTION="true"

# Uncomment the following line to display red dots whilst waiting for completion.
# COMPLETION_WAITING_DOTS="true"

# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY="true"

# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# You can set one of the optional three formats:
# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# or set a custom format using the strftime function format specifications,
# see 'man strftime' for details.
# HIST_STAMPS="mm/dd/yyyy"

# Would you like to use another custom folder than $ZSH/custom?
# ZSH_CUSTOM=/path/to/new-custom-folder

# Which plugins would you like to load?
# Standard plugins can be found in ~/.oh-my-zsh/plugins/*
# Custom plugins may be added to ~/.oh-my-zsh/custom/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(git)

source $ZSH/oh-my-zsh.sh

# User configuration

# export MANPATH="/usr/local/man:$MANPATH"

# You may need to manually set your language environment
# export LANG=en_US.UTF-8

# Preferred editor for local and remote sessions
# if [[ -n $SSH_CONNECTION ]]; then
#   export EDITOR='vim'
# else
#   export EDITOR='mvim'
# fi

# Compilation flags
# export ARCHFLAGS="-arch x86_64"

# Set personal aliases, overriding those provided by oh-my-zsh libs,
# plugins, and themes. Aliases can be placed here, though oh-my-zsh
# users are encouraged to define aliases within the ZSH_CUSTOM folder.
# For a full list of active aliases, run `alias`.
#
# Example aliases
# alias zshconfig="mate ~/.zshrc"
# alias ohmyzsh="mate ~/.oh-my-zsh"

# env variable for django cookiecutter
export CELERY_BROKER_URL="${REDIS_URL}"

if [ -z "${POSTGRES_USER}" ]; then
    base_postgres_image_default_user='postgres'
    export POSTGRES_USER="${base_postgres_image_default_user}"
fi
export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
```
and finally the `docker-compose.override.yml`:

```yml
version: "3.0"

services:
  django:
    volumes:
      - ./.devcontainer/.zshrc:/root/.zshrc
```

Let me know what you think about this setup


Discussion:
@grll Thank you for putting this together. I haven't used devcontainers before, and I was wondering how is the development experience improved.
there are several development improvement by using this approach. The fact that the IDE is within the container means that you don't need to work with potentially slow mounted volumes, most importantly all the IDE related experience can now be encapsulated within the container and easily shared with your colleagues on a project bases (prettier, pylint, the python interpreter ...) so you not only share the code for the project but also the complete environment to work with the code
@grll nice setup!

What is your experience with debugging in your setup? 

Have you tried the new debugpy integration in the Python extension?
I am currently struggling to make it work. If you have any tips could you share them please?

Thanks!
If you have problems with the missing `DATABASE_URL`, add this to `devcontainer.json`
```
{
...
  // set ENV variables such as DATABASE_URL
  "postCreateCommand": "echo 'source /entrypoint' >> ~/.bashrc",
}
```
This ensures that the environment variables from `/entrypoint` get set every time you start an interactive Bash session. However, you need to remove some shell options, so a session does not close whenever a program exists with a non-zero exit code.

```
# production/django/entrypoint
# comment this out or remove them
# set -o errexit
# set -o pipefail
# set -o nounset
```
I appreciate [this description](https://github.com/pydanny/cookiecutter-django/issues/2580#issue-610082041)! 

My configuration was getting a database connection error using zsh. I fixed it with this change to `devcontainer.json`:
```diff
-  "postCreateCommand": "apt-get update && apt-get install -y git zsh wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true"
+  "postCreateCommand": "wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true && echo 'source /entrypoint' >> ~/.zshrc"
```
and installing `git` and `wget` in my debug Dockerfile.

Also, I had a problem using `docker-compose.override.yml` (can a file be mounted?) so I removed it and the `.devcontainer/.zshrc` file. I suppose this means I'll need to edit the container's `/root/.zshrc` with any desired changes.
I finally got things working for debugging View code in VS Code. A few notes:

- No need to use debugpy, I changed by debug /start script to start runserver_plus on port 8001
- Set up a launch.json configuration to start runserver_plus on port 8000 – this works for starting a View code debug session
- Had to comment out the `**/compose*` line that VS Code added in .dockerignore
- Committed two .vscode config files to my repo: `launch.json` and `settings.json`

It seems that cookiecutter-django could add some set of these files to provide a VS Code friendly setup.
+1 for out-of-the-box optional VS Code setup.
would you accept a pull request for this issue..?
> would you accept a pull request for this issue..?

Yes sure
Any luck?

Resolving patch:
diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py
--- a/hooks/post_gen_project.py
+++ b/hooks/post_gen_project.py
@@ -74,12 +74,13 @@ def remove_pycharm_files():
 
 
 def remove_docker_files():
+    shutil.rmtree(".devcontainer")
     shutil.rmtree("compose")
 
     file_names = ["local.yml", "production.yml", ".dockerignore"]
     for file_name in file_names:
         os.remove(file_name)
-    if "{{ cookiecutter.use_pycharm }}".lower() == "y":
+    if "{{ cookiecutter.editor }}".lower() == "PyCharm":
         file_names = ["docker_compose_up_django.xml", "docker_compose_up_docs.xml"]
         for file_name in file_names:
             os.remove(os.path.join(".idea", "runConfigurations", file_name))
@@ -427,7 +428,7 @@ def main():
     if "{{ cookiecutter.username_type }}" == "username":
         remove_custom_user_manager_files()
 
-    if "{{ cookiecutter.use_pycharm }}".lower() == "n":
+    if "{{ cookiecutter.editor }}".lower() != "PyCharm":
         remove_pycharm_files()
 
     if "{{ cookiecutter.use_docker }}".lower() == "y":
@@ -445,8 +446,8 @@ def main():
         if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y":
             print(
                 INFO + ".env(s) are only utilized when Docker Compose and/or "
-                "Heroku support is enabled so keeping them does not "
-                "make sense given your current setup." + TERMINATOR
+                "Heroku support is enabled so keeping them does not make sense "
+                "given your current setup." + TERMINATOR
             )
         remove_envs_and_associated_files()
     else:


Test patch:
diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py
--- a/tests/test_cookiecutter_generation.py
+++ b/tests/test_cookiecutter_generation.py
@@ -52,8 +52,9 @@ def context():
     {"open_source_license": "Not open source"},
     {"windows": "y"},
     {"windows": "n"},
-    {"use_pycharm": "y"},
-    {"use_pycharm": "n"},
+    {"editor": "None"},
+    {"editor": "PyCharm"},
+    {"editor": "VS Code"},
     {"use_docker": "y"},
     {"use_docker": "n"},
     {"postgresql_version": "14"},
@@ -373,14 +374,15 @@ def test_error_if_incompatible(cookies, context, invalid_context):
 
 
 @pytest.mark.parametrize(
-    ["use_pycharm", "pycharm_docs_exist"],
+    ["editor", "pycharm_docs_exist"],
     [
-        ("n", False),
-        ("y", True),
+        ("None", False),
+        ("PyCharm", True),
+        ("VS Code", False),
     ],
 )
-def test_pycharm_docs_removed(cookies, context, use_pycharm, pycharm_docs_exist):
-    context.update({"use_pycharm": use_pycharm})
+def test_pycharm_docs_removed(cookies, context, editor, pycharm_docs_exist):
+    context.update({"editor": editor})
     result = cookies.bake(extra_context=context)
 
     with open(f"{result.project_path}/docs/index.rst") as f: