Upgrade to Pro — share decks privately, control downloads, hide ads and more …

2017 - Your Django app is a User Interface

Db2ee812bdc6fd057f8f4209c08b6f63?s=47 PyBay
August 21, 2017

2017 - Your Django app is a User Interface

Description

Usability heuristics are a useful way to evaluate an interface. An often overlooked aspect of Django apps is they’re interfaces too, one that connects the developer with a reusable solution. In this talk, we’ll learn how to apply usability concepts to Django apps to make them better to (re)use.

Abstract

Django docs say "Reusability is the way of life in Python. You only need to write the parts that make your project unique". The Django way to write reusable code is Django apps, which are straightforward to write. The vast quantity of apps available in PyPI and Django Packages proves that.

However, there is one overlooked aspect of apps: they are an interface between the developer and a reusable solution for a problem. Therefore, as any interface, Usability Heuristics should be used to evaluate Django apps efficacy. In this talk, we'll learn how to apply Usability Heuristics to Django apps to make them better to (re)use.

Talk outline:

Unix Philosophy and Django apps concept
Aesthetic and minimalist design
How to design for the 90% use case
Progressive disclosure and Affordance
Docs first
How to write beautiful app code with declarative programming
How to write simple app code by minimizing state
Consistency and Recognition rather than recall
How common Django idioms increase recognition
How existing Django abstractions help increase recognition
How separating concerns with Django abstractions increase recognition
Flexibility and efficiency of use
How making the other 10% use case possible with an extensible granular API
The concept of Integration Discontinuity
How to break Django abstractions to increase extensibility
How a granular API allows composition of apps
Error prevention and recovery
How to use Django system check framework to prevent errors and give tips
How to fail-fast if an error occurs, preventing some unexpected state
djangoappschecklist.com
How the community can help define a good practices checklist

Bio

