Slide 1

Slide 1 text

Building your MVP with Django Lessons Learned Building and Launching a SaaS @jamesvandyne

Slide 2

Slide 2 text

Agenda 1. Introduction 2. What is a SaaS / MVP? 3. Django Backend Lessons 4. Django Frontend Lessons 5. Q&A

Slide 3

Slide 3 text

James Van Dyne ϰΝϯμΠϯɾδΣʔϜε @jamesvandyne BeProud, Inc.

Slide 4

Slide 4 text

Why should you listen to me?

Slide 5

Slide 5 text

Why should you listen to me? 4 Been working with Django since around 1.0-ish 4 Worked on some of the largest Django sites in the US

Slide 6

Slide 6 text

4 Full Remote OK 4 7.5 Hour Standard Work Day 4 Great co-workers 4 Work-life balance

Slide 7

Slide 7 text

Slack Reminders

Slide 8

Slide 8 text

Python Professional Programming 3 Coming soon

Slide 9

Slide 9 text

work with me beproud.jp/careers/en/

Slide 10

Slide 10 text

Goals 4 Avoid the same mistakes I made 4 Help you make your code base more maintainable 4 Share some techniques that work well for me

Slide 11

Slide 11 text

Software As A Service Minimum Viable Product

Slide 12

Slide 12 text

What is an MVP

Slide 13

Slide 13 text

image: anoda.mobi

Slide 14

Slide 14 text

image: anoda.mobi

Slide 15

Slide 15 text

What is your MVP? It depends

Slide 16

Slide 16 text

Project Management for Software Teams kwoosh.com

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

Django has batteries (use them)

Slide 19

Slide 19 text

Proper implementations are still a lot of work

Slide 20

Slide 20 text

User

Slide 21

Slide 21 text

What NOT to do

Slide 22

Slide 22 text

# models.py class UserProfile(models.Model): user = models.OneToOneField(User) accounts = models.ManyToManyField(CompanyAccount) title = models.CharField(max_length=32, blank=True, null=True) TZ_CHOICES = [(tz, tz) for tz in common_timezones] timezone = models.CharField(max_length=30, default='UTC', choices=TZ_CHOICES)

Slide 23

Slide 23 text

# monkey_patches.py from django.contrib.auth.models import User from company_accounts.models import UserProfile def user_profile(self): return UserProfile.objects.get(pk=self.pk) User.profile = user_profile

Slide 24

Slide 24 text

# apps.py class CompanyAccountConfig(AppConfig): name = 'company_accounts' def ready(self): import company_accounts.monkey_patches

Slide 25

Slide 25 text

A better way from django.contrib.auth.models import AbstractBaseUser class User(AbstractBaseUser): email = models.EmailField(_('email address'), max_length=254, unique=True) first_name = models.CharField(_('first name'), max_length=30, blank=True) last_name = models.CharField(_('last name'), max_length=30, blank=True) is_confirmed = models.BooleanField(default=False) title = models.CharField(max_length=32, blank=True, null=True) timezone = models.CharField(max_length=30, default='UTC', choices=TZ_CHOICES) USERNAME_FIELD = 'email' class Meta: db_table = 'users'

Slide 26

Slide 26 text

ManyToManyField

Slide 27

Slide 27 text

4 2 Examples: 1. User Account Access 2. Task Assignees

Slide 28

Slide 28 text

User Account Access

Slide 29

Slide 29 text

Before class UserProfile(models.Model): user = models.OneToOneField(User) accounts = models.ManyToManyField(CompanyAccount)

Slide 30

Slide 30 text

After from django.contrib.auth import get_user_model from accounts.models import Account class AccountAccess(StatusMixin, models.Model): account = models.ForeignKey(Account) user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='user_%(class)s') class Meta: unique_together = ('user', 'account',) db_table = 'account_access'

Slide 31

Slide 31 text

class StatusMixin(BaseMixin): ACTIVE = 1 INACTIVE = 0 DELETED = -1 STATUS_CHOICES = ( (ACTIVE, _('Active')), (INACTIVE, _('Inactive')), (DELETED, _('Deleted')), ) status = models.IntegerField(choices=STATUS_CHOICES, default=ACTIVE) objects = StatusMixinManager() all_objects = models.Manager() class Meta: abstract = True

Slide 32

Slide 32 text

Task Assignees

Slide 33

Slide 33 text

Before class Task(ItemData): cache_key = "task_" _clearing = False _assignees_on_create = None assigned_to = models.ManyToManyField('auth.User', blank=True, related_name='related_name') state = models.IntegerField(choices=STATE_CHOICES, default=TODO) is_priority = models.BooleanField(default=False) class Meta: verbose_name_plural = "Tasks"

