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

2017 - Your Django app is a User Interface

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

PyBay

August 21, 2017
Tweet

More Decks by PyBay

Other Decks in Programming

Transcript

  1. "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/
  2. 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.
  3. 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
  4. 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
  5. ‒ 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
  6. ‒ 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
  7. ‒ Show how to use, add sample code for the

    most common use-cases Start with the README
  8. 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
  9. 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.
  10. ‒ 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
  11. email1 = mail.EmailMessage( subject='Hello', to='[email protected]', ) # vs… email1 =

    mail.EmailMessage( subject='Hello', to=['[email protected]'], ) Avoid cumbersome inputs
  12. assert not isinstance(to, basestring), \ '"to" argument must be a

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

    must be a list or tuple') code.djangoproject.com/ticket/23924
  14. To abstract in APIs draw away from the physical how

    nature of something to focus on the what nature
  15. message = AnymailMessage( subject="Welcome", body="Welcome to our site", to=["New User

    <[email protected]>"], # extra anymail attrs tags=["Onboarding"], metadata={ "onboarding_experiment": "var 1"}, track_clicks=True ) message.send() github.com/anymail/django-anymail
  16. ‒ 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. # 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
  23. Everybody loves the declarative interfaces ‒ Great examples in the

    ecosystem: ◦ django-import-export ◦ django-haystack ◦ django-rest-framework ◦ social-app-django
  24. Asymmetry of behavior -> Asymmetry of form The Little Manual

    of API Design people.mpi-inf.mpg.de/~jblanche/api-design.pdf
  25. 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
  26. 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
  27. 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
  28. Options of integration in purple Unsolved Solved Slightly overkill Way

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

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

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

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

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

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

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

    client wants a filter for price field New requirements
  36. Integration Work Benefit to project What if django-filter didn't have

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

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

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

    - and preferably only one - obvious way to do it"
  40. 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
  41. 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)
  42. - 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
  43. ‒ 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
  44. 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
  45. ‒ 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
  46. ‒ 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
  47. attribute vs. method smell Every attribute that could be a

    method is a missed option of integration
  48. ‒ 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
  49. 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
  50. $ 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).
  51. 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
  52. Suggestion for Django: Perhaps every "ensure", "remember to", "don't forget",

    or similar warnings on Django docs should become a new system check…
  53. ‒ 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
  54. Thanks! Questions? Feel free to reach me: twitter.com/flaviojuvenal [email protected] vinta.com.br

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