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

Building your MVP with Django: Lessons Learned Building and Launching a SaaS

Building your MVP with Django: Lessons Learned Building and Launching a SaaS

A presentation given at DjangoCongress 2018 discussing lessons learned when building kwoosh.com with Django. Includes methods and techniques for helping you improve the maintainability of your Django service.

James Van Dyne

May 19, 2018
Tweet

More Decks by James Van Dyne

Other Decks in Technology

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  4. Why should you listen to
    me?

    View full-size slide

  5. 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

    View full-size slide

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

    View full-size slide

  7. Slack Reminders

    View full-size slide

  8. Python Professional
    Programming 3
    Coming soon

    View full-size slide

  9. work with me
    beproud.jp/careers/en/

    View full-size slide

  10. 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

    View full-size slide

  11. Software As A Service
    Minimum Viable Product

    View full-size slide

  12. What is an MVP

    View full-size slide

  13. image: anoda.mobi

    View full-size slide

  14. image: anoda.mobi

    View full-size slide

  15. What is your MVP?
    It depends

    View full-size slide

  16. Project Management for Software Teams
    kwoosh.com

    View full-size slide

  17. Django has batteries
    (use them)

    View full-size slide

  18. Proper implementations are
    still a lot of work

    View full-size slide

  19. What NOT to do

    View full-size slide

  20. # 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)

    View full-size slide

  21. # 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

    View full-size slide

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

    View full-size slide

  23. 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'

    View full-size slide

  24. ManyToManyField

    View full-size slide

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

    View full-size slide

  26. User Account Access

    View full-size slide

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

    View full-size slide

  28. 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'

    View full-size slide

  29. 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

    View full-size slide

  30. Task Assignees

    View full-size slide

  31. 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"

    View full-size slide

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

    View full-size slide

  33. Class Based Views

    View full-size slide

  34. 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'),
    ]

    View full-size slide

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

    View full-size slide

  36. Do Class Based Views
    Deliver?

    View full-size slide

  37. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  41. 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 %}


    View full-size slide

  42. Template Issues
    4 Unexpected database calls
    4 Cache invalidation

    View full-size slide

  43. Too slow for complex apps

    View full-size slide

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

    Hello, {{x}}

    View full-size slide

  45. Inline
    {% for x in loops %}
    Hello, {{x}}
    {% endfor %}

    View full-size slide

  46. 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

    View full-size slide

  47. 1. Build Less
    Build Better

    View full-size slide

  48. 2. Think through your
    models

    View full-size slide

  49. 3. Avoid Class Based Views

    View full-size slide

  50. 4. Consider Rendering in
    the Client

    View full-size slide

  51. 5. Sometimes ! is the
    right solution

    View full-size slide

  52. Thank you
    kwoosh.com

    View full-size slide