Repository: cookiecutter/cookiecutter-django

Issue:
Add Blob Storage as an option for static and media files
## Description

[//]: # (What's it you're proposing? How should it be implemented?)
Can I add Blob Storage (Microsoft Azure) as an option to serve static and media files?



## Rationale

[//]: # (Why should this feature be implemented?)
I always use Amazon S3 for serving static and media files, but now I have a client that has a preference for Blob Storage (Microsoft Azure), so I thought about adding this as an option because it can be helpful for other developers.



## Use case(s) / visualization(s)

[//]: # ("Better to see something once than to hear about it a thousand times.")
In my case, I have to use it because a client has a preference for it. But maybe someone feels more comfortable using Azure instead of AWS or Google Cloud.

I can make the PR if you all think it's ok.



Discussion:
Not sure how I didn't get around to reply, but yes, this would be a welcomed addition. PR welcome 👍🏻 
@Jeanluis019: I know it has been a while but are you still interested in this? We could collaborate on a PR.

I am doing something naive like looking at GCP (for an example) and seeing what could need changing. It doesn't seem like much at all.


````
$ grep -riI gcp ./*
./config/settings/production.py:GS_BUCKET_NAME = env("DJANGO_GCP_STORAGE_BUCKET_NAME")

$ grep -riI google ./*
./config/settings/production.py:STATICFILES_STORAGE = "project.utils.storages.StaticRootGoogleCloudStorage"
./config/settings/production.py:COLLECTFAST_STRATEGY = "collectfast.strategies.gcloud.GoogleCloudStrategy"
./config/settings/production.py:STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/"
./config/settings/production.py:DEFAULT_FILE_STORAGE = "project.utils.storages.MediaRootGoogleCloudStorage"
./config/settings/production.py:MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/"
./docs/howto.rst:Numpy or Google style docstrings will be picked up from project files and available for documentation. See the `Napoleon <https://sphinxcontrib-napoleon.readthedocs.io/en/latest/>`_ extension for details.
./project/utils/storages.py:from storages.backends.gcloud import GoogleCloudStorage
./project/utils/storages.py:class StaticRootGoogleCloudStorage(GoogleCloudStorage):
./project/utils/storages.py:class MediaRootGoogleCloudStorage(GoogleCloudStorage):
./requirements/production.txt:django-storages[google]==1.12.3  # https://github.com/jschneier/django-storages
````

Resolving patch:
diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py
--- a/hooks/pre_gen_project.py
+++ b/hooks/pre_gen_project.py
@@ -72,11 +72,8 @@
     sys.exit(1)
 
 if (
-    "{{ cookiecutter.cloud_provider }}" == "GCP"
-    and "{{ cookiecutter.mail_service }}" == "Amazon SES"
-) or (
-    "{{ cookiecutter.cloud_provider }}" == "None"
-    and "{{ cookiecutter.mail_service }}" == "Amazon SES"
+    "{{ cookiecutter.mail_service }}" == "Amazon SES"
+    and "{{ cookiecutter.cloud_provider }}" != "AWS"
 ):
     print(
         "You should either use AWS or select a different "
diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py
--- a/{{cookiecutter.project_slug}}/config/settings/production.py
+++ b/{{cookiecutter.project_slug}}/config/settings/production.py
@@ -100,6 +100,10 @@
 {% elif cookiecutter.cloud_provider == 'GCP' %}
 GS_BUCKET_NAME = env("DJANGO_GCP_STORAGE_BUCKET_NAME")
 GS_DEFAULT_ACL = "publicRead"
+{% elif cookiecutter.cloud_provider == 'Azure' %}
+AZURE_ACCOUNT_KEY = env("DJANGO_AZURE_ACCOUNT_KEY")
+AZURE_ACCOUNT_NAME = env("DJANGO_AZURE_ACCOUNT_NAME")
+AZURE_CONTAINER = env("DJANGO_AZURE_CONTAINER_NAME")
 {% endif -%}
 
 {% if cookiecutter.cloud_provider != 'None' or cookiecutter.use_whitenoise == 'y' -%}
@@ -116,6 +120,9 @@
 STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticRootGoogleCloudStorage"
 COLLECTFAST_STRATEGY = "collectfast.strategies.gcloud.GoogleCloudStrategy"
 STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/"
+{% elif cookiecutter.cloud_provider == 'Azure' -%}
+STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticRootAzureStorage"
+STATIC_URL = f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/static/"
 {% endif -%}
 
 # MEDIA
@@ -126,6 +133,9 @@
 {%- elif cookiecutter.cloud_provider == 'GCP' %}
 DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaRootGoogleCloudStorage"
 MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/"
+{%- elif cookiecutter.cloud_provider == 'Azure' %}
+DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaRootAzureStorage"
+MEDIA_URL = f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/media/"
 {%- endif %}
 
 # EMAIL
@@ -228,7 +238,7 @@
 {%- if cookiecutter.cloud_provider == 'None' %}
 # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE
 COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage"
-{%- elif cookiecutter.cloud_provider in ('AWS', 'GCP') and cookiecutter.use_whitenoise == 'n' %}
+{%- elif cookiecutter.cloud_provider in ('AWS', 'GCP', 'Azure') and cookiecutter.use_whitenoise == 'n' %}
 # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE
 COMPRESS_STORAGE = STATICFILES_STORAGE
 {%- endif %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py
@@ -22,4 +22,15 @@ class StaticRootGoogleCloudStorage(GoogleCloudStorage):
 class MediaRootGoogleCloudStorage(GoogleCloudStorage):
     location = "media"
     file_overwrite = False
+{%- elif cookiecutter.cloud_provider == 'Azure' -%}
+from storages.backends.azure_storage import AzureStorage
+
+
+class StaticRootAzureStorage(AzureStorage):
+    location = "static"
+
+
+class MediaRootAzureStorage(AzureStorage):
+    location = "media"
+    file_overwrite = False
 {%- endif %}


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
@@ -56,6 +56,8 @@ def context():
     {"cloud_provider": "AWS", "use_whitenoise": "n"},
     {"cloud_provider": "GCP", "use_whitenoise": "y"},
     {"cloud_provider": "GCP", "use_whitenoise": "n"},
+    {"cloud_provider": "Azure", "use_whitenoise": "y"},
+    {"cloud_provider": "Azure", "use_whitenoise": "n"},
     {"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mailgun"},
     {"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mailjet"},
     {"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mandrill"},
@@ -82,7 +84,16 @@ def context():
     {"cloud_provider": "GCP", "mail_service": "SendinBlue"},
     {"cloud_provider": "GCP", "mail_service": "SparkPost"},
     {"cloud_provider": "GCP", "mail_service": "Other SMTP"},
-    # Note: cloud_providers GCP and None with mail_service Amazon SES is not supported
+    {"cloud_provider": "Azure", "mail_service": "Mailgun"},
+    {"cloud_provider": "Azure", "mail_service": "Mailjet"},
+    {"cloud_provider": "Azure", "mail_service": "Mandrill"},
+    {"cloud_provider": "Azure", "mail_service": "Postmark"},
+    {"cloud_provider": "Azure", "mail_service": "Sendgrid"},
+    {"cloud_provider": "Azure", "mail_service": "SendinBlue"},
+    {"cloud_provider": "Azure", "mail_service": "SparkPost"},
+    {"cloud_provider": "Azure", "mail_service": "Other SMTP"},
+    # Note: cloud_providers GCP, Azure, and None
+    # with mail_service Amazon SES is not supported
     {"use_async": "y"},
     {"use_async": "n"},
     {"use_drf": "y"},
@@ -113,6 +124,7 @@ def context():
 UNSUPPORTED_COMBINATIONS = [
     {"cloud_provider": "None", "use_whitenoise": "n"},
     {"cloud_provider": "GCP", "mail_service": "Amazon SES"},
+    {"cloud_provider": "Azure", "mail_service": "Amazon SES"},
     {"cloud_provider": "None", "mail_service": "Amazon SES"},
 ]