├── .circleci └── config.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── app ├── db.sqlite3 ├── djangotip │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── products │ ├── __init__.py │ ├── apps.py │ ├── decorators.py │ ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── load_products.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── queries.py │ └── tests │ ├── __init__.py │ ├── test_models.py │ └── test_queries.py ├── requirements.txt └── setup.cfg /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/python:3.6.1 10 | 11 | working_directory: ~/project 12 | 13 | steps: 14 | - checkout 15 | 16 | # Download and cache dependencies 17 | - restore_cache: 18 | keys: 19 | - v1-dependencies-{{ checksum "requirements.txt" }} 20 | # fallback to using the latest cache if no exact match is found 21 | - v1-dependencies- 22 | 23 | - run: 24 | name: Install dependencies 25 | command: | 26 | python3 -m venv venv 27 | . venv/bin/activate 28 | make setup 29 | 30 | - save_cache: 31 | paths: 32 | - ./venv 33 | key: v1-dependencies-{{ checksum "requirements.txt" }} 34 | 35 | - run: 36 | name: Run tests 37 | command: | 38 | . venv/bin/activate 39 | make tests 40 | 41 | - store_artifacts: 42 | path: test-reports 43 | destination: test-reports 44 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at lucasmagnumlopes@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Project purpose 2 | =============== 3 | 4 | This project was created just to have fun, so don't take it too serious. 5 | I'm trying to follow the best practices with code, integrations and documentation. 6 | 7 | If you are missing something or want to suggest better ways to do, I'm always open to hear it from you. 8 | 9 | --- 10 | Code & Fun <3 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lucas Magnum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: ## Clean environment 2 | @find . -name "*.pyc" | xargs rm -rf 3 | @find . -name "*.pyo" | xargs rm -rf 4 | @find . -name "__pycache__" -type d | xargs rm -rf 5 | @rm -f .coverage 6 | @rm -f *.log 7 | 8 | help: ## This help 9 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 10 | 11 | setup: setup-tests ## Install dependencies 12 | 13 | setup-tests: ## Install python requirements 14 | pip install -r requirements.txt 15 | 16 | tests: clean lint ## Run tests 17 | cd app & python app/manage.py test products -v2 18 | 19 | lint: ## Lint project 20 | flake8 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #DjangoTip 02 2 | ================== 3 | 4 | [![CircleCI](https://circleci.com/gh/LucasMagnum/django-tip-02.svg?style=shield)](https://circleci.com/gh/LucasMagnum/django-tip-02) 5 | 6 | 7 | This is just a sample app used in [Select & Prefetch related](https://medium.com/@lucasmagnum/djangotip-select-prefetch-related-e76b683aa457). 8 | 9 | 10 | ### How to use it 11 | 12 | Basically, we have to follow the steps bellow: 13 | 14 | 1. Clone this repository 15 | 2. Install all dependencies (see [setup](#1-setup)) 16 | 3. Execute all tests (see [tests](#2-running-tests)) 17 | 18 | 19 | ### Table of Contents 20 | 21 | * [1. Setup](#1-setup) 22 | * [2. Running tests](#2-running-tests) 23 | * [Useful links](#useful-links) 24 | 25 | --- 26 | 27 | ## Development 28 | 29 | ### 1. Setup 30 | 31 | To properly run this project, we should install the dependencies: 32 | 33 | ```bash 34 | make setup 35 | ``` 36 | 37 | ### 2. Running tests 38 | 39 | Run the command bellow to run the tests 40 | 41 | ```bash 42 | make tests 43 | ``` 44 | 45 | ### Useful links 46 | 47 | 1 - [Select & Prefetch related](https://medium.com/@lucasmagnum/djangotip-select-prefetch-related-e76b683aa457) 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasMagnum/django-tip-02/7ab01c999b6df7f53ed3ee6a22a0b584cf40d48a/app/db.sqlite3 -------------------------------------------------------------------------------- /app/djangotip/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasMagnum/django-tip-02/7ab01c999b6df7f53ed3ee6a22a0b584cf40d48a/app/djangotip/__init__.py -------------------------------------------------------------------------------- /app/djangotip/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djangotip project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/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.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '+nrx1kn1iv61uf_)0gc$z_sg)=t3$yg%^qq+s0kk!a7^u!d-oy' 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 | INSTALLED_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 | 'products' 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'djangotip.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'djangotip.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | -------------------------------------------------------------------------------- /app/djangotip/urls.py: -------------------------------------------------------------------------------- 1 | """djangotip URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/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: url(r'^$', 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: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /app/djangotip/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djangotip02 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.11/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", "djangotip.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /app/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", "djangotip.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /app/products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasMagnum/django-tip-02/7ab01c999b6df7f53ed3ee6a22a0b584cf40d48a/app/products/__init__.py -------------------------------------------------------------------------------- /app/products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductsConfig(AppConfig): 5 | name = 'products' 6 | -------------------------------------------------------------------------------- /app/products/decorators.py: -------------------------------------------------------------------------------- 1 | import time 2 | import functools 3 | 4 | from django.db import connection, reset_queries 5 | 6 | 7 | def debugger_queries(func): 8 | """Basic function to debug queries.""" 9 | @functools.wraps(func) 10 | def wrapper(*args, **kwargs): 11 | print("func: ", func.__name__) 12 | reset_queries() 13 | 14 | start = time.time() 15 | start_queries = len(connection.queries) 16 | 17 | result = func(*args, **kwargs) 18 | 19 | end = time.time() 20 | end_queries = len(connection.queries) 21 | 22 | print("queries:", end_queries - start_queries) 23 | print("took: %.2fs" % (end - start)) 24 | return result 25 | 26 | return wrapper 27 | -------------------------------------------------------------------------------- /app/products/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasMagnum/django-tip-02/7ab01c999b6df7f53ed3ee6a22a0b584cf40d48a/app/products/management/__init__.py -------------------------------------------------------------------------------- /app/products/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasMagnum/django-tip-02/7ab01c999b6df7f53ed3ee6a22a0b584cf40d48a/app/products/management/commands/__init__.py -------------------------------------------------------------------------------- /app/products/management/commands/load_products.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from products.models import Category, Product 3 | 4 | 5 | class Command(BaseCommand): 6 | """This command was created to insert products and categories into database.""" 7 | 8 | help = "Insert 500 products and 50 categories into database" 9 | 10 | def handle(self, *args, **options): 11 | self.stdout.write("Cleaning database...") 12 | Category.objects.all().delete() 13 | Product.objects.all().delete() 14 | 15 | self.stdout.write("Generating categories") 16 | categories = [] 17 | 18 | for num in range(1, 51): 19 | # Is active will be False when the num is multiple of 5 20 | # This will generate 10 categories with the False flag 21 | is_active = num % 5 != 0 22 | categories.append( 23 | Category(name=f"category #{num}", is_active=is_active) 24 | ) 25 | 26 | Category.objects.bulk_create(categories) 27 | 28 | self.stdout.write("Generating subcatories") 29 | 30 | # The first 5 catories will have 10 subcatories each 31 | for num, category in enumerate(Category.objects.all()[:5]): 32 | subcategories = Category.objects.filter( 33 | id__gt=category.id + 10 * num 34 | ) 35 | category.subcategories = subcategories[:10] 36 | category.save() 37 | 38 | self.stdout.write("Generating products") 39 | 40 | products = [] 41 | product_id = 1 42 | 43 | # Each category will have 10 products 44 | for category in Category.objects.all(): 45 | for num in range(product_id * 10, (product_id * 10 + 10)): 46 | products.append( 47 | Product( 48 | title=f"product #{num}", 49 | category=category 50 | ) 51 | ) 52 | 53 | product_id += 1 54 | 55 | Product.objects.bulk_create(products) 56 | 57 | products_count = Product.objects.count() 58 | categories_count = Category.objects.count() 59 | 60 | self.stdout.write( 61 | self.style.SUCCESS(f'Inserted {products_count} products and {categories_count} categories') 62 | ) 63 | -------------------------------------------------------------------------------- /app/products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-08-14 12:27 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Category', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=120)), 22 | ('is_active', models.BooleanField(default=True)), 23 | ('subcategories', models.ManyToManyField(to='products.Category')), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='Product', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('title', models.CharField(max_length=120)), 31 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.Category')), 32 | ], 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /app/products/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasMagnum/django-tip-02/7ab01c999b6df7f53ed3ee6a22a0b584cf40d48a/app/products/migrations/__init__.py -------------------------------------------------------------------------------- /app/products/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Category(models.Model): 5 | name = models.CharField(max_length=120) 6 | is_active = models.BooleanField(default=True) 7 | 8 | subcategories = models.ManyToManyField('self', symmetrical=False) 9 | 10 | def __str__(self): 11 | return self.name 12 | 13 | 14 | class Product(models.Model): 15 | title = models.CharField(max_length=120) 16 | 17 | category = models.ForeignKey(Category) 18 | 19 | def __str__(self): 20 | return self.title 21 | -------------------------------------------------------------------------------- /app/products/queries.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Prefetch 2 | 3 | from .decorators import debugger_queries 4 | from .models import Category, Product 5 | 6 | 7 | def categories_list(): 8 | categories_qs = Category.objects.all() 9 | 10 | categories = [] 11 | 12 | for category in categories_qs: 13 | subcategories = [sub.name for sub in category.subcategories.all()] 14 | 15 | categories.append({ 16 | 'name': category.name, 17 | 'is_active': category.is_active, 18 | 'subcategories': subcategories 19 | }) 20 | 21 | return categories 22 | 23 | 24 | def debugger_categories_list(): 25 | """Run debugger queries for categories_list function.""" 26 | return debugger_queries(categories_list)() 27 | 28 | 29 | def categories_list_prefetch_related(): 30 | categories_qs = Category.objects.prefetch_related('subcategories') 31 | 32 | categories = [] 33 | 34 | for category in categories_qs: 35 | subcategories = [sub.name for sub in category.subcategories.all()] 36 | 37 | categories.append({ 38 | 'name': category.name, 39 | 'is_active': category.is_active, 40 | 'subcategories': subcategories 41 | }) 42 | 43 | return categories 44 | 45 | 46 | def debugger_categories_list_prefetch_related(): 47 | """Run debugger queries for categories_list_prefetch_related function.""" 48 | return debugger_queries(categories_list_prefetch_related)() 49 | 50 | 51 | def categories_list_active_subcategories(): 52 | """Return all categories and list only the active subcatories.""" 53 | categories_qs = Category.objects.prefetch_related("subcategories") 54 | 55 | categories = [] 56 | 57 | for category in categories_qs: 58 | subcategories = [sub.name for sub in 59 | category.subcategories.filter(is_active=True)] 60 | 61 | categories.append({ 62 | 'name': category.name, 63 | 'is_active': category.is_active, 64 | 'subcategories': subcategories 65 | }) 66 | 67 | return categories 68 | 69 | 70 | def debugger_categories_list_active_subcategories(): 71 | """Run debugger queries for categories_list_active_subcategories.""" 72 | return debugger_queries(categories_list_active_subcategories)() 73 | 74 | 75 | def categories_list_active_subcategories_using_prefetch_attr(): 76 | categories_qs = Category.objects.prefetch_related( 77 | Prefetch( 78 | 'subcategories', 79 | queryset=Category.objects.filter(is_active=True), 80 | to_attr='active_subcategories' 81 | ) 82 | ) 83 | 84 | categories = [] 85 | 86 | for category in categories_qs: 87 | subcategories = [sub.name for sub in category.active_subcategories] 88 | 89 | categories.append({ 90 | 'name': category.name, 91 | 'is_active': category.is_active, 92 | 'subcategories': subcategories 93 | }) 94 | 95 | return categories 96 | 97 | 98 | def debugger_categories_list_active_subcategories_using_prefetch_attr(): 99 | return debugger_queries(categories_list_active_subcategories_using_prefetch_attr)() # noqa 100 | 101 | 102 | def categories_list_active_subcategories_using_prefetch_queryset(): 103 | categories_qs = Category.objects.prefetch_related( 104 | Prefetch( 105 | 'subcategories', 106 | queryset=Category.objects.filter(is_active=True), 107 | ) 108 | ) 109 | 110 | categories = [] 111 | 112 | for category in categories_qs: 113 | subcategories = [sub.name for sub in category.subcategories.all()] 114 | 115 | categories.append({ 116 | 'name': category.name, 117 | 'is_active': category.is_active, 118 | 'subcategories': subcategories 119 | }) 120 | 121 | return categories 122 | 123 | 124 | def debugger_categories_list_active_subcategories_using_prefetch_queryset(): 125 | return debugger_queries(categories_list_active_subcategories_using_prefetch_queryset)() # noqa 126 | 127 | 128 | def products_list(): 129 | product_qs = Product.objects.all() 130 | 131 | products = [] 132 | 133 | for product in product_qs: 134 | products.append({ 135 | 'id': product.id, 136 | 'title': product.title, 137 | 'category': product.category.name 138 | }) 139 | 140 | return products 141 | 142 | 143 | def debugger_products_list(): 144 | """Run debugger queries for products_list function.""" 145 | return debugger_queries(products_list)() 146 | 147 | 148 | def products_list_select_related(): 149 | product_qs = Product.objects.select_related('category').all() 150 | 151 | products = [] 152 | 153 | for product in product_qs: 154 | products.append({ 155 | 'id': product.id, 156 | 'title': product.title, 157 | 'category': product.category.name 158 | }) 159 | 160 | return products 161 | 162 | 163 | def debugger_products_list_select_related(): 164 | """Run debugger queries for products_list_select_related function.""" 165 | return debugger_queries(products_list_select_related)() 166 | -------------------------------------------------------------------------------- /app/products/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucasMagnum/django-tip-02/7ab01c999b6df7f53ed3ee6a22a0b584cf40d48a/app/products/tests/__init__.py -------------------------------------------------------------------------------- /app/products/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class TestProductsModel(TestCase): 5 | 6 | def test_setup(self): 7 | assert True 8 | -------------------------------------------------------------------------------- /app/products/tests/test_queries.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..models import Category, Product 4 | from ..queries import products_list, products_list_select_related 5 | 6 | 7 | class ProductsTestMixin(object): 8 | def setUp(self): 9 | category = Category.objects.create(name="Test category") 10 | 11 | for num in range(1, 10): 12 | Product.objects.create( 13 | title=f"product_{num}", 14 | category=category 15 | ) 16 | 17 | 18 | class TestProductList(ProductsTestMixin, TestCase): 19 | def test_products_list_return_all_products(self): 20 | """Test products list return all products.""" 21 | products = products_list() 22 | 23 | assert len(products) == Product.objects.count() 24 | 25 | for product in Product.objects.all(): 26 | product_data = { 27 | "id": product.id, 28 | "title": product.title, 29 | "category": product.category.name 30 | } 31 | assert product_data in products 32 | 33 | def test_products_list_queries(self): 34 | """Test products list number of queries.""" 35 | products_count = Product.objects.count() 36 | 37 | # Num queries is equal the number of products + 1 38 | # For each product we run one new query to get category 39 | # and we have the first query to get all products 40 | with self.assertNumQueries(products_count + 1): 41 | products_list() 42 | 43 | 44 | class TestProductListSelectRelated(ProductsTestMixin, TestCase): 45 | def test_products_list_select_related_return_all_products(self): 46 | """Test products list return all products.""" 47 | assert products_list() == products_list_select_related() 48 | 49 | products = products_list_select_related() 50 | 51 | assert len(products) > 1 52 | assert len(products) == Product.objects.count() 53 | 54 | for product in Product.objects.all(): 55 | product_data = { 56 | "id": product.id, 57 | "title": product.title, 58 | "category": product.category.name 59 | } 60 | assert product_data in products 61 | 62 | def test_products_list_queries(self): 63 | """Test products list number of queries.""" 64 | # Num queries is equal to 1 because we are using select related 65 | with self.assertNumQueries(1): 66 | products_list_select_related() 67 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | flake8 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = venv, settings.py, manage.py --------------------------------------------------------------------------------