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. Agenda 1. Introduction 2. What is a SaaS / MVP?

    3. Django Backend Lessons 4. Django Frontend Lessons 5. Q&A
  2. 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
  3. 4 Full Remote OK 4 7.5 Hour Standard Work Day

    4 Great co-workers 4 Work-life balance
  4. 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
  5. # 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)
  6. # 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
  7. 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'
  8. 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'
  9. 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
  10. 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"
  11. 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')
  12. 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'), ]
  13. The Promise 4 Write simpler, shorter code 4 Reuse existing

    components for quicker development 4 Easily mix in new functionality
  14. 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
  15. 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)
  16. 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)
  17. Server Side Rendering 4 Simple and well documented 4 I

    can spend more time in Python (!)
  18. 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>
  19. Include {% for x in loops %} {% include "inner.html"

    } {% endfor %} <!-- inner.html--> <h1>Hello, {{x}}</h1>
  20. 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
  21. !