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.

Efcaa677ee9679daf30ff903212ade61?s=128

James Van Dyne

May 19, 2018
Tweet

Transcript

  1. Building your MVP with Django Lessons Learned Building and Launching

    a SaaS @jamesvandyne
  2. Agenda 1. Introduction 2. What is a SaaS / MVP?

    3. Django Backend Lessons 4. Django Frontend Lessons 5. Q&A
  3. James Van Dyne ϰΝϯμΠϯɾδΣʔϜε @jamesvandyne BeProud, Inc.

  4. Why should you listen to me?

  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
  6. 4 Full Remote OK 4 7.5 Hour Standard Work Day

    4 Great co-workers 4 Work-life balance
  7. Slack Reminders

  8. Python Professional Programming 3 Coming soon

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

  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
  11. Software As A Service Minimum Viable Product

  12. What is an MVP

  13. image: anoda.mobi

  14. image: anoda.mobi

  15. What is your MVP? It depends

  16. Project Management for Software Teams kwoosh.com

  17. None
  18. Django has batteries (use them)

  19. Proper implementations are still a lot of work

  20. User

  21. What NOT to do

  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)
  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
  24. # apps.py class CompanyAccountConfig(AppConfig): name = 'company_accounts' def ready(self): import

    company_accounts.monkey_patches
  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'
  26. ManyToManyField

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

  28. User Account Access

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

  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'
  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
  32. Task Assignees

  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"
  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')
  35. Class Based Views

  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<pk>\d+)/$', UserProfileView.as_view(), name='user-profile'), ]
  37. The Promise 4 Write simpler, shorter code 4 Reuse existing

    components for quicker development 4 Easily mix in new functionality
  38. Do Class Based Views Deliver?

  39. It depends

  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
  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)
  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)
  43. Templates

  44. Server Side Rendering 4 Simple and well documented 4 I

    can spend more time in Python (!)
  45. Don't Repeat Yourself <!-- tasks.html--> <ul> {% for task in

    tasks %} {% include "task_item.html" } {% endfor %} </ul> <!-- tasks_item.html--> <li> <div class="task"> {{ task.title }} {% for assignee in task.assignees %} {% include "user_icon.html" %} {% endfor %} </div> </li>
  46. None
  47. Template Issues 4 Unexpected database calls 4 Cache invalidation

  48. Too slow for complex apps

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

    } {% endfor %} <!-- inner.html--> <h1>Hello, {{x}}</h1>
  50. Inline {% for x in loops %} <h1>Hello, {{x}}</h1> {%

    endfor %}
  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
  52. 270% faster

  53. !

  54. After

  55. Take Aways

  56. 1. Build Less Build Better

  57. 2. Think through your models

  58. 3. Avoid Class Based Views

  59. 4. Consider Rendering in the Client

  60. 5. Sometimes ! is the right solution

  61. Thank you kwoosh.com