Web developer from Brazil. Loves beautiful high-quality products, from UX to code, and will defend them against unreasonable deadlines and crazy features. Partner at Vinta (https://www.vinta.com.br/), a web consultancy specialized in building products with React and Django.

https://www.youtube.com/watch?v=Mnzvjn1v1CY

Db2ee812bdc6fd057f8f4209c08b6f63?s=128

PyBay

August 21, 2017
Tweet

Transcript

  1. Your Django app is a User Interface Flávio Juvenal @flaviojuvenal

    vintasoftware.com
  2. None
  3. "Reusability is the way of life in Python. You only

    need to write the parts that make your project unique" docs.djangoproject.com/en/1.11/ intro/reusable-apps/
  4. Think about Django apps as APIs to reusable solutions

  5. An API is a UI

  6. A good UI has: Simplicity Flexibility Consistency Safety

  7. Simplicity

  8. Zen of Python Beautiful is better than ugly. Explicit is

    better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts.
  9. Simplicity - Idea 1: Focus on the 80% use cases

  10. 80% just use: add app, provide input 15% customize: change

    settings, inherit classes 4% control: assume flow, use helpers, mixins, etc 1% fork Focus on the 80% use cases
  11. 80% just use: add app, provide input 15% customize: change

    settings, inherit classes 4% control: assume flow, use helpers, mixins, etc 1% fork Focus on the 80% use cases Simplicity Flexiblity
  12. 80% use cases: Start with the README www.kennethreitz.org/essays/ how-i-develop-things-and-why

  13. ‒ Pitch to your users: Why this app exists? What

    it solves? To which extent? ◦ django-js-reverse: "Javascript URL handling for Django that doesn't hurt" ◦ django-impersonate: "Simple app to allow superusers to 'impersonate' other non-superuser accounts" Start with the README
  14. ‒ Avoid "and" in your app description, break in two

    apps if necessary: ◦ my-user-app: "Django app to handle user authentication and profiles" ◦ Why not 2 apps? ▪ my-auth-app ▪ my-profiles-app Start with the README
  15. ‒ Show how to use, add sample code for the

    most common use-cases Start with the README
  16. 80% use cases: Know your users

  17. Know your users ‒ Test the app design with other

    developers ◦ Ask what they need ◦ Ask them to use ‒ Measure how app is being used ◦ Figure out the 80% more common use cases ◦ Know what the other 20% need to extend
  18. Simplicity - Idea 2: Reduce clutter for the 80% use

    cases
  19. Reduce clutter: Progressive disclosure

  20. None
  21. docs.djangoproject.com/en/1.11/ref/contrib/postgres/search/ # Simple Pizza.objects.filter( description__search='egg') # Advanced Entry.objects.annotate( search=SearchVector( 'description',

    config='french') ).filter( search=SearchQuery( 'œuf', config='french'))
  22. Progressive disclosure == Good defaults

  23. Have good defaults ‒ Make assumptions: ◦ Decide for the

    80% of users ◦ Require only the essentials ‒ Many defaults to set: ◦ Settings ◦ Argument values ◦ Argument order ◦ Behavior ◦ Environment ◦ etc.
  24. ‒ 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... Have good defaults
  25. ‒ Except for one thing... Have good defaults django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips docs.djangoproject.com/en/1.11/ref/settings/#std:setting-INTERNAL_IPS

  26. Bad defaults propagate... be careful!

  27. Reduce clutter: Avoid cumbersome inputs

  28. email1 = mail.EmailMessage( subject='Hello', to='to1@example.com', ) # vs… email1 =

    mail.EmailMessage( subject='Hello', to=['to1@example.com'], ) Avoid cumbersome inputs
  29. assert not isinstance(to, basestring), \ '"to" argument must be a

    list or tuple' Avoid cumbersome inputs code.djangoproject.com/ticket/7655
  30. Avoid cumbersome inputs if isinstance(to, str): raise TypeError( '"to" argument

    must be a list or tuple') code.djangoproject.com/ticket/23924
  31. Reduce clutter: Create abstractions for the 80% use-cases

  32. To abstract draw away from the physical nature of something

  33. Low abstraction

  34. Medium abstraction

  35. High abstraction

  36. None
  37. To abstract in APIs draw away from the physical how

    nature of something to focus on the what nature
  38. @app.task def add(x, y): return x + y github.com/celery/celery

  39. message = AnymailMessage( subject="Welcome", body="Welcome to our site", to=["New User

    <user1@example.com>"], # extra anymail attrs tags=["Onboarding"], metadata={ "onboarding_experiment": "var 1"}, track_clicks=True ) message.send() github.com/anymail/django-anymail
  40. anymail.readthedocs.io/en/stable/esps/

  41. The Law of Leaky Abstractions All abstractions leak www.joelonsoftware.com/2002/11/11/ the-law-of-leaky-abstractions/

  42. ‒ Back to Celery, workers can crash: Task app.tasks.send_notification with

    id 123 raised exception: "WorkerLostError('Worker exited prematurely: signal 15 (SIGTERM).',)" ‒ Solution? acks_late=True + idempotent tasks Law of Leaky Abstractions
  43. Law of Leaky Abstractions ‒ It's impossible to abstract perfectly

    All abstractions lie ‒ Sometimes leaks are necessary ◦ "Complex is better than complicated" ◦ Better embrace complexity than creating complicated situations
  44. RPC guido_profile = Facebook.get_profile('guido') guido_profile.set_name("Benevolent Dictator For Life") REST PUT

    /profiles/guido/ If-Unmodified-Since: Sun, 11 Jul 2010 11:11:11 GMT name=Benevolent Dictator For Life Complex is better than complicated
  45. Don't abstract what cannot be abstracted

  46. Consistency

  47. Consistency - Idea 1: Recognition rather than recall

  48. Press F1 on Windows == Help

  49. Press F1 on Windows ==

  50. Press F1 on Windows == www.computerworld.com/article/2520194/malware-vulnerabilities/ microsoft--don-t-press-f1-key-in-windows-xp.html

  51. Recognition rather than recall: Use Django idioms

  52. my_app/ management/ migrations/ templates/ templatetags/ __init__.py admin.py apps.py checks.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
  53. my_app/ management/ migrations/ templates/ templatetags/ __init__.py admin.py apps.py checks.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
  54. ‒ Provide template tags for presenting data ‒ django-avatar e.g.:

    Use Django idioms
  55. 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) - Respect the configurability of class-based views - django-authtools PasswordResetView e.g.: Use Django idioms Django attrs/methods
  56. # Search all live EventPages # that are under the

    events index EventPage.objects\ .live()\ .descendant_of(events_index)\ .search("Event") ‒ Use custom queryset methods for chaining filters ‒ wagtail search e.g.: Use Django idioms
  57. Recognition rather than recall: Make declarative interfaces

  58. class PostAdmin(admin.ModelAdmin): fields = ('author', 'title', 'text', 'created', 'published') Everybody

    loves the admin
  59. Everybody loves the declarative interfaces

  60. Everybody loves the declarative interfaces ‒ Great examples in the

    ecosystem: ◦ django-import-export ◦ django-haystack ◦ django-rest-framework ◦ social-app-django
  61. Consistency - Idea 2: Keep similar things together and different

    things apart
  62. sorted(numbers) != numbers.sort()

  63. Asymmetry of behavior -> Asymmetry of form The Little Manual

    of API Design people.mpi-inf.mpg.de/~jblanche/api-design.pdf
  64. utils.py? No! utils/... Yes django-haystack utils/ example: geo.py, highlighting.py, loading.py,

    etc…
  65. Beware of false consistency

  66. class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer @detail_route(methods=['post']) def

    set_password(self, request, pk=None): # ... @list_route() def recent_users(self, request): # ... Beware of false consistency
  67. class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer @detail_route( methods=['post'],

    permission_classes=[IsAdminOrIsSelf]) def set_password(self, request, pk=None): # … Beware of false consistency
  68. Flexibility

  69. 80% just use: add app, provide input 15% customize: change

    settings, inherit classes 4% control: assume flow, use helpers, mixins, etc 1% fork Make the 20% possible
  70. Flexibility - Idea 1: Increase granularity

  71. Integration Discontinuity Designing and Evaluating Reusable Components mollyrocket.com/casey/stream_0028.html

  72. Options of integration in purple Unsolved Solved Slightly overkill Way

    overkill Unsolved Way overkill Discontinuity
  73. Imagine you're building a web app for an online store.

    They're asking for a feature to filter products by exact price
  74. Options of integration Integration Work Benefit to project Use case:

    client wants a filter for price field
  75. Options of integration Integration Work Benefit to project Use case:

    client wants a filter for price field Starting here
  76. class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = ['price']

    Implementing with django-filter
  77. New requirement: filter products by price, greater than equal and

    less than equal
  78. Options of integration Integration Work Benefit to project Use case:

    client wants a filter for price field New requirements
  79. Options of integration Integration Work Benefit to project Use case:

    client wants a filter for price field New requirements
  80. class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = {

    'price': ['lte', 'gte'], } Implementing with django-filter
  81. New requirement: filter products by price, greater than equal and

    less than equal, but include approximate prices
  82. Options of integration Integration Work Benefit to project Use case:

    client wants a filter for price field New requirements
  83. class ApproxPriceFilter(django_filters.NumberFilter): # ... class ProductFilter(django_filters.FilterSet): price = ApproxPriceFilter() class

    Meta: model = Product Implementing with django-filter
  84. Integration Work Benefit to project What if django-filter didn't have

    multiple levels of abstractions? Discontinuity New requirements
  85. Increase granularity: Have multiple levels of abstraction Alex Martelli -

    Good API design www.youtube.com/watch?v=LsfrMjcIudA
  86. What about the Zen of Python? "There should be one

    - and preferably only one - obvious way to do it"
  87. What about the Zen of Python? "There should be one

    - and preferably only one - obvious way to do it"
  88. obvious way == 80% non-obvious way == 20%

  89. obvious way == simplicity, high-level non-obvious way == flexibility, low-level

  90. Increase granularity: Ensure extensibility of Django idioms

  91. my_app/ management/ migrations/ templates/ templatetags/ __init__.py admin.py apps.py checks.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
  92. Ensure extensibility of Django idioms ‒ Use helpers/providers/backends to encapsulate

    logic. Accept custom ones class GravatarAvatarProvider: def get_avatar_url(self, user, size): return facebook_api.get_picture_url( user, size) @register.simple_tag def avatar_url(user, size): for provider_path in settings.AVATAR_PROVIDERS: provider = import_string(provider_path) url = provider.get_avatar_url(user, size)
  93. - Break behaviours of class-based views in attributes and methods

    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) Ensure extensibility of Django idioms Custom attrs/methods
  94. ‒ Use custom querysets instead of managers to support chaining

    intertwined with custom filtering # Search future EventPages EventPage.objects\ .filter(date__gt=timezone.now())\ .search("Hello world!") Ensure extensibility of Django idioms
  95. Flexibility - Idea 2: Increase opportunities for extension

  96. mock.patch smell Every mock.patch is a missed option of integration

    mauveweb.co.uk/posts/2014/09/ every-mock-patch-is-a-little-smell.html
  97. ‒ One more mock.patch == one less integration: @patch('django.core.mail.send_mail') def

    test_sends_email(send_email_mock): # ...call logic send_email_mock.assert_called_once() mock.patch smell
  98. ‒ One more mock.patch == one less integration: # tests.py

    def test_sends_email(): # ...call logic self.assertEqual( len(mail.outbox), 1) # settings/test.py EMAIL_BACKEND = \ 'django.core.mail.backends.locmem.EmailBackend' mock.patch smell
  99. attribute vs. method smell Every attribute that could be a

    method is a missed option of integration
  100. None
  101. None
  102. Safety

  103. Safety - Idea 1: Think your Django app user is

    a serial-killer
  104. Safety - Idea 1: Think your Django app user is

    a serial-killer
  105. Safety - Idea 1: Don't do dangerous things by default

  106. ‒ What if no fields is specified? class UserSerializer(serializers.ModelSerializer): class

    Meta: model = User # fields = [...] ‒ Show all or nothing? Both Django and DRF changed to the safer behavior Don't do dangerous things by default
  107. github.com/blog/1068-public-key-security-vulnerability-and-mitigation

  108. Safety - Idea 2: Prevent common mistakes

  109. None
  110. Prevent common mistakes: Bundle your app with Django system checks

  111. my_app/ management/ migrations/ templates/ templatetags/ __init__.py admin.py apps.py checks.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
  112. python manage.py check docs.djangoproject.com/en/dev/topics/checks/

  113. $ cat example_project/urls.py urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^app1/', include('app1.urls',

    namespace='app1')), url(r'^app2/', include('app2.urls', namespace='app1')), ] $ python manage.py check System check identified some issues: WARNINGS: ?: (urls.W005) URL namespace 'app1' isn't unique. You may not be able to reverse all URLs in this namespace System check identified 1 issue (0 silenced).
  114. class ArrayField(base_field, size=None, **options) If you give the field a

    default, ensure it’s a callable. Incorrectly using default=[] creates a mutable default shared between all instances of ArrayField. docs.djangoproject.com/en/1.11/ref/ contrib/postgres/fields/#arrayfield
  115. Suggestion for Django: Perhaps every "ensure", "remember to", "don't forget",

    or similar warnings on Django docs should become a new system check…
  116. Prevent common mistakes: If you can't prevent it, fail-fast

  117. ‒ Raise exceptions ASAP if the developer made a mistake:

    ◦ django-filter raises ImproperlyConfigured if user forgets to set filterset_class in a FilterView ◦ django-rest-framework now raises TypeError if user forgets to set fields in a ModelSerializer Fail-fast
  118. djangoappschecklist.com

  119. Thanks! Questions? Feel free to reach me: twitter.com/flaviojuvenal flavio@vinta.com.br vinta.com.br

    This and other slides from Vinta: bit.ly/vinta2017 References here