Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

"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/

Slide 4

Slide 4 text

Think about Django apps as APIs to reusable solutions

Slide 5

Slide 5 text

An API is a UI

Slide 6

Slide 6 text

A good UI has: Simplicity Flexibility Consistency Safety

Slide 7

Slide 7 text

Simplicity

Slide 8

Slide 8 text

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.

Slide 9

Slide 9 text

Simplicity - Idea 1: Focus on the 80% use cases

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

80% use cases: Start with the README www.kennethreitz.org/essays/ how-i-develop-things-and-why

Slide 13

Slide 13 text

‒ 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

Slide 14

Slide 14 text

‒ 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

Slide 15

Slide 15 text

‒ Show how to use, add sample code for the most common use-cases Start with the README

Slide 16

Slide 16 text

80% use cases: Know your users

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Simplicity - Idea 2: Reduce clutter for the 80% use cases

Slide 19

Slide 19 text

Reduce clutter: Progressive disclosure

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

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'))

Slide 22

Slide 22 text

Progressive disclosure == Good defaults

Slide 23

Slide 23 text

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.

Slide 24

Slide 24 text

‒ 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

Slide 25

Slide 25 text

‒ 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

Slide 26

Slide 26 text

Bad defaults propagate... be careful!

Slide 27

Slide 27 text

Reduce clutter: Avoid cumbersome inputs

Slide 28

Slide 28 text

email1 = mail.EmailMessage( subject='Hello', to='to1@example.com', ) # vs… email1 = mail.EmailMessage( subject='Hello', to=['to1@example.com'], ) Avoid cumbersome inputs

Slide 29

Slide 29 text

assert not isinstance(to, basestring), \ '"to" argument must be a list or tuple' Avoid cumbersome inputs code.djangoproject.com/ticket/7655

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Reduce clutter: Create abstractions for the 80% use-cases

Slide 32

Slide 32 text

To abstract draw away from the physical nature of something

Slide 33

Slide 33 text

Low abstraction

Slide 34

Slide 34 text

Medium abstraction

Slide 35

Slide 35 text

High abstraction

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

To abstract in APIs draw away from the physical how nature of something to focus on the what nature

Slide 38

Slide 38 text

@app.task def add(x, y): return x + y github.com/celery/celery

Slide 39

Slide 39 text

message = AnymailMessage( subject="Welcome", body="Welcome to our site", to=["New User "], # extra anymail attrs tags=["Onboarding"], metadata={ "onboarding_experiment": "var 1"}, track_clicks=True ) message.send() github.com/anymail/django-anymail

Slide 40

Slide 40 text

anymail.readthedocs.io/en/stable/esps/

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

‒ 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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Don't abstract what cannot be abstracted

Slide 46

Slide 46 text

Consistency

Slide 47

Slide 47 text

Consistency - Idea 1: Recognition rather than recall

Slide 48

Slide 48 text

Press F1 on Windows == Help

Slide 49

Slide 49 text

Press F1 on Windows ==

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Recognition rather than recall: Use Django idioms

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

‒ Provide template tags for presenting data ‒ django-avatar e.g.: Use Django idioms

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

# 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

Slide 57

Slide 57 text

Recognition rather than recall: Make declarative interfaces

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Everybody loves the declarative interfaces

Slide 60

Slide 60 text

Everybody loves the declarative interfaces ‒ Great examples in the ecosystem: ○ django-import-export ○ django-haystack ○ django-rest-framework ○ social-app-django

Slide 61

Slide 61 text

Consistency - Idea 2: Keep similar things together and different things apart

Slide 62

Slide 62 text

sorted(numbers) != numbers.sort()

Slide 63

Slide 63 text

Asymmetry of behavior -> Asymmetry of form The Little Manual of API Design people.mpi-inf.mpg.de/~jblanche/api-design.pdf

Slide 64

Slide 64 text

utils.py? No! utils/... Yes django-haystack utils/ example: geo.py, highlighting.py, loading.py, etc…

Slide 65

Slide 65 text

Beware of false consistency

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Flexibility

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Flexibility - Idea 1: Increase granularity

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Options of integration in purple Unsolved Solved Slightly overkill Way overkill Unsolved Way overkill Discontinuity

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = ['price'] Implementing with django-filter

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = { 'price': ['lte', 'gte'], } Implementing with django-filter

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

class ApproxPriceFilter(django_filters.NumberFilter): # ... class ProductFilter(django_filters.FilterSet): price = ApproxPriceFilter() class Meta: model = Product Implementing with django-filter

Slide 84

Slide 84 text

Integration Work Benefit to project What if django-filter didn't have multiple levels of abstractions? Discontinuity New requirements

Slide 85

Slide 85 text

Increase granularity: Have multiple levels of abstraction Alex Martelli - Good API design www.youtube.com/watch?v=LsfrMjcIudA

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

obvious way == 80% non-obvious way == 20%

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

Increase granularity: Ensure extensibility of Django idioms

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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)

Slide 93

Slide 93 text

- 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

Slide 94

Slide 94 text

‒ 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

Slide 95

Slide 95 text

Flexibility - Idea 2: Increase opportunities for extension

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

‒ 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

Slide 98

Slide 98 text

‒ 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

Slide 99

Slide 99 text

attribute vs. method smell Every attribute that could be a method is a missed option of integration

Slide 100

Slide 100 text

No content

Slide 101

Slide 101 text

No content

Slide 102

Slide 102 text

Safety

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

Safety - Idea 1: Don't do dangerous things by default

Slide 106

Slide 106 text

‒ 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

Slide 107

Slide 107 text

github.com/blog/1068-public-key-security-vulnerability-and-mitigation

Slide 108

Slide 108 text

Safety - Idea 2: Prevent common mistakes

Slide 109

Slide 109 text

No content

Slide 110

Slide 110 text

Prevent common mistakes: Bundle your app with Django system checks

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

python manage.py check docs.djangoproject.com/en/dev/topics/checks/

Slide 113

Slide 113 text

$ 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).

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

Suggestion for Django: Perhaps every "ensure", "remember to", "don't forget", or similar warnings on Django docs should become a new system check…

Slide 116

Slide 116 text

Prevent common mistakes: If you can't prevent it, fail-fast

Slide 117

Slide 117 text

‒ 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

Slide 118

Slide 118 text

djangoappschecklist.com

Slide 119

Slide 119 text

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