Slide 34

Slide 34 text

After # models.py class Task(StatusMixin, models.Model): assigned_to = models.ManyToManyField(get_user_model(), through='AssignedTo', through_fields=( 'task', 'user')) class AssignedTo(models.Model): user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='user_%(class)s') task = models.ForeignKey('tasks.Task', on_delete=models.CASCADE, related_name='task_%(class)s')

Slide 35

Slide 35 text

Class Based Views

Slide 36

Slide 36 text

What are Class Based Views? #views.py from django.views.generic.detail import DetailView class UserProfileView(DetailView): model = User # urls.py urlpatterns = [ url(r'^(?P\d+)/$', UserProfileView.as_view(), name='user-profile'), ]

Slide 37

Slide 37 text

The Promise 4 Write simpler, shorter code 4 Reuse existing components for quicker development 4 Easily mix in new functionality

Slide 38

Slide 38 text

Do Class Based Views Deliver?

Slide 39

Slide 39 text

It depends

Slide 40

Slide 40 text

Before class BasicProfileView(KwooshModelFormView, ProfilePageNav, AccountBasicFilterbar): form_class = KwooshProfileForm template_name = "users/profile_basic.html" page_template = "users/parts/basic.html" active_pagenav = "profile" active_filterbar = 'profile' page_id = "profile" def get_form(self, form_class=None): if form_class is None: form_class = self.form_class return form_class(self.request.POST or None, **self.get_form_kwargs()) def get_form_kwargs(self): self.object = get_object_or_404(User, pk=self.kwargs['pk']) if UserAccountAccess.objects.filter(user=self.object, account__pk__in=self.request.accounts).exists(): return { 'instance': self.object, 'request': self.request } else: raise exceptions.PermissionDenied

Slide 41

Slide 41 text

def get_context_data(self, **kwargs): if 'form' not in kwargs: kwargs['form'] = self.get_form() # context updating return context def form_invalid(self, form): context = self.get_context_data(form=form) return self.render_to_response(context) def form_valid(self, form): form.save() context = self.get_context_data(form=form) messages.success(self.request, "Saved Profile Successfully", extra_tags="local") return self.render_to_response(context)

Slide 42

Slide 42 text

After @authenticated @set_instance_object('users.models.User') def user(request, pk): # Show user def get(request, pk): form = EditUser(request=request, instance=request.instance) ... return response # Edit user @ensure_csrf_cookie @owns_instance_user def post(request, pk): # Create/Process the form return response return resolve_http_method({'get': get, 'post': post}, request, pk=pk)

Slide 43

Slide 43 text

Templates

Slide 44

Slide 44 text

Server Side Rendering 4 Simple and well documented 4 I can spend more time in Python (!)

Slide 45

Slide 45 text

Don't Repeat Yourself
    {% for task in tasks %} {% include "task_item.html" } {% endfor %}
  • {{ task.title }} {% for assignee in task.assignees %} {% include "user_icon.html" %} {% endfor %}
  • Slide 46

    Slide 46 text

    No content

    Slide 47

    Slide 47 text

    Template Issues 4 Unexpected database calls 4 Cache invalidation

    Slide 48

    Slide 48 text

    Too slow for complex apps

    Slide 49

    Slide 49 text

    Include {% for x in loops %} {% include "inner.html" } {% endfor %}

    Hello, {{x}}

    Slide 50

    Slide 50 text

    Inline {% for x in loops %}

    Hello, {{x}}

    {% endfor %}

    Slide 51

    Slide 51 text

    Results run include inline 1 0.09034180641174316 0.030562877655029297 2 0.08458805084228516 0.032724857330322266 3 0.08182239532470703 0.03140902519226074 4 0.08819007873535156 0.03286027908325195 Avg 86.24ms Avg: 31.89

    Slide 52

    Slide 52 text

    270% faster

    Slide 53

    Slide 53 text

    !

    Slide 54

    Slide 54 text

    After

    Slide 55

    Slide 55 text

    Take Aways

    Slide 56

    Slide 56 text

    1. Build Less Build Better

    Slide 57

    Slide 57 text

    2. Think through your models

    Slide 58

    Slide 58 text

    3. Avoid Class Based Views

    Slide 59

    Slide 59 text

    4. Consider Rendering in the Client

    Slide 60

    Slide 60 text

    5. Sometimes ! is the right solution

    Slide 61

    Slide 61 text

    Thank you kwoosh.com