├── mysite ├── mysite │ ├── __init__.py │ ├── urls.py │ ├── asgi.py │ ├── wsgi.py │ └── settings.py ├── polls │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_rules │ │ │ ├── __init__.py │ │ │ ├── rule_1 │ │ │ │ ├── __init__.py │ │ │ │ ├── bad_factories.py │ │ │ │ ├── test_rule_1.py │ │ │ │ └── good_factories.py │ │ │ ├── rule_2 │ │ │ │ ├── __init__.py │ │ │ │ ├── bad_factories.py │ │ │ │ ├── good_factories.py │ │ │ │ └── test_rule_2.py │ │ │ ├── rule_3 │ │ │ │ ├── __init__.py │ │ │ │ ├── bad_factories.py │ │ │ │ ├── good_factories.py │ │ │ │ └── test_rule_3.py │ │ │ ├── rule_4 │ │ │ │ ├── __init__.py │ │ │ │ ├── good_factories.py │ │ │ │ └── test_rule_4.py │ │ │ ├── rule_5 │ │ │ │ ├── __init__.py │ │ │ │ ├── test_rule_5.py │ │ │ │ └── good_factories.py │ │ │ └── rule_6 │ │ │ │ ├── __init__.py │ │ │ │ └── test_rule_6.py │ │ ├── test_with_factories │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ └── test_models.py │ │ └── test_without_factory_boy │ │ │ ├── __init__.py │ │ │ └── test_models.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── static │ │ └── polls │ │ │ └── style.css │ ├── apps.py │ ├── templates │ │ └── polls │ │ │ ├── results.html │ │ │ ├── index.html │ │ │ └── detail.html │ ├── urls.py │ ├── admin.py │ ├── views.py │ └── models.py ├── pytest.ini ├── Makefile ├── .pre-commit-config.yaml ├── pyproject.toml ├── manage.py ├── README.md └── poetry.lock ├── LICENSE ├── README.md └── .gitignore /mysite/mysite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_3/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_4/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_5/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_6/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_with_factories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_without_factory_boy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mysite/polls/static/polls/style.css: -------------------------------------------------------------------------------- 1 | li a { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /mysite/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = mysite.settings 3 | -------------------------------------------------------------------------------- /mysite/Makefile: -------------------------------------------------------------------------------- 1 | format: 2 | @black -l 80 . --exclude=.venv 3 | 4 | install: 5 | @poetry install 6 | @pre-commit install 7 | 8 | .PHONY: test format check install sh run 9 | -------------------------------------------------------------------------------- /mysite/polls/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PollsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "polls" 7 | -------------------------------------------------------------------------------- /mysite/mysite/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [ 5 | path("polls/", include("polls.urls")), 6 | path("admin/", admin.site.urls), 7 | ] 8 | -------------------------------------------------------------------------------- /mysite/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/python/black 3 | # It must match black's version that is inside pyproject.toml 4 | rev: 22.8.0 5 | hooks: 6 | - id: black 7 | language_version: python3 8 | args: ["-l 80", "--exclude=.venv"] 9 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/results.html: -------------------------------------------------------------------------------- 1 |

{{ poll.question.question_text }}

2 | 3 | 8 | 9 | Vote again? 10 | -------------------------------------------------------------------------------- /mysite/polls/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from polls import views 4 | 5 | app_name = "polls" 6 | urlpatterns = [ 7 | path("", views.IndexView.as_view(), name="index"), 8 | path("/", views.DetailView.as_view(), name="detail"), 9 | path("/results/", views.ResultsView.as_view(), name="results"), 10 | path("/vote/", views.vote, name="vote"), 11 | ] 12 | -------------------------------------------------------------------------------- /mysite/mysite/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for mysite 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/3.2/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", "mysite.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /mysite/mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite 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/3.2/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", "mysite.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /mysite/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "factory-boy-best-practices" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Camila Maia "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9" 10 | Django = "^3.2.4" 11 | 12 | [tool.poetry.dev-dependencies] 13 | factory-boy = "^3.2.1" 14 | pytest-django = "^4.5.2" 15 | black = "22.8.0" 16 | pre-commit = "^2.20.0" 17 | ipdb = "^0.13.9" 18 | 19 | [build-system] 20 | requires = ["poetry-core>=1.0.0"] 21 | build-backend = "poetry.core.masonry.api" 22 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_1/bad_factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | 4 | from polls.models import Poll, Question 5 | 6 | 7 | class PollFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Poll 10 | 11 | pub_date = factory.LazyFunction(timezone.now) 12 | 13 | 14 | class QuestionFactory(factory.django.DjangoModelFactory): 15 | class Meta: 16 | model = Question 17 | 18 | question_text = "What's Up?" 19 | poll = factory.RelatedFactory(PollFactory, "question") 20 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_1/test_rule_1.py: -------------------------------------------------------------------------------- 1 | from polls.tests.test_rules.rule_1.bad_factories import ( 2 | QuestionFactory as BadQuestionFactory, 3 | ) 4 | from polls.tests.test_rules.rule_1.good_factories import ( 5 | PollFactory as GoodPollFactory, 6 | ) 7 | 8 | 9 | def test_bad_factories(): 10 | question = BadQuestionFactory.build() 11 | assert question.poll 12 | assert question.poll.pub_date 13 | 14 | 15 | def test_good_factories(): 16 | poll = GoodPollFactory.build() 17 | assert poll.question 18 | assert poll.question.question_text 19 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_2/bad_factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | 4 | from polls.models import Poll, Question 5 | 6 | 7 | class PollFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Poll 10 | 11 | pub_date = factory.LazyFunction(timezone.now) 12 | question = factory.SubFactory( 13 | "polls.tests.test_rules.rule_2.bad_factories.QuestionFactory" 14 | ) 15 | 16 | 17 | class QuestionFactory(factory.django.DjangoModelFactory): 18 | class Meta: 19 | model = Question 20 | 21 | question_text = "What's Up?" 22 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_1/good_factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | 4 | from polls.models import Poll, Question 5 | 6 | 7 | class PollFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Poll 10 | 11 | pub_date = factory.LazyFunction(timezone.now) 12 | question = factory.SubFactory( 13 | "polls.tests.test_rules.rule_1.good_factories.QuestionFactory" 14 | ) 15 | 16 | 17 | class QuestionFactory(factory.django.DjangoModelFactory): 18 | class Meta: 19 | model = Question 20 | 21 | question_text = "Coke or Pepsi?" 22 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_2/good_factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | 4 | from polls.models import Poll, Question 5 | 6 | 7 | class PollFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Poll 10 | 11 | pub_date = factory.LazyFunction(timezone.now) 12 | question = factory.SubFactory( 13 | "polls.tests.test_rules.rule_2.good_factories.QuestionFactory" 14 | ) 15 | 16 | 17 | class QuestionFactory(factory.django.DjangoModelFactory): 18 | class Meta: 19 | model = Question 20 | 21 | question_text = factory.Faker("sentence") 22 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/index.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 |

Polls

6 | 7 | {% if latest_poll_list %} 8 |
    9 | {% for poll in latest_poll_list %} 10 |
  • 11 | {{ poll }} 12 | {% if poll.was_published_recently %} 13 | new! 14 | {% endif %} 15 |
  • 16 | {% endfor %} 17 |
18 | {% else %} 19 |

No polls are available.

20 | {% endif %} 21 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_2/test_rule_2.py: -------------------------------------------------------------------------------- 1 | from polls.tests.test_rules.rule_2.bad_factories import ( 2 | PollFactory as BadPollFactory, 3 | ) 4 | from polls.tests.test_rules.rule_2.good_factories import ( 5 | PollFactory as GoodPollFactory, 6 | ) 7 | 8 | 9 | def test_bad_to_string_with_non_premium_question_without_author(): 10 | poll = BadPollFactory.build() 11 | assert str(poll) == "What's Up?" 12 | 13 | 14 | def test_good_to_string_with_non_premium_question_without_author(): 15 | question = GoodPollFactory.build( 16 | premium=False, author=None, question__question_text="Pepsi or Coke?" 17 | ) 18 | assert str(question) == "Pepsi or Coke?" 19 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_3/bad_factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | 4 | from polls.models import Poll, Question 5 | 6 | 7 | class PollFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Poll 10 | 11 | pub_date = factory.LazyFunction(timezone.now) 12 | author = "João" 13 | premium = True 14 | question = factory.SubFactory( 15 | "polls.tests.test_rules.rule_3.bad_factories.QuestionFactory" 16 | ) 17 | 18 | 19 | class QuestionFactory(factory.django.DjangoModelFactory): 20 | class Meta: 21 | model = Question 22 | 23 | question_text = factory.Faker("sentence") 24 | language = Question.PT_BR 25 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_3/good_factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | 4 | from polls.models import Poll, Question 5 | 6 | 7 | class PollFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Poll 10 | 11 | pub_date = factory.LazyFunction(timezone.now) 12 | question = factory.SubFactory( 13 | "polls.tests.test_rules.rule_3.good_factories.QuestionFactory" 14 | ) 15 | 16 | class Params: 17 | with_author = factory.Trait(author=factory.Faker("name")) 18 | 19 | 20 | class QuestionFactory(factory.django.DjangoModelFactory): 21 | class Meta: 22 | model = Question 23 | 24 | question_text = factory.Faker("sentence") 25 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_4/good_factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | 4 | from polls.models import Poll, Question 5 | 6 | 7 | class PollFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = Poll 10 | 11 | pub_date = factory.LazyFunction(timezone.now) 12 | question = factory.SubFactory( 13 | "polls.tests.test_rules.rule_3.good_factories.QuestionFactory" 14 | ) 15 | 16 | class Params: 17 | with_author = factory.Trait(author=factory.Faker("name")) 18 | 19 | 20 | class QuestionFactory(factory.django.DjangoModelFactory): 21 | class Meta: 22 | model = Question 23 | 24 | question_text = factory.Faker("sentence") 25 | -------------------------------------------------------------------------------- /mysite/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /mysite/polls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from polls.models import Choice, Poll, Question 4 | 5 | 6 | class ChoiceInline(admin.TabularInline): 7 | model = Choice 8 | extra = 3 9 | 10 | 11 | class PollInline(admin.TabularInline): 12 | model = Poll 13 | 14 | 15 | class QuestionAdmin(admin.ModelAdmin): 16 | inlines = [PollInline, ChoiceInline] 17 | list_display = ("question_text", "language", "poll", "get_pub_date") 18 | list_filter = ("poll__pub_date", "language", "poll__premium") 19 | search_fields = ("question_text",) 20 | ordering = ("-poll__pub_date",) 21 | 22 | @admin.display(description="Pub Date") 23 | def get_pub_date(self, obj): 24 | return obj.poll.pub_date 25 | 26 | 27 | admin.site.register(Question, QuestionAdmin) 28 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_5/test_rule_5.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from polls.tests.test_rules.rule_5.good_factories import ( 4 | ChoiceFactory, 5 | QuestionFactory, 6 | ) 7 | 8 | 9 | def test_build_choice(): 10 | choice = ChoiceFactory.build( 11 | question__question_text="Pepsi or Coke?", 12 | ) 13 | 14 | assert str(choice.question) == "Pepsi or Coke?" 15 | 16 | 17 | def test_build_question_without_choices(): 18 | question = QuestionFactory.build() 19 | 20 | assert not question.choices.all() 21 | 22 | 23 | @mark.django_db 24 | def test_build_question_with_choices(): 25 | # We need to use `create` here in order to create the relationships 26 | question = QuestionFactory.create(with_choices=True) 27 | 28 | assert question.choices.count() == 2 29 | -------------------------------------------------------------------------------- /mysite/polls/templates/polls/detail.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 |
4 | 5 |

{{ poll.question.question_text }}

6 |
7 | 8 | {% if error_message %} 9 |

{{ error_message }}

10 | {% endif %} 11 | 12 | {% for choice in poll.question.choices.all %} 13 | 14 |
15 | {% endfor %} 16 |
17 | 18 | 19 |
20 | 21 |

See Results

22 |

See all Polls

23 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_4/test_rule_4.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from polls.models import Question 4 | from polls.tests.test_rules.rule_4.good_factories import PollFactory 5 | 6 | 7 | @mark.django_db 8 | def test_bad_to_string_with_non_premium_english_question_with_author(): 9 | question = PollFactory.create( 10 | question__question_text="Pepsi or Coke?", 11 | question__language=Question.EN, 12 | premium=False, 13 | with_author=True, 14 | ) 15 | assert str(question) == f"Pepsi or Coke? - By {question.author}" 16 | 17 | 18 | def test_good_to_string_with_non_premium_english_question_with_author(): 19 | question = PollFactory.build( 20 | question__question_text="Pepsi or Coke?", 21 | question__language=Question.EN, 22 | premium=False, 23 | with_author=True, 24 | ) 25 | assert str(question) == f"Pepsi or Coke? - By {question.author}" 26 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_5/good_factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from polls.models import Choice, Question 4 | 5 | 6 | class ChoiceFactory(factory.django.DjangoModelFactory): 7 | class Meta: 8 | model = Choice 9 | 10 | question = factory.SubFactory( 11 | "polls.tests.test_rules.rule_5.good_factories.QuestionFactory" 12 | ) 13 | 14 | 15 | class QuestionFactory(factory.django.DjangoModelFactory): 16 | class Meta: 17 | model = Question 18 | 19 | class Params: 20 | with_choices = factory.Trait( 21 | choice_1=factory.RelatedFactory( 22 | ChoiceFactory, 23 | "question", 24 | choice_text="Option 1", 25 | ), 26 | choice_2=factory.RelatedFactory( 27 | ChoiceFactory, 28 | "question", 29 | choice_text="Option 2", 30 | ), 31 | ) 32 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_3/test_rule_3.py: -------------------------------------------------------------------------------- 1 | from polls.models import Question 2 | from polls.tests.test_rules.rule_3.bad_factories import ( 3 | PollFactory as BadPollFactory, 4 | ) 5 | from polls.tests.test_rules.rule_3.good_factories import ( 6 | PollFactory as GoodPollFactory, 7 | ) 8 | 9 | 10 | def test_bad_to_string_with_non_premium_english_question_with_author(): 11 | poll = BadPollFactory.build( 12 | question__question_text="Pepsi or Coke?", 13 | question__language=Question.EN, 14 | premium=False, 15 | author="John", 16 | ) 17 | assert str(poll) == "Pepsi or Coke? - By John" 18 | 19 | 20 | def test_good_to_string_with_non_premium_english_question_with_author(): 21 | poll = GoodPollFactory.build( 22 | question__question_text="Pepsi or Coke?", 23 | question__language=Question.EN, 24 | premium=False, 25 | with_author=True, 26 | ) 27 | 28 | assert str(poll) == f"Pepsi or Coke? - By {poll.author}" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Camila Maia 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 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_rules/rule_6/test_rule_6.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from polls.models import Question 4 | from polls.tests.test_with_factories.factories import PollFactory 5 | 6 | # Good 7 | 8 | 9 | @fixture 10 | def english_non_premium_with_author_poll(): 11 | return PollFactory.build( 12 | question__question_text="Pepsi or Coke?", 13 | question__language=Question.EN, 14 | premium=False, 15 | with_author=True, 16 | ) 17 | 18 | 19 | def test_1(english_non_premium_with_author_poll): 20 | # ... 21 | pass 22 | 23 | 24 | def test_2(english_non_premium_with_author_poll): 25 | # ... 26 | pass 27 | 28 | 29 | # Bad 30 | 31 | 32 | def test_3(): 33 | poll = PollFactory.build( 34 | question__question_text="Pepsi or Coke?", 35 | question__language=Question.EN, 36 | premium=False, 37 | with_author=True, 38 | ) 39 | # ... 40 | pass 41 | 42 | 43 | def test_4(): 44 | poll = PollFactory.build( 45 | question__question_text="Pepsi or Coke?", 46 | question__language=Question.EN, 47 | premium=False, 48 | with_author=True, 49 | ) 50 | # ... 51 | pass 52 | 53 | 54 | # ... 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # factory_boy Best Practices 2 | 3 | 1. Factories should represent their models 4 | 5 | 1. Do not rely on defaults from factories 6 | 7 | * If a default value is changed, all tests that depend on it will break 8 | * The setup of a test should contain all the logic to ensure it will always pass 9 | * Explicit better than implicit 10 | 11 | 1. Factories should contain only the required data 12 | 13 | * If the field is nullable (null=True) the attribute should be under a trait 14 | and not as a default value 15 | * If we want to have explicitly the attribute, we can use MyFactory(with_myattr=True) 16 | * When are we going to remember to test the case MyFactory(myattr=None)? 17 | * We should not assume there is an author when DB actually allows to not have it. 18 | 19 | 1. Build over create 20 | 21 | * MyFactory.build(): creates a local object (memory) 22 | * MyFactory.create(): creates a local object + stores it in the DB 23 | 24 | 1. If FK is in the table: SubFactor 25 | 26 | If FK is in the other table: RelatedFactory + trait 27 | 28 | 29 | * SubFactory: builds/creates the SubFactory during the process of creation of the main factory 30 | * RelatedFactory: builds/creates the RelatedFactory after creating the main factory 31 | 32 | 1. Use fixtures to wrap factories to avoid duplication 33 | 34 | 1. Avoid sharing factories or fixtures among different files 35 | 36 | 1. get_or_create should be used only for unique keys 37 | -------------------------------------------------------------------------------- /mysite/README.md: -------------------------------------------------------------------------------- 1 | # mysite 2 | 3 | This is a demo app built based on top of the [Django's Tutorial][django-tutorial]. 4 | 5 | ## Install 6 | 7 | ### Requirements: 8 | 9 | - [Python 3.9+][python] 10 | - [Poetry][poetry] 11 | 12 | ### Fork camilamaia/factory-boy-best-practices repository 13 | 14 | [Fork][fork] the repo in your github account such as you get 15 | `_yourusername_/factory-boy-best-practices`. 16 | 17 | ### Clone 18 | 19 | Now that you own a copy of the repo, you can easily clone the repo with the command below: 20 | 21 | ```shell 22 | $ git clone https://github.com//factory-boy-best-practices.git 23 | ``` 24 | 25 | ### Enter in the mysite folder 26 | 27 | ```shell 28 | $ cd factory-boy-best-practices/mysite 29 | ``` 30 | 31 | ### Create and activate a new virtual environment 32 | 33 | ```shell 34 | $ poetry shell 35 | ``` 36 | 37 | ### Install the dependencies 38 | 39 | ```shell 40 | $ make install 41 | ``` 42 | 43 | ### Setup the Database 44 | 45 | ```shell 46 | $ python manage.py migrate 47 | ``` 48 | 49 | ## Run 50 | 51 | ```shell 52 | $ python manage.py runserver 53 | ``` 54 | 55 | If everything runs smoothly, you should see the polls index page live at 56 | `http://127.0.0.1:8000/polls`. Feel free to create an issue if run into problems while setting up 57 | the project. 58 | 59 | ## Tests 60 | 61 | ```shell 62 | $ pytest 63 | ``` 64 | 65 | [django-tutorial]: https://docs.djangoproject.com/en/3.2/intro/tutorial01/ 66 | [python]: https://www.python.org/downloads/ 67 | [fork]: https://docs.github.com/en/github/getting-started-with-github/quickstart/fork-a-repo 68 | [poetry]: https://python-poetry.org/docs/#installation 69 | -------------------------------------------------------------------------------- /mysite/polls/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.shortcuts import get_object_or_404, render 3 | from django.urls import reverse 4 | from django.utils import timezone 5 | from django.views import generic 6 | 7 | from polls.models import Choice, Poll, Question 8 | 9 | 10 | class IndexView(generic.ListView): 11 | template_name = "polls/index.html" 12 | context_object_name = "latest_poll_list" 13 | 14 | def get_queryset(self): 15 | """ 16 | Return the published polls (not including those set to be 17 | published in the future) order by DESC pub_date 18 | """ 19 | return Poll.objects.filter(pub_date__lte=timezone.now()).order_by( 20 | "-pub_date" 21 | ) 22 | 23 | 24 | class DetailView(generic.DetailView): 25 | model = Poll 26 | template_name = "polls/detail.html" 27 | 28 | 29 | class ResultsView(generic.DetailView): 30 | model = Poll 31 | template_name = "polls/results.html" 32 | 33 | 34 | def vote(request, poll_id): 35 | poll = get_object_or_404(Poll, pk=poll_id) 36 | 37 | try: 38 | selected_choice = poll.question.choices.get(pk=request.POST["choice"]) 39 | except (KeyError, Choice.DoesNotExist): 40 | # Redisplay the question voting form. 41 | return render( 42 | request, 43 | "polls/detail.html", 44 | { 45 | "poll": poll, 46 | "error_message": "You didn't select a choice.", 47 | }, 48 | ) 49 | else: 50 | selected_choice.votes += 1 51 | selected_choice.save() 52 | # Always return an HttpResponseRedirect after successfully dealing 53 | # with POST data. This prevents data from being posted twice if a 54 | # user hits the Back button. 55 | return HttpResponseRedirect(reverse("polls:results", args=(poll.id,))) 56 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_with_factories/factories.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | from factory import Faker, LazyFunction, RelatedFactory, SubFactory, Trait 3 | from factory.django import DjangoModelFactory 4 | 5 | from polls.models import Choice, Poll, Question 6 | 7 | 8 | class ChoiceFactory(DjangoModelFactory): 9 | class Meta: 10 | model = Choice 11 | 12 | question = SubFactory("polls.factories.QuestionFactory") 13 | 14 | 15 | class QuestionFactory(DjangoModelFactory): 16 | class Meta: 17 | model = Question 18 | 19 | question_text = Faker("sentence") 20 | 21 | class Params: 22 | english = Trait(language=Question.EN) 23 | portuguese = Trait(language=Question.PT_BR) 24 | with_choices = Trait( 25 | choice_1=RelatedFactory( 26 | ChoiceFactory, 27 | "question", 28 | choice_text="Option 1", 29 | ), 30 | choice_2=RelatedFactory( 31 | ChoiceFactory, 32 | "question", 33 | choice_text="Option 2", 34 | ), 35 | ) 36 | 37 | 38 | class PollFactory(DjangoModelFactory): 39 | class Meta: 40 | model = Poll 41 | 42 | pub_date = LazyFunction(timezone.now) 43 | question = SubFactory(QuestionFactory) 44 | 45 | class Params: 46 | with_author = Trait(author=Faker("name")) 47 | 48 | 49 | class PastPollFactory(PollFactory): 50 | pub_date = Faker( 51 | "date_time_between", 52 | end_date="-2d", 53 | tzinfo=timezone.get_current_timezone(), 54 | ) 55 | 56 | 57 | class RecentPollFactory(PollFactory): 58 | pub_date = LazyFunction(timezone.now) 59 | 60 | 61 | class FuturePollFactory(PollFactory): 62 | pub_date = Faker( 63 | "date_time_between", 64 | start_date="+1d", 65 | tzinfo=timezone.get_current_timezone(), 66 | ) 67 | -------------------------------------------------------------------------------- /mysite/polls/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.contrib import admin 4 | from django.db import models 5 | from django.utils import timezone 6 | 7 | 8 | class Poll(models.Model): 9 | question = models.OneToOneField( 10 | "polls.Question", 11 | on_delete=models.CASCADE, 12 | related_name="poll", 13 | ) 14 | pub_date = models.DateTimeField("date published") 15 | premium = models.BooleanField(default=False) 16 | author = models.CharField(max_length=100, null=True, blank=True) 17 | 18 | def __str__(self): 19 | question = str(self.question) 20 | 21 | if self.premium: 22 | question = f"⭐️ {question}" 23 | 24 | if self.author: 25 | by = self._get_preposition_by() # ["By", "Por"] | None 26 | question = f"{question} - {by} {self.author}" 27 | 28 | return question 29 | 30 | @property 31 | def was_published_recently(self): 32 | now = timezone.now() 33 | return now - datetime.timedelta(days=1) <= self.pub_date <= now 34 | 35 | def _get_preposition_by(self): 36 | if self.question.is_in_english: 37 | return "By" 38 | 39 | if self.question.is_in_portuguese: 40 | return "Por" 41 | 42 | 43 | class Question(models.Model): 44 | EN = "EN" 45 | PT_BR = "PT-BR" 46 | LANGUAGE_CHOICES = ( 47 | (EN, "English"), 48 | (PT_BR, "Português (Brasil)"), 49 | ) 50 | 51 | question_text = models.CharField(max_length=200) 52 | language = models.CharField( 53 | choices=LANGUAGE_CHOICES, default=EN, max_length=5 54 | ) 55 | 56 | @property 57 | def is_in_english(self): 58 | return self.language == self.EN 59 | 60 | @property 61 | def is_in_portuguese(self): 62 | return self.language == self.PT_BR 63 | 64 | def __str__(self): 65 | return self.question_text 66 | 67 | 68 | class Choice(models.Model): 69 | question = models.ForeignKey( 70 | Question, on_delete=models.CASCADE, related_name="choices" 71 | ) 72 | choice_text = models.CharField(max_length=200) 73 | votes = models.IntegerField(default=0) 74 | 75 | def __str__(self): 76 | return self.choice_text 77 | -------------------------------------------------------------------------------- /.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 | .vscode/ 132 | -------------------------------------------------------------------------------- /mysite/polls/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-10-06 17:28 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Question", 16 | fields=[ 17 | ( 18 | "id", 19 | models.BigAutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("question_text", models.CharField(max_length=200)), 27 | ( 28 | "language", 29 | models.CharField( 30 | choices=[ 31 | ("EN", "English"), 32 | ("PT-BR", "Português (Brasil)"), 33 | ], 34 | default="EN", 35 | max_length=5, 36 | ), 37 | ), 38 | ], 39 | ), 40 | migrations.CreateModel( 41 | name="Poll", 42 | fields=[ 43 | ( 44 | "id", 45 | models.BigAutoField( 46 | auto_created=True, 47 | primary_key=True, 48 | serialize=False, 49 | verbose_name="ID", 50 | ), 51 | ), 52 | ( 53 | "pub_date", 54 | models.DateTimeField(verbose_name="date published"), 55 | ), 56 | ("premium", models.BooleanField(default=False)), 57 | ( 58 | "author", 59 | models.CharField(blank=True, max_length=100, null=True), 60 | ), 61 | ( 62 | "question", 63 | models.OneToOneField( 64 | on_delete=django.db.models.deletion.CASCADE, 65 | related_name="poll", 66 | to="polls.question", 67 | ), 68 | ), 69 | ], 70 | ), 71 | migrations.CreateModel( 72 | name="Choice", 73 | fields=[ 74 | ( 75 | "id", 76 | models.BigAutoField( 77 | auto_created=True, 78 | primary_key=True, 79 | serialize=False, 80 | verbose_name="ID", 81 | ), 82 | ), 83 | ("choice_text", models.CharField(max_length=200)), 84 | ("votes", models.IntegerField(default=0)), 85 | ( 86 | "question", 87 | models.ForeignKey( 88 | on_delete=django.db.models.deletion.CASCADE, 89 | related_name="choices", 90 | to="polls.question", 91 | ), 92 | ), 93 | ], 94 | ), 95 | ] 96 | -------------------------------------------------------------------------------- /mysite/mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = ( 24 | "django-insecure-&fagamivpz1+qquydj@m#gh+fi0oot@qaa*=ft3yi$-3@$@zi6" 25 | ) 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | "django.contrib.admin", 37 | "django.contrib.auth", 38 | "django.contrib.contenttypes", 39 | "django.contrib.sessions", 40 | "django.contrib.messages", 41 | "django.contrib.staticfiles", 42 | # internal apps 43 | "polls.apps.PollsConfig", 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 | ROOT_URLCONF = "mysite.urls" 57 | 58 | TEMPLATES = [ 59 | { 60 | "BACKEND": "django.template.backends.django.DjangoTemplates", 61 | "DIRS": [], 62 | "APP_DIRS": True, 63 | "OPTIONS": { 64 | "context_processors": [ 65 | "django.template.context_processors.debug", 66 | "django.template.context_processors.request", 67 | "django.contrib.auth.context_processors.auth", 68 | "django.contrib.messages.context_processors.messages", 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = "mysite.wsgi.application" 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 79 | 80 | DATABASES = { 81 | "default": { 82 | "ENGINE": "django.db.backends.sqlite3", 83 | "NAME": BASE_DIR / "db.sqlite3", 84 | } 85 | } 86 | 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 94 | }, 95 | { 96 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 97 | }, 98 | { 99 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 100 | }, 101 | { 102 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 103 | }, 104 | ] 105 | 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 109 | 110 | LANGUAGE_CODE = "en-us" 111 | 112 | TIME_ZONE = "UTC" 113 | 114 | USE_I18N = True 115 | 116 | USE_L10N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 123 | 124 | STATIC_URL = "/static/" 125 | 126 | # Default primary key field type 127 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 128 | 129 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 130 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_with_factories/test_models.py: -------------------------------------------------------------------------------- 1 | from polls.tests.test_with_factories.factories import ( 2 | FuturePollFactory, 3 | PastPollFactory, 4 | PollFactory, 5 | QuestionFactory, 6 | RecentPollFactory, 7 | ) 8 | 9 | 10 | class TestPoll: 11 | def test_to_string_non_premium_and_without_author(self): 12 | poll = PollFactory.build( 13 | question__question_text="Summer or Winter?", 14 | premium=False, 15 | author=None, 16 | ) 17 | assert str(poll) == "Summer or Winter?" 18 | 19 | def test_to_string_non_premium_and_with_author_on_english(self): 20 | poll = PollFactory.build( 21 | question__question_text="Summer or Winter?", 22 | question__english=True, 23 | premium=False, 24 | author="John", 25 | ) 26 | assert str(poll) == "Summer or Winter? - By John" 27 | 28 | def test_to_string_non_premium_and_with_author_on_portuguese(self): 29 | poll = PollFactory.build( 30 | question__question_text="Summer or Winter?", 31 | question__portuguese=True, 32 | premium=False, 33 | author="João", 34 | ) 35 | assert str(poll) == "Summer or Winter? - Por João" 36 | 37 | def test_to_string_premium_and_without_author(self): 38 | poll = PollFactory.build( 39 | question__question_text="Summer or Winter?", 40 | premium=True, 41 | author=None, 42 | ) 43 | assert str(poll) == "⭐️ Summer or Winter?" 44 | 45 | def test_to_string_premium_and_with_author_on_english(self): 46 | poll = PollFactory.build( 47 | question__question_text="Summer or Winter?", 48 | question__english=True, 49 | premium=True, 50 | author="John", 51 | ) 52 | assert str(poll) == "⭐️ Summer or Winter? - By John" 53 | 54 | def test_to_string_premium_and_with_author_on_portuguese(self): 55 | poll = PollFactory.build( 56 | question__question_text="Summer or Winter?", 57 | question__portuguese=True, 58 | premium=True, 59 | author="João", 60 | ) 61 | assert str(poll) == "⭐️ Summer or Winter? - Por João" 62 | 63 | def test_was_published_recently_with_old_poll(self): 64 | """ 65 | was_published_recently returns False for polls whose pub_date 66 | is older than 1 day. 67 | """ 68 | old_poll = PastPollFactory.build() 69 | assert old_poll.was_published_recently is False 70 | 71 | def test_was_published_recently_with_recent_poll(self): 72 | """ 73 | was_published_recently returns True for polls whose pub_date 74 | is within the last day. 75 | """ 76 | recent_poll = RecentPollFactory.build() 77 | assert recent_poll.was_published_recently is True 78 | 79 | def test_was_published_recently_with_future_poll(self): 80 | """ 81 | was_published_recently returns False for polls whose pub_date 82 | is in the future. 83 | """ 84 | future_question = FuturePollFactory.build() 85 | assert future_question.was_published_recently is False 86 | 87 | 88 | class TestQuestion: 89 | def test_is_in_english_when_language_is_english(self): 90 | assert QuestionFactory.build(english=True).is_in_english is True 91 | 92 | def test_is_in_english_when_language_is_not_english(self): 93 | assert QuestionFactory.build(portuguese=True).is_in_english is False 94 | 95 | def test_is_in_portuguese_when_language_is_portuguese(self): 96 | assert QuestionFactory.build(portuguese=True).is_in_portuguese is True 97 | 98 | def test_is_in_portuguese_when_language_is_not_portuguese(self): 99 | assert QuestionFactory.build(english=True).is_in_portuguese is False 100 | -------------------------------------------------------------------------------- /mysite/polls/tests/test_without_factory_boy/test_models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils import timezone 4 | 5 | from polls.models import Poll, Question 6 | 7 | 8 | class TestPoll: 9 | def test_to_string_non_premium_and_without_author(self): 10 | question = Question(question_text="Summer or Winter?") 11 | assert ( 12 | str(Poll(question=question, premium=False, author=None)) 13 | == "Summer or Winter?" 14 | ) 15 | 16 | def test_to_string_non_premium_and_with_author_on_english(self): 17 | question = Question( 18 | question_text="Summer or Winter?", language=Question.EN 19 | ) 20 | assert ( 21 | str(Poll(question=question, premium=False, author="John")) 22 | == "Summer or Winter? - By John" 23 | ) 24 | 25 | def test_to_string_non_premium_and_with_author_on_portuguese(self): 26 | question = Question( 27 | question_text="Summer or Winter?", language=Question.PT_BR 28 | ) 29 | assert ( 30 | str(Poll(question=question, premium=False, author="João")) 31 | == "Summer or Winter? - Por João" 32 | ) 33 | 34 | def test_to_string_premium_and_without_author(self): 35 | question = Question(question_text="Summer or Winter?") 36 | assert ( 37 | str(Poll(question=question, premium=True, author=None)) 38 | == "⭐️ Summer or Winter?" 39 | ) 40 | 41 | def test_to_string_premium_and_with_author_on_english(self): 42 | question = Question( 43 | question_text="Summer or Winter?", language=Question.EN 44 | ) 45 | assert ( 46 | str(Poll(question=question, premium=True, author="John")) 47 | == "⭐️ Summer or Winter? - By John" 48 | ) 49 | 50 | def test_to_string_premium_and_with_author_on_portuguese(self): 51 | question = Question( 52 | question_text="Summer or Winter?", language=Question.PT_BR 53 | ) 54 | assert ( 55 | str(Poll(question=question, premium=True, author="João")) 56 | == "⭐️ Summer or Winter? - Por João" 57 | ) 58 | 59 | def test_was_published_recently_with_old_poll(self): 60 | """ 61 | was_published_recently returns False for polls whose pub_date 62 | is older than 1 day. 63 | """ 64 | time = timezone.now() - datetime.timedelta(days=1, seconds=1) 65 | old_poll = Poll(pub_date=time) 66 | assert old_poll.was_published_recently is False 67 | 68 | def test_was_published_recently_with_recent_poll(self): 69 | """ 70 | was_published_recently returns True for polls whose pub_date 71 | is within the last day. 72 | """ 73 | time = timezone.now() - datetime.timedelta( 74 | hours=23, minutes=59, seconds=59 75 | ) 76 | recent_poll = Poll(pub_date=time) 77 | assert recent_poll.was_published_recently is True 78 | 79 | def test_was_published_recently_with_future_poll(self): 80 | """ 81 | was_published_recently returns False for polls whose pub_date 82 | is in the future. 83 | """ 84 | time = timezone.now() + datetime.timedelta(days=30) 85 | future_question = Poll(pub_date=time) 86 | assert future_question.was_published_recently is False 87 | 88 | 89 | class TestQuestion: 90 | def test_is_in_english_when_language_is_english(self): 91 | question = Question(language=Question.EN) 92 | assert question.is_in_english is True 93 | 94 | def test_is_in_english_when_language_is_not_english(self): 95 | question = Question(language=Question.PT_BR) 96 | assert question.is_in_english is False 97 | 98 | def test_is_in_portuguese_when_language_is_portuguese(self): 99 | question = Question(language=Question.PT_BR) 100 | assert question.is_in_portuguese is True 101 | 102 | def test_is_in_portuguese_when_language_is_not_portuguese(self): 103 | question = Question(language=Question.EN) 104 | assert question.is_in_portuguese is False 105 | -------------------------------------------------------------------------------- /mysite/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appnope" 3 | version = "0.1.3" 4 | description = "Disable App Nap on macOS >= 10.9" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "asgiref" 11 | version = "3.5.2" 12 | description = "ASGI specs, helper code, and adapters" 13 | category = "main" 14 | optional = false 15 | python-versions = ">=3.7" 16 | 17 | [package.extras] 18 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 19 | 20 | [[package]] 21 | name = "asttokens" 22 | version = "2.0.8" 23 | description = "Annotate AST trees with source code positions" 24 | category = "dev" 25 | optional = false 26 | python-versions = "*" 27 | 28 | [package.dependencies] 29 | six = "*" 30 | 31 | [package.extras] 32 | test = ["astroid (<=2.5.3)", "pytest"] 33 | 34 | [[package]] 35 | name = "attrs" 36 | version = "22.1.0" 37 | description = "Classes Without Boilerplate" 38 | category = "dev" 39 | optional = false 40 | python-versions = ">=3.5" 41 | 42 | [package.extras] 43 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 44 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 45 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 46 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] 47 | 48 | [[package]] 49 | name = "backcall" 50 | version = "0.2.0" 51 | description = "Specifications for callback functions passed in to an API" 52 | category = "dev" 53 | optional = false 54 | python-versions = "*" 55 | 56 | [[package]] 57 | name = "black" 58 | version = "22.8.0" 59 | description = "The uncompromising code formatter." 60 | category = "dev" 61 | optional = false 62 | python-versions = ">=3.6.2" 63 | 64 | [package.dependencies] 65 | click = ">=8.0.0" 66 | mypy-extensions = ">=0.4.3" 67 | pathspec = ">=0.9.0" 68 | platformdirs = ">=2" 69 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 70 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 71 | 72 | [package.extras] 73 | colorama = ["colorama (>=0.4.3)"] 74 | d = ["aiohttp (>=3.7.4)"] 75 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 76 | uvloop = ["uvloop (>=0.15.2)"] 77 | 78 | [[package]] 79 | name = "cfgv" 80 | version = "3.3.1" 81 | description = "Validate configuration and produce human readable error messages." 82 | category = "dev" 83 | optional = false 84 | python-versions = ">=3.6.1" 85 | 86 | [[package]] 87 | name = "click" 88 | version = "8.1.3" 89 | description = "Composable command line interface toolkit" 90 | category = "dev" 91 | optional = false 92 | python-versions = ">=3.7" 93 | 94 | [package.dependencies] 95 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 96 | 97 | [[package]] 98 | name = "colorama" 99 | version = "0.4.5" 100 | description = "Cross-platform colored terminal text." 101 | category = "dev" 102 | optional = false 103 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 104 | 105 | [[package]] 106 | name = "decorator" 107 | version = "5.1.1" 108 | description = "Decorators for Humans" 109 | category = "dev" 110 | optional = false 111 | python-versions = ">=3.5" 112 | 113 | [[package]] 114 | name = "distlib" 115 | version = "0.3.6" 116 | description = "Distribution utilities" 117 | category = "dev" 118 | optional = false 119 | python-versions = "*" 120 | 121 | [[package]] 122 | name = "django" 123 | version = "3.2.15" 124 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 125 | category = "main" 126 | optional = false 127 | python-versions = ">=3.6" 128 | 129 | [package.dependencies] 130 | asgiref = ">=3.3.2,<4" 131 | pytz = "*" 132 | sqlparse = ">=0.2.2" 133 | 134 | [package.extras] 135 | argon2 = ["argon2-cffi (>=19.1.0)"] 136 | bcrypt = ["bcrypt"] 137 | 138 | [[package]] 139 | name = "executing" 140 | version = "1.0.0" 141 | description = "Get the currently executing AST node of a frame, and other information" 142 | category = "dev" 143 | optional = false 144 | python-versions = "*" 145 | 146 | [[package]] 147 | name = "factory-boy" 148 | version = "3.2.1" 149 | description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." 150 | category = "dev" 151 | optional = false 152 | python-versions = ">=3.6" 153 | 154 | [package.dependencies] 155 | Faker = ">=0.7.0" 156 | 157 | [package.extras] 158 | dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"] 159 | doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] 160 | 161 | [[package]] 162 | name = "faker" 163 | version = "14.2.0" 164 | description = "Faker is a Python package that generates fake data for you." 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=3.6" 168 | 169 | [package.dependencies] 170 | python-dateutil = ">=2.4" 171 | 172 | [[package]] 173 | name = "filelock" 174 | version = "3.8.0" 175 | description = "A platform independent file lock." 176 | category = "dev" 177 | optional = false 178 | python-versions = ">=3.7" 179 | 180 | [package.extras] 181 | docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] 182 | testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] 183 | 184 | [[package]] 185 | name = "identify" 186 | version = "2.5.3" 187 | description = "File identification library for Python" 188 | category = "dev" 189 | optional = false 190 | python-versions = ">=3.7" 191 | 192 | [package.extras] 193 | license = ["ukkonen"] 194 | 195 | [[package]] 196 | name = "iniconfig" 197 | version = "1.1.1" 198 | description = "iniconfig: brain-dead simple config-ini parsing" 199 | category = "dev" 200 | optional = false 201 | python-versions = "*" 202 | 203 | [[package]] 204 | name = "ipdb" 205 | version = "0.13.9" 206 | description = "IPython-enabled pdb" 207 | category = "dev" 208 | optional = false 209 | python-versions = ">=2.7" 210 | 211 | [package.dependencies] 212 | decorator = {version = "*", markers = "python_version > \"3.6\""} 213 | ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""} 214 | toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} 215 | 216 | [[package]] 217 | name = "ipython" 218 | version = "8.4.0" 219 | description = "IPython: Productive Interactive Computing" 220 | category = "dev" 221 | optional = false 222 | python-versions = ">=3.8" 223 | 224 | [package.dependencies] 225 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 226 | backcall = "*" 227 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 228 | decorator = "*" 229 | jedi = ">=0.16" 230 | matplotlib-inline = "*" 231 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 232 | pickleshare = "*" 233 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" 234 | pygments = ">=2.4.0" 235 | stack-data = "*" 236 | traitlets = ">=5" 237 | 238 | [package.extras] 239 | all = ["black", "Sphinx (>=1.3)", "ipykernel", "nbconvert", "nbformat", "ipywidgets", "notebook", "ipyparallel", "qtconsole", "pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "numpy (>=1.19)", "pandas", "trio"] 240 | black = ["black"] 241 | doc = ["Sphinx (>=1.3)"] 242 | kernel = ["ipykernel"] 243 | nbconvert = ["nbconvert"] 244 | nbformat = ["nbformat"] 245 | notebook = ["ipywidgets", "notebook"] 246 | parallel = ["ipyparallel"] 247 | qtconsole = ["qtconsole"] 248 | test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] 249 | test_extra = ["pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "trio"] 250 | 251 | [[package]] 252 | name = "jedi" 253 | version = "0.18.1" 254 | description = "An autocompletion tool for Python that can be used for text editors." 255 | category = "dev" 256 | optional = false 257 | python-versions = ">=3.6" 258 | 259 | [package.dependencies] 260 | parso = ">=0.8.0,<0.9.0" 261 | 262 | [package.extras] 263 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 264 | testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] 265 | 266 | [[package]] 267 | name = "matplotlib-inline" 268 | version = "0.1.6" 269 | description = "Inline Matplotlib backend for Jupyter" 270 | category = "dev" 271 | optional = false 272 | python-versions = ">=3.5" 273 | 274 | [package.dependencies] 275 | traitlets = "*" 276 | 277 | [[package]] 278 | name = "mypy-extensions" 279 | version = "0.4.3" 280 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 281 | category = "dev" 282 | optional = false 283 | python-versions = "*" 284 | 285 | [[package]] 286 | name = "nodeenv" 287 | version = "1.7.0" 288 | description = "Node.js virtual environment builder" 289 | category = "dev" 290 | optional = false 291 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 292 | 293 | [[package]] 294 | name = "packaging" 295 | version = "21.3" 296 | description = "Core utilities for Python packages" 297 | category = "dev" 298 | optional = false 299 | python-versions = ">=3.6" 300 | 301 | [package.dependencies] 302 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 303 | 304 | [[package]] 305 | name = "parso" 306 | version = "0.8.3" 307 | description = "A Python Parser" 308 | category = "dev" 309 | optional = false 310 | python-versions = ">=3.6" 311 | 312 | [package.extras] 313 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 314 | testing = ["docopt", "pytest (<6.0.0)"] 315 | 316 | [[package]] 317 | name = "pathspec" 318 | version = "0.10.1" 319 | description = "Utility library for gitignore style pattern matching of file paths." 320 | category = "dev" 321 | optional = false 322 | python-versions = ">=3.7" 323 | 324 | [[package]] 325 | name = "pexpect" 326 | version = "4.8.0" 327 | description = "Pexpect allows easy control of interactive console applications." 328 | category = "dev" 329 | optional = false 330 | python-versions = "*" 331 | 332 | [package.dependencies] 333 | ptyprocess = ">=0.5" 334 | 335 | [[package]] 336 | name = "pickleshare" 337 | version = "0.7.5" 338 | description = "Tiny 'shelve'-like database with concurrency support" 339 | category = "dev" 340 | optional = false 341 | python-versions = "*" 342 | 343 | [[package]] 344 | name = "platformdirs" 345 | version = "2.5.2" 346 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 347 | category = "dev" 348 | optional = false 349 | python-versions = ">=3.7" 350 | 351 | [package.extras] 352 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 353 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 354 | 355 | [[package]] 356 | name = "pluggy" 357 | version = "1.0.0" 358 | description = "plugin and hook calling mechanisms for python" 359 | category = "dev" 360 | optional = false 361 | python-versions = ">=3.6" 362 | 363 | [package.extras] 364 | testing = ["pytest-benchmark", "pytest"] 365 | dev = ["tox", "pre-commit"] 366 | 367 | [[package]] 368 | name = "pre-commit" 369 | version = "2.20.0" 370 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 371 | category = "dev" 372 | optional = false 373 | python-versions = ">=3.7" 374 | 375 | [package.dependencies] 376 | cfgv = ">=2.0.0" 377 | identify = ">=1.0.0" 378 | nodeenv = ">=0.11.1" 379 | pyyaml = ">=5.1" 380 | toml = "*" 381 | virtualenv = ">=20.0.8" 382 | 383 | [[package]] 384 | name = "prompt-toolkit" 385 | version = "3.0.31" 386 | description = "Library for building powerful interactive command lines in Python" 387 | category = "dev" 388 | optional = false 389 | python-versions = ">=3.6.2" 390 | 391 | [package.dependencies] 392 | wcwidth = "*" 393 | 394 | [[package]] 395 | name = "ptyprocess" 396 | version = "0.7.0" 397 | description = "Run a subprocess in a pseudo terminal" 398 | category = "dev" 399 | optional = false 400 | python-versions = "*" 401 | 402 | [[package]] 403 | name = "pure-eval" 404 | version = "0.2.2" 405 | description = "Safely evaluate AST nodes without side effects" 406 | category = "dev" 407 | optional = false 408 | python-versions = "*" 409 | 410 | [package.extras] 411 | tests = ["pytest"] 412 | 413 | [[package]] 414 | name = "py" 415 | version = "1.11.0" 416 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 417 | category = "dev" 418 | optional = false 419 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 420 | 421 | [[package]] 422 | name = "pygments" 423 | version = "2.13.0" 424 | description = "Pygments is a syntax highlighting package written in Python." 425 | category = "dev" 426 | optional = false 427 | python-versions = ">=3.6" 428 | 429 | [package.extras] 430 | plugins = ["importlib-metadata"] 431 | 432 | [[package]] 433 | name = "pyparsing" 434 | version = "3.0.9" 435 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 436 | category = "dev" 437 | optional = false 438 | python-versions = ">=3.6.8" 439 | 440 | [package.extras] 441 | diagrams = ["railroad-diagrams", "jinja2"] 442 | 443 | [[package]] 444 | name = "pytest" 445 | version = "7.1.3" 446 | description = "pytest: simple powerful testing with Python" 447 | category = "dev" 448 | optional = false 449 | python-versions = ">=3.7" 450 | 451 | [package.dependencies] 452 | attrs = ">=19.2.0" 453 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 454 | iniconfig = "*" 455 | packaging = "*" 456 | pluggy = ">=0.12,<2.0" 457 | py = ">=1.8.2" 458 | tomli = ">=1.0.0" 459 | 460 | [package.extras] 461 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 462 | 463 | [[package]] 464 | name = "pytest-django" 465 | version = "4.5.2" 466 | description = "A Django plugin for pytest." 467 | category = "dev" 468 | optional = false 469 | python-versions = ">=3.5" 470 | 471 | [package.dependencies] 472 | pytest = ">=5.4.0" 473 | 474 | [package.extras] 475 | docs = ["sphinx", "sphinx-rtd-theme"] 476 | testing = ["django", "django-configurations (>=2.0)"] 477 | 478 | [[package]] 479 | name = "python-dateutil" 480 | version = "2.8.2" 481 | description = "Extensions to the standard Python datetime module" 482 | category = "dev" 483 | optional = false 484 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 485 | 486 | [package.dependencies] 487 | six = ">=1.5" 488 | 489 | [[package]] 490 | name = "pytz" 491 | version = "2022.2.1" 492 | description = "World timezone definitions, modern and historical" 493 | category = "main" 494 | optional = false 495 | python-versions = "*" 496 | 497 | [[package]] 498 | name = "pyyaml" 499 | version = "6.0" 500 | description = "YAML parser and emitter for Python" 501 | category = "dev" 502 | optional = false 503 | python-versions = ">=3.6" 504 | 505 | [[package]] 506 | name = "six" 507 | version = "1.16.0" 508 | description = "Python 2 and 3 compatibility utilities" 509 | category = "dev" 510 | optional = false 511 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 512 | 513 | [[package]] 514 | name = "sqlparse" 515 | version = "0.4.2" 516 | description = "A non-validating SQL parser." 517 | category = "main" 518 | optional = false 519 | python-versions = ">=3.5" 520 | 521 | [[package]] 522 | name = "stack-data" 523 | version = "0.5.0" 524 | description = "Extract data from python stack frames and tracebacks for informative displays" 525 | category = "dev" 526 | optional = false 527 | python-versions = "*" 528 | 529 | [package.dependencies] 530 | asttokens = "*" 531 | executing = "*" 532 | pure-eval = "*" 533 | 534 | [package.extras] 535 | tests = ["cython", "littleutils", "pygments", "typeguard", "pytest"] 536 | 537 | [[package]] 538 | name = "toml" 539 | version = "0.10.2" 540 | description = "Python Library for Tom's Obvious, Minimal Language" 541 | category = "dev" 542 | optional = false 543 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 544 | 545 | [[package]] 546 | name = "tomli" 547 | version = "2.0.1" 548 | description = "A lil' TOML parser" 549 | category = "dev" 550 | optional = false 551 | python-versions = ">=3.7" 552 | 553 | [[package]] 554 | name = "traitlets" 555 | version = "5.3.0" 556 | description = "" 557 | category = "dev" 558 | optional = false 559 | python-versions = ">=3.7" 560 | 561 | [package.extras] 562 | test = ["pre-commit", "pytest"] 563 | 564 | [[package]] 565 | name = "typing-extensions" 566 | version = "4.3.0" 567 | description = "Backported and Experimental Type Hints for Python 3.7+" 568 | category = "dev" 569 | optional = false 570 | python-versions = ">=3.7" 571 | 572 | [[package]] 573 | name = "virtualenv" 574 | version = "20.16.4" 575 | description = "Virtual Python Environment builder" 576 | category = "dev" 577 | optional = false 578 | python-versions = ">=3.6" 579 | 580 | [package.dependencies] 581 | distlib = ">=0.3.5,<1" 582 | filelock = ">=3.4.1,<4" 583 | platformdirs = ">=2.4,<3" 584 | 585 | [package.extras] 586 | docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] 587 | testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] 588 | 589 | [[package]] 590 | name = "wcwidth" 591 | version = "0.2.5" 592 | description = "Measures the displayed width of unicode strings in a terminal" 593 | category = "dev" 594 | optional = false 595 | python-versions = "*" 596 | 597 | [metadata] 598 | lock-version = "1.1" 599 | python-versions = "^3.9" 600 | content-hash = "04095c23876db6300ca8bd60bdcb507b0c7c5590aa5f032692cb68da16e11f49" 601 | 602 | [metadata.files] 603 | appnope = [] 604 | asgiref = [] 605 | asttokens = [] 606 | attrs = [] 607 | backcall = [] 608 | black = [] 609 | cfgv = [] 610 | click = [] 611 | colorama = [] 612 | decorator = [] 613 | distlib = [] 614 | django = [] 615 | executing = [] 616 | factory-boy = [] 617 | faker = [] 618 | filelock = [] 619 | identify = [] 620 | iniconfig = [ 621 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 622 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 623 | ] 624 | ipdb = [] 625 | ipython = [] 626 | jedi = [] 627 | matplotlib-inline = [] 628 | mypy-extensions = [ 629 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 630 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 631 | ] 632 | nodeenv = [] 633 | packaging = [] 634 | parso = [] 635 | pathspec = [] 636 | pexpect = [] 637 | pickleshare = [] 638 | platformdirs = [] 639 | pluggy = [] 640 | pre-commit = [] 641 | prompt-toolkit = [] 642 | ptyprocess = [] 643 | pure-eval = [] 644 | py = [] 645 | pygments = [] 646 | pyparsing = [] 647 | pytest = [] 648 | pytest-django = [] 649 | python-dateutil = [ 650 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 651 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 652 | ] 653 | pytz = [] 654 | pyyaml = [] 655 | six = [ 656 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 657 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 658 | ] 659 | sqlparse = [] 660 | stack-data = [] 661 | toml = [ 662 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 663 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 664 | ] 665 | tomli = [] 666 | traitlets = [] 667 | typing-extensions = [] 668 | virtualenv = [] 669 | wcwidth = [] 670 | --------------------------------------------------------------------------------