├── .gitignore ├── README.md ├── cookbook ├── __init__.py ├── admin.py ├── apps.py ├── fixtures │ ├── __init__.py │ └── cookbook.json ├── models.py ├── services.py ├── templates │ └── cookbook │ │ └── recipes.html ├── urls.py └── views.py ├── example ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | db.sqlite3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Caching in Django with Redis 2 | 3 | ## Want to learn how to build this project? 4 | 5 | Check out the [blog post](https://realpython.com/blog/python/caching-in-django-with-redis/). 6 | 7 | ## Want to use this project? 8 | 9 | 1. Fork/Clone 10 | 1. Create and activate a virtual environment 11 | 1. Install dependencies - `pip install -r requirements` 12 | 1. Make migrations - `python manage.py makemigrations cookbook` 13 | 1. Build the database - `python manage.py migrate` 14 | 1. Create a superuser - `python manage.py createsuperuser` 15 | 1. Add seed data - `python manage.py loaddata cookbook/fixtures/cookbook.json` 16 | 1. Run the development server - `python manage.py runserver` 17 | -------------------------------------------------------------------------------- /cookbook/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realpython/django-redis-cache/64c4e47b24f9a2d4fb1e484cbc590987829de8ea/cookbook/__init__.py -------------------------------------------------------------------------------- /cookbook/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Recipe, Food, Ingredient 3 | 4 | 5 | class IngredientInline(admin.TabularInline): 6 | model = Ingredient 7 | 8 | 9 | @admin.register(Recipe) 10 | class RecipeAdmin(admin.ModelAdmin): 11 | inlines = (IngredientInline,) 12 | 13 | 14 | @admin.register(Food) 15 | class FoodAdmin(admin.ModelAdmin): 16 | pass 17 | -------------------------------------------------------------------------------- /cookbook/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class CookbookConfig(AppConfig): 7 | name = 'cookbook' 8 | -------------------------------------------------------------------------------- /cookbook/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realpython/django-redis-cache/64c4e47b24f9a2d4fb1e484cbc590987829de8ea/cookbook/fixtures/__init__.py -------------------------------------------------------------------------------- /cookbook/fixtures/cookbook.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "cookbook.recipe", 4 | "pk": 1, 5 | "fields": { 6 | "name": "Classic Teriyaki Sauce", 7 | "desc": "Many Americans think of teriyaki as a marinade, but in traditional Japanese cuisine it's actually a glaze or barbecue sauce brushed on simply grilled meats and seafood. Teri is the Japanese word for gloss or luster; yaki means grilled. Zen-like in its simplicity, this recipe was inspired by the late Shizuo Tsuji, founder of the Ecole Technique H\u00f4teli\u00e8re Tsuji in Osaka and author of the seminal book Japanese Cooking: A Simple Art.", 8 | "instructions": "
    \r\n
  1. Combine the soy sauce, sake, mirin, and sugar in a small, heavy saucepan and bring to a boil over high heat. Reduce the heat to medium and let simmer until the sugar dissolves and the sauce is thick and syrupy, about 5 minutes.
  2. \r\n
  3. Remove the sauce from the heat and let cool before using it as a glaze or as a sauce for serving. The teriyaki sauce will keep, tightly covered in the refrigerator, for at least 2 weeks.
  4. \r\n
" 9 | } 10 | }, 11 | { 12 | "model": "cookbook.recipe", 13 | "pk": 2, 14 | "fields": { 15 | "name": "Red Miso Barbecue Sauce", 16 | "desc": "Red miso (actually a reddish brown in color) has a deep, rich, salty flavor and isn't quite as sweet as white miso. Known as aka-miso in Japanese, it contains barley as well as soybeans and rice. Red miso barbecue sauce is particularly good on grilled vegetables and salmon.", 17 | "instructions": "
    \r\n
  1. Combine the red miso, sake, mirin, sugar, and mayonnaise in the top of a double boiler and whisk until smooth. Gradually whisk in the vegetable stock. Cook the sauce over simmering water, stirring occasionally, until thick and creamy, about 5 minutes. Remove the pan from over the water and let the sauce cool to room temperature. Taste for seasoning, adding more sugar necessary; be sure to stir any added sugar in thoroughly.
  2. \r\n
  3. Transfer the red miso sauce to a serving bowl. It can be refrigerated, covered, for up to 3 days. Let it return to room temperature before serving.
  4. \r\n
