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 Slide

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

    View Slide

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

    View Slide

  4. Why should you listen to
    me?

    View 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 Slide

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

    View Slide

  7. Slack Reminders

    View Slide

  8. Python Professional
    Programming 3
    Coming soon

    View Slide

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

    View 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 Slide

  11. Software As A Service
    Minimum Viable Product

    View Slide

  12. What is an MVP

    View Slide

  13. image: anoda.mobi

    View Slide

  14. image: anoda.mobi

    View Slide

  15. What is your MVP?
    It depends

    View Slide

  16. Project Management for Software Teams
    kwoosh.com

    View Slide

  17. View Slide

  18. Django has batteries
    (use them)

    View Slide

  19. Proper implementations are
    still a lot of work

    View Slide

  20. User

    View Slide

  21. What NOT to do

    View Slide

  22. # 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 Slide

  23. # 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 Slide

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

    View Slide

  25. 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 Slide

  26. ManyToManyField

    View Slide

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

    View Slide

  28. User Account Access

    View Slide

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

    View Slide

  30. 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 Slide

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

  32. Task Assignees

    View Slide

  33. 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 Slide

  34. 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 Slide

  35. Class Based Views

    View Slide

  36. 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 Slide

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

    View Slide

  38. Do Class Based Views
    Deliver?

    View Slide

  39. It depends

    View Slide

  40. 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 Slide

  41. 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 Slide

  42. 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 Slide

  43. Templates

    View Slide

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

    View Slide

  45. 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 Slide

  46. View Slide

  47. Template Issues
    4 Unexpected database calls
    4 Cache invalidation

    View Slide

  48. Too slow for complex apps

    View Slide

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

    Hello, {{x}}

    View Slide

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

    View Slide

  51. 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 Slide

  52. 270% faster

    View Slide

  53. !

    View Slide

  54. After

    View Slide

  55. Take Aways

    View Slide

  56. 1. Build Less
    Build Better

    View Slide

  57. 2. Think through your
    models

    View Slide

  58. 3. Avoid Class Based Views

    View Slide

  59. 4. Consider Rendering in
    the Client

    View Slide

  60. 5. Sometimes ! is the
    right solution

    View Slide

  61. Thank you
    kwoosh.com

    View Slide