1 of 67

Qualities of great reusable Django apps

2 of 67

3 of 67

4 of 67

Concept

  • Django app: encapsulates a project feature.
  • Django project: is made up of multiple apps.
  • Reusable Django apps are the ones can be used in multiple projects.

  • Django docs says "Reusability is the way of life in Python. You only need to write the parts that make your project unique".�

�Throughout the talk, you'll see lots of red words. Those are links. There is a lot of additional content there.

5 of 67

Concept

  • Django encourages multiple apps. Don’t be afraid of multiple apps!
    • Add more as the project grows:
      • manage.py startproject: 6 apps.
      • Vinta's boilerplate: 10 apps.
      • Vinta's mature projects: 40 apps.
    • Don't reinvent the wheel, use third-party apps.

6 of 67

Concept

  • Check if your app follows Unix philosophy:�"Do one thing, and do it well".�
  • Check if your app's description fits a few words:
    • django-taggit: "a simpler approach to tagging with Django".
    • django-js-reverse: "JavaScript URL handling for Django that doesn't hurt".

�Every time you see some statement with a little square before it, it's an advice. Consider this a checklist for building great Django apps.

7 of 67

That's the concept of a Django app. But what makes an app great?

8 of 67

  1. Easy to install
  2. Easy to use
  3. Easy to integrate

9 of 67

Easy to install

10 of 67

Easy to install: Distributing

  • Make it available on PyPI
    • pip install django-something
    • Prefix the name with django-
      • django-filter, django-taggit, django-import-export, etc.
    • Check for name clashes:
      • django-filter vs django-filters, both are on PyPI.
    • Use Wheels, the new standard of Python distribution.
    • Add a LICENSE file.

11 of 67

Easy to install: Distributing

  • Publish it onDjango Packages
    • Developers want to compare your app with others.
    • Make your app stand out.

12 of 67

Easy to install: Distributing

  • Install dependencies automatically
    • Add dependencies on install_requires on setup.py.
    • Don't pin version with ==, use >=. See django-templated-email's setup.py:

install_requires=['django-render-block>=0.5','six>=1',],

13 of 67

Easy to install: Django app vs Python package

  • Check if you need a Django app or a regular Python package
    • A Django app needs to be added to INSTALLED_APPS.
    • A regular Python package does not.
      • It's easier to configure.
      • It's easier to use.
      • It might even depend on Django. For example, you can use settings, yet not be an app.

�Let's see in practice…

14 of 67

Easy to install: Django app vs Python package

  • django-templated-email
    • In a nutshell, it's just a helper function to send emails made of Django templates.
    • It uses Django's django.core.mail internally, so it depends on Django. But it's not an app.

from templated_email import send_templated_mail��send_templated_mail(� template_name='welcome',� from_email='from@example.com',� recipient_list=['to@example.com'],� context={user: request.user},)

15 of 67

Easy to install: Defaults

  • Have sane and smart defaults
  • django-debug-toolbar example:
    • only works if DEBUG==True
    • has a default panels
    • uses a jQuery from a CDN
    • doesn't show collapsed by default
    • etc.

16 of 67

Easy to install: Config

  • Have declarative settings to allow easy configuration
    • Add a prefix to all settings of the app,�like django-guardian:�GUARDIAN_TEMPLATE_403 = 'custom/forbidden.html'
    • Convert hardcoded internal parameters to settings,�like django-avatar:�AVATAR_MAX_SIZE = 1 * 1024 * 1024 # 1 MB

17 of 67

Easy to install: Config

  • Provide default views with URLs to allow the app to be easily included, like django-notifications:

urlpatterns = [� url('^notifications/',� include(notifications.urls, namespace='notifications')),]

18 of 67

Easy to install: Upgrade

  • Have a friendly upgrade policy
    • Deprecate before removing. Raise deprecation warnings, use Python warnings built-in module.
    • Don't rewrite migrations.
    • keep a changelog.com
    • Follow Semantic Versioning: semver.org

19 of 67

Easy to use

20 of 67

Easy to use: Docs

  • Provide documentation
    • Python Requests creator, Kenneth Reitz says: write docs first.
      • Makes you think about the problem first and come up with a simple solution.
      • Forces you think from user perspective.
    • Provide a quick start tutorial describing the most common use case. See: django-filter, django-mptt, django-haystack.
    • Separate high level from low-level docs, as Django does.
    • Be inclusive: use gender neutral pronouns, as Django does.
  • Ship with an example project.�See: django-guardian, django-avatar, django-haystack

21 of 67

Easy to use: Recognition rather than recall

  • A good documentation isn't an excuse for a bad API:
    • "If you have to refer to the documentation every time you use a module, find (or build) a new module" - Kenneth Reitz

  • Strive for "Recognition rather than recall". Stick to Django abstractions like views, forms, models, etc...

22 of 67

Easy to use: Recognition rather than recall

my_app/� management/� migrations/� templates/� templatetags/� __init__.py� admin.py� apps.py� context_processors.py� exceptions.py� fields.py

� forms.py� helpers.py� managers.py� middleware.py� models.py� querysets.py� signals.py� urls.py� validators.py� views.py� widgets.py�

23 of 67

Easy to use: Fail-fast

  • Raise ImproperlyConfigured if the developer makes a mistake on the config
    • For example, django-filter raises ImproperlyConfigured if user forgets to set filterset_class in a FilterView.�
  • Raise TypeError or ValueError when the app gets an invalid argument

24 of 67

Easy to integrate

25 of 67

Easy to adapt it to your project's needs

26 of 67

Discontinuity of integration

27 of 67

Unsolved

Solved

Slightly overkill

Way overkill

Unsolved

Way overkill

Discontinuity

Options of integration in purple

28 of 67

Imagine you're building a web app for an online store.�They're asking for a feature to�filter products by exact price

29 of 67

Use case: client wants a filter for price field

Options of integration

Integration Work

Benefit to project

30 of 67

Use case: client wants a filter for price field

Integration Work

Benefit to project

Starting here

31 of 67

Implementing with django-filter

class ProductFilter(django_filters.FilterSet):� class Meta:� model = Product� fields = ['price']

32 of 67

New requirement:�filter products by price,�greater than equal and less than equal

33 of 67

Use case: client wants a filter for price field

Integration Work

Benefit to project

New requirements

34 of 67

Use case: client wants a filter for price field

Integration Work

Benefit to project

New requirements

35 of 67

Implementing a filter with django-filter

class ProductFilter(django_filters.FilterSet):� class Meta:� model = Product� fields = {'price': ['lte', 'gte'],}

36 of 67

New requirement:�filter products by price,�greater than equal and less than equal,�but include approximate prices

37 of 67

Use case: client wants a filter for price field

Integration Work

Benefit to project

New requirements

38 of 67

Implementing a filter with django-filter

class CustomPriceFilter(django_filters.NumberFilter):� # ...� �class ProductFilter(django_filters.FilterSet):� price = CustomPriceFilter()� � class Meta:� model = Product

39 of 67

What if django-filter wasn't so extensible?

40 of 67

Use case: client wants a filter for price field

Discontinuity

Integration Work

Benefit to project

New requirements

41 of 67

Integration Work

Benefit to project

More options of integration means more use cases addressed, i.e., a more reusable API

42 of 67

The most important thing to do to make Django apps reusable is to eliminate integration discontinuities

43 of 67

Thankfully, Django abstractions help a lot to eliminate integration discontinuities

44 of 67

Easy to integrate: Django abstractions

  • Separation of concerns:
    • AppConfig
    • Templates
    • Views
    • Paginators
    • Forms
    • Form fields

    • Models
    • Model fields
    • Managers
    • Querysets
    • Validators
    • etc

45 of 67

Eliminating integration discontinuities is a matter of properly using Django abstractions and further breaking those abstractions into extensible parts

46 of 67

Easy to integrate: Paginator (using properly)

  • Like Django's Paginator, Django REST Framework has abstractions for paginating querysets.
  • One of them is CursorPagination. It's a proper usage of Django's Paginator abstraction.
  • But it had a problem: it originally supported a fixed page_size.

47 of 67

48 of 67

49 of 67

DRF CursorPagination is now more extensible because it has more methods

50 of 67

Easy to integrate: Classes (going extensible)

From this example, we can get a general advice for classes, not only for paginators:

  • Break class behaviors into methods
    • Consider if an attribute should be a method:
      • page_size vs get_page_size
    • Pass arguments that methods might need
      • get_page_size needs the request, because the page_size might vary according to request.user, request.GET, etc.

51 of 67

More methods�==�More options of integration

52 of 67

Easy to integrate: Views (using properly)

  • Respect the configurability of class-based views.�For example, django-authtools PasswordResetView:�

class CustomPasswordResetView(PasswordResetView):� template_name = 'custom-auth/forgot_password.html'� email_template_name = 'reset_password'� from_email = settings.DEFAULT_FROM_EMAIL� � def form_valid(self, form):� messages.success(self.request,� _("An email with a reset link has been sent to your inbox."))return super().form_valid(form)

53 of 67

Easy to integrate: Views (using properly)

  • Respect the configurability of class-based views.�For example, django-authtools PasswordResetView:�

class CustomPasswordResetView(PasswordResetView):� template_name = 'custom-auth/forgot_password.html'� email_template_name = 'reset_password'� from_email = settings.DEFAULT_FROM_EMAIL� � def form_valid(self, form):� messages.success(self.request,� _("An email with a reset link has been sent to your inbox."))return super().form_valid(form)

Django attrs/methods

54 of 67

Easy to integrate: Views (using properly)

  • Respect the configurability of class-based views.�For example, django-authtools PasswordResetView:�

class CustomPasswordResetView(PasswordResetView):� template_name = 'custom-auth/forgot_password.html'email_template_name = 'reset_password'from_email = settings.DEFAULT_FROM_EMAIL� � def form_valid(self, form):� messages.success(self.request,� _("An email with a reset link has been sent to your inbox."))return super().form_valid(form)

App attrs/methods

55 of 67

Easy to integrate: Views (going extensible)

  • Break views into mixins.�Django's own views do that:

class ListView(MultipleObjectTemplateResponseMixin, BaseListView):� # methods…� �class BaseListView(MultipleObjectMixin, View):� # methods…

56 of 67

Easy to integrate: Views (going extensible)

  • Break views into mixins.�So does django-filter:

class FilterView(MultipleObjectTemplateResponseMixin, BaseFilterView):� # methods…� �class BaseFilterView(FilterMixin, MultipleObjectMixin, View):� # methods…� �class FilterMixin:� # methods…

57 of 67

More mixins�==�More options of integration

58 of 67

Easy to integrate: Template tags (using properly)

  • Template tags should be concerned in presenting data.�django-avatar example:

{% load avatar_tags %}��{% avatar user=user size=100 %} <!--- 100x100 avatar -->

<img src="https://www.gravatar.com/avatar/123?s=100"alt="user" width="100" height="100" />

59 of 67

Easy to integrate: Template tags (going extensible)

  • Leave only presentation logic into template tags, isolate the rest of logic into helpers:

@register.simple_tag

def avatar(user, size, **kwargs):� for provider_path in settings.AVATAR_PROVIDERS:� provider = import_string(provider_path)� url = provider.get_avatar_url(user, size)� � return render_to_string('avatar/avatar_tag.html', {'url': url,})

* This code is simplified

60 of 67

Easy to integrate: Template tags (going extensible)

class GravatarAvatarProvider:�� def get_avatar_url(self, user, size):� path = generate_gravatar_path(user)return urljoin(settings.AVATAR_GRAVATAR_BASE_URL, path)� � �class FacebookAvatarProvider:�� def get_avatar_url(self, user, size):� fb_id = get_facebook_id(user)return f'https://graph.facebook.com/{fb_id}/picture'

* This code is simplified

61 of 67

django-avatar calls them providers.�Others call them backends, services, etc.�They're all custom extensible abstractions,�custom helpers.

62 of 67

More helpers�==�More options of integration

63 of 67

  1. Use Django abstractions
  2. Make those abstractions even more reusable with:
    1. more methods
    2. more mixins
    3. and more helpers

Easy to integrate: Eliminating discontinuities

64 of 67

Easy to integrate: Much more to do

  • Break reusable model parts into abstract models
  • Break model filter logic into queryset methods
  • Break model table-level logic into managers methods
  • Break validation logic into validators
  • etc… check: djangoappschecklist.com

65 of 67

66 of 67

References

67 of 67

Thanks! Questions?

Slides are available at: bit.ly/djangoapps2017�Please contribute: github.com/vintasoftware/django-apps-checklist

Contact me at twitter.com/flaviojuvenal