" 18 | } 19 | }, 20 | { 21 | "model": "cookbook.food", 22 | "pk": 1, 23 | "fields": { 24 | "name": "dark soy sauce" 25 | } 26 | }, 27 | { 28 | "model": "cookbook.food", 29 | "pk": 2, 30 | "fields": { 31 | "name": "sake" 32 | } 33 | }, 34 | { 35 | "model": "cookbook.food", 36 | "pk": 3, 37 | "fields": { 38 | "name": "mirin" 39 | } 40 | }, 41 | { 42 | "model": "cookbook.food", 43 | "pk": 4, 44 | "fields": { 45 | "name": "sugar" 46 | } 47 | }, 48 | { 49 | "model": "cookbook.food", 50 | "pk": 5, 51 | "fields": { 52 | "name": "red miso" 53 | } 54 | }, 55 | { 56 | "model": "cookbook.food", 57 | "pk": 6, 58 | "fields": { 59 | "name": "mayonnaise" 60 | } 61 | }, 62 | { 63 | "model": "cookbook.food", 64 | "pk": 7, 65 | "fields": { 66 | "name": "dashi" 67 | } 68 | }, 69 | { 70 | "model": "cookbook.ingredient", 71 | "pk": 1, 72 | "fields": { 73 | "recipe": 1, 74 | "food": 1, 75 | "amount": "0.500", 76 | "unit_of_measure": "cup", 77 | "desc": "1/2 cup dark soy sauce" 78 | } 79 | }, 80 | { 81 | "model": "cookbook.ingredient", 82 | "pk": 2, 83 | "fields": { 84 | "recipe": 1, 85 | "food": 2, 86 | "amount": "0.500", 87 | "unit_of_measure": "cup", 88 | "desc": "1/2 cup sake or dry sherry" 89 | } 90 | }, 91 | { 92 | "model": "cookbook.ingredient", 93 | "pk": 3, 94 | "fields": { 95 | "recipe": 1, 96 | "food": 3, 97 | "amount": "0.500", 98 | "unit_of_measure": "cup", 99 | "desc": "1/2 cup mirin (sweet rice wine) or cream sherry" 100 | } 101 | }, 102 | { 103 | "model": "cookbook.ingredient", 104 | "pk": 4, 105 | "fields": { 106 | "recipe": 1, 107 | "food": 4, 108 | "amount": "2.000", 109 | "unit_of_measure": "tbsp", 110 | "desc": "2 tablespoons sugar" 111 | } 112 | }, 113 | { 114 | "model": "cookbook.ingredient", 115 | "pk": 5, 116 | "fields": { 117 | "recipe": 2, 118 | "food": 5, 119 | "amount": "1.000", 120 | "unit_of_measure": "cup", 121 | "desc": "1 cup red miso" 122 | } 123 | }, 124 | { 125 | "model": "cookbook.ingredient", 126 | "pk": 6, 127 | "fields": { 128 | "recipe": 2, 129 | "food": 2, 130 | "amount": "2.000", 131 | "unit_of_measure": "tbsp", 132 | "desc": "2 tablespoons sake or dry sherry" 133 | } 134 | }, 135 | { 136 | "model": "cookbook.ingredient", 137 | "pk": 7, 138 | "fields": { 139 | "recipe": 2, 140 | "food": 3, 141 | "amount": "2.000", 142 | "unit_of_measure": "tbsp", 143 | "desc": "2 tablespoons mirin (sweet rice wine), or cream sherry" 144 | } 145 | }, 146 | { 147 | "model": "cookbook.ingredient", 148 | "pk": 8, 149 | "fields": { 150 | "recipe": 2, 151 | "food": 4, 152 | "amount": "2.000", 153 | "unit_of_measure": "tbsp", 154 | "desc": "2 tablespoons sugar, or more to taste" 155 | } 156 | }, 157 | { 158 | "model": "cookbook.ingredient", 159 | "pk": 9, 160 | "fields": { 161 | "recipe": 2, 162 | "food": 6, 163 | "amount": "2.000", 164 | "unit_of_measure": "tbsp", 165 | "desc": "2 tablespoons mayonnaise" 166 | } 167 | }, 168 | { 169 | "model": "cookbook.ingredient", 170 | "pk": 10, 171 | "fields": { 172 | "recipe": 2, 173 | "food": 7, 174 | "amount": "2.000", 175 | "unit_of_measure": "tbsp", 176 | "desc": "2 tablespoons vegetable stock, dashi, or water" 177 | } 178 | } 179 | ] 180 | -------------------------------------------------------------------------------- /cookbook/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Recipe(models.Model): 5 | """A preparation of food.""" 6 | 7 | name = models.CharField(max_length=255) 8 | 9 | desc = models.TextField(null=True, blank=True) 10 | 11 | ingredients = models.ManyToManyField( 12 | 'cookbook.Food', 13 | through='cookbook.Ingredient', 14 | through_fields=('recipe', 'food') 15 | ) 16 | 17 | instructions = models.TextField(null=True, blank=True) 18 | 19 | class Meta(object): 20 | app_label = 'cookbook' 21 | default_related_name = 'recipes' 22 | 23 | def __unicode__(self): 24 | return self.name 25 | 26 | 27 | class Food(models.Model): 28 | """An edible item.""" 29 | 30 | name = models.CharField(max_length=255) 31 | 32 | class Meta(object): 33 | app_label = 'cookbook' 34 | default_related_name = 'foods' 35 | 36 | def __unicode__(self): 37 | return self.name 38 | 39 | 40 | class Ingredient(models.Model): 41 | """A food that is used in a recipe.""" 42 | 43 | recipe = models.ForeignKey(Recipe) 44 | 45 | food = models.ForeignKey(Food) 46 | 47 | # ex. 1/8 = 0.125, 1/4 = 0.250 48 | amount = models.DecimalField(max_digits=6, decimal_places=3, 49 | null=True, blank=True) 50 | 51 | # ex. tsp, tbsp, cup 52 | unit_of_measure = models.CharField(max_length=255) 53 | 54 | # ex. 2 cloves of garlic, minced 55 | desc = models.TextField() 56 | 57 | class Meta(object): 58 | app_label = 'cookbook' 59 | 60 | def __unicode__(self): 61 | return '{recipe}: {amount} {unit_of_measure} {food}'.format( 62 | recipe=self.recipe, 63 | amount=self.amount, 64 | unit_of_measure=self.unit_of_measure, 65 | food=self.food 66 | ) 67 | -------------------------------------------------------------------------------- /cookbook/services.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.cache import cache 3 | from django.core.cache.backends.base import DEFAULT_TIMEOUT 4 | from .models import Recipe 5 | 6 | CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT) 7 | 8 | 9 | def get_recipes_without_cache(): 10 | return list(Recipe.objects.prefetch_related('ingredient_set__food')) 11 | 12 | 13 | def get_recipes_with_cache(): 14 | if 'recipes' in cache: 15 | recipes = cache.get('recipes') 16 | else: 17 | recipes = list(Recipe.objects.prefetch_related('ingredient_set__food')) 18 | cache.set('recipes', recipes, timeout=CACHE_TTL) 19 | return recipes 20 | -------------------------------------------------------------------------------- /cookbook/templates/cookbook/recipes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Recipes 4 | 5 | 6 | {% for recipe in recipes %} 7 |

{{ recipe.name }}

8 | {% autoescape off %} 9 |

{{ recipe.desc }}

10 | {% endautoescape %} 11 |

Ingredients

12 | 17 |

Instructions

18 | {% autoescape off %} 19 |

{{ recipe.instructions }}

20 | {% endautoescape %} 21 | {% endfor %} 22 | 23 | 24 | -------------------------------------------------------------------------------- /cookbook/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from .views import recipes_view 3 | 4 | 5 | urlpatterns = [ 6 | url(r'^$', recipes_view), 7 | ] 8 | -------------------------------------------------------------------------------- /cookbook/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.cache.backends.base import DEFAULT_TIMEOUT 3 | from django.shortcuts import render 4 | from django.views.decorators.cache import cache_page 5 | from .services import get_recipes_with_cache as get_recipes 6 | 7 | CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT) 8 | 9 | 10 | @cache_page(CACHE_TTL) 11 | def recipes_view(request): 12 | return render(request, 'cookbook/recipes.html', { 13 | 'recipes': get_recipes() 14 | }) 15 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realpython/django-redis-cache/64c4e47b24f9a2d4fb1e484cbc590987829de8ea/example/__init__.py -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '7#w^)8%x1r=902vwea^(@ros*wwkwttur_bxsq3s4h2z6)@d7m' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | DEFAULT_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | THIRD_PARTY_APPS = ['debug_toolbar'] 43 | 44 | LOCAL_APPS = ['cookbook'] 45 | 46 | INSTALLED_APPS = DEFAULT_APPS + THIRD_PARTY_APPS + LOCAL_APPS 47 | 48 | MIDDLEWARE_CLASSES = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'example.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'example.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.sqlite3', 86 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 126 | 127 | STATIC_URL = '/static/' 128 | 129 | CACHES = { 130 | "default": { 131 | "BACKEND": "django_redis.cache.RedisCache", 132 | "LOCATION": "redis://127.0.0.1:6379/1", 133 | "OPTIONS": { 134 | "CLIENT_CLASS": "django_redis.client.DefaultClient" 135 | }, 136 | "KEY_PREFIX": "example" 137 | } 138 | } 139 | 140 | # Cache time to live is 15 minutes. 141 | CACHE_TTL = 60 * 15 142 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = [ 5 | url(r'^admin/', admin.site.urls), 6 | ] 7 | 8 | urlpatterns += [ 9 | url(r'^cookbook/', include('cookbook.urls')) 10 | ] 11 | -------------------------------------------------------------------------------- /example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example 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/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.9.8 2 | django-debug-toolbar==1.5 3 | django-redis==4.4.4 4 | redis==2.10.5 5 | sqlparse==0.2.0 6 | --------------------------------------------------------------------------------