Ionic and Django Kickoff tutorial: Scaffold your Ionic and Django projects ready to use in few seconds

Each time i have a new mobile application to develop, i need to create the Ionic project and the Django project as backend.
It is always a boring and repetitive task and a lot of copy/paste code from previous projects. So i decided to create a tool which will scaffold the ionic and django projects code for me.

Ionic and Django Kickoff project

I have made this tool available on a dedicated website. You will find basic free plan and paid plans.

To let the magic happens, all we have to do is to create a django models.py file and just upload it.

Then we will receive an email with a download link containing our code ready to use.

The generated code will be using Django 3.0 and Ionic 5.0 (Angular 10 version) + Capacitor 2.0

Design and implement a django models.py file to scaffold your project

Each project needs entities to deal with business cases. Let's say we want to build an eShop app for a restaurant or any kind of shops (such as bakery,...).

Entities, could be:

EntityRole
UserUsers can register/login into our app
ShopInformation about the shop such as the address, the opening rules, the closing rules...
ProductsObviously shops are selling products.
CategoriesA product belongs to a category
NewsSome actualities about the shop to communicate with users
OrderUsers should be able to order products

These few entities are minimal requirements but we could imagine a lot more such as payment, delivery, dealing with promotion, vat...
And as a minimal requirement you will need to create CRUD methods for each entity (create, read, update, delete) and some other methods to find or filter data.

It's a lot of required code that we need to create twice : in the Ionic application and on the Django backend project.

So since good developers are lazy one, let's see how to let Ionic and Django kickoff website do the job for us.

Django models.py file for our Ionic eShop app

One implementation could be:

# Models for an eShop.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import urllib
import uuid
from urllib.request import urlopen

from django.contrib.auth.base_user import BaseUserManager, AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.shortcuts import reverse
from django.db import models
from django.utils.encoding import smart_str
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.datetime_safe import datetime
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.utils import timezone
from django.contrib.gis.db import models

WEEKDAYS = [
    (1, _("Monday")),
    (2, _("Tuesday")),
    (3, _("Wednesday")),
    (4, _("Thursday")),
    (5, _("Friday")),
    (6, _("Saturday")),
    (0, _("Sunday")),
]


class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = models.EmailField(_('email address'), unique=True)
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
    date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
    is_active = models.BooleanField(_('active'), default=True)
    is_staff = models.BooleanField(_('staff'), default=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)

    objects = UserManager()
    
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

class News(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(max_length=250)
    text = models.TextField()
    image = models.ImageField(upload_to="media")
    createdAt = models.DateTimeField(auto_now_add=True)
    updatedAt = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = _('News')
        verbose_name_plural = _('News')
        ordering = ['-createdAt']

    def getThumb(self):
        return mark_safe('<img src="%s" style="width:100px;"/>' % self.image.url)
    getThumb.short_description = 'Thumbnail'


class Shop(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=100, verbose_name=_('name'))
    logo = models.ImageField(null=True, blank=True)
    address = models.CharField(
        max_length=255, default='', blank=True, verbose_name=_('address'))
    zipCode = models.CharField(
        max_length=20, default='', blank=True, verbose_name=_('zipCode'))
    city = models.CharField(max_length=100, default='',
                            blank=True, verbose_name=_('city'))
    phone = models.CharField(max_length=20, default='',
                             blank=True, verbose_name=_('phone'))
    location = models.PointField(null=True, blank=True)
    email = models.EmailField(max_length=200, verbose_name=_('email'))
    facebookUrl = models.CharField(
        max_length=100, verbose_name=_('Facebook'), null=True, blank=True)
    twitterUrl = models.CharField(
        max_length=100, verbose_name=_('Twitter'), null=True, blank=True)
    instagramUrl = models.CharField(
        max_length=100, verbose_name=_('Instagram'), null=True, blank=True)
    googleUrl = models.CharField(
        max_length=100, verbose_name=_('Google'), null=True, blank=True)
    createdAt = models.DateTimeField(auto_now_add=True)
    updatedAt = models.DateTimeField(auto_now=True)

    def getThumb(self):
        if self.logo:
            return mark_safe('<img src="%s" style="width:100px;"/>' % self.logo.url)
        else:
            return mark_safe(self.name)
    getThumb.short_description = 'Thumbnail'

    def getLogo(self):
        if self.logo:
            return mark_safe('<img src="%s" style="width:100px;"/>' % self.logo.url)
        else:
            return mark_safe(self.name)
    getThumb.short_description = 'Thumbnail'

    class Meta:
        verbose_name = _('Shop')
        verbose_name_plural = _('Shops')

    def __str__(self):
        return u'%s' % (self.name)

    def idString(self):
        return str(self.id)
class ShopOpeningHours(models.Model):
    """
     Store opening times of merchant premises,
     defined on a daily basis (per day) using one or more
     start and end times of opening slots.
     """
    class Meta:
        verbose_name = _('Shop Opening Hours')  # plurale tantum
        verbose_name_plural = _('Shop Opening Hours')
        ordering = ['refShop', 'weekday', 'from_hour']
    refShop = models.ForeignKey(
        Shop, on_delete=models.CASCADE, related_name='shopopening', verbose_name=_('refShop'))
    weekday = models.IntegerField(_('Weekday'), choices=WEEKDAYS)
    from_hour = models.TimeField(_('Opening'))
    to_hour = models.TimeField(_('Closing'))

    def getJour(self):
        if self.weekday == 1:
            return _("Monday")
        elif self.weekday == 2:
            return _("Tuesday")
        elif self.weekday == 3:
            return _("Wednesday")
        elif self.weekday == 4:
            return _("Thursday")
        elif self.weekday == 5:
            return _("Friday")
        elif self.weekday == 6:
            return _("Saturday")
        elif self.weekday == 0:
            return _("Sunday")

    def __str__(self):
        return _("%(premises)s %(weekday)s (%(from_hour)s - %(to_hour)s)") % {
            'premises': self.refShop,
            'weekday': self.weekday,
            'from_hour': self.from_hour,
            'to_hour': self.to_hour
        }


class ShopClosingRules(models.Model):
    """
      Used to overrule the OpeningHours. This will "close" the store due to
      public holiday, annual closing or private party, etc.
    """
    class Meta:
        verbose_name = _('Shop Closing Rule')
        verbose_name_plural = _('Shop Closing Rules')
        ordering = ['start']
    refShop = models.ForeignKey(
        Shop, on_delete=models.CASCADE, related_name='shopclosing', verbose_name=_('refShop'))
    start = models.DateField(_('Start'))
    end = models.DateField(_('End'))
    reason = models.TextField(_('Reason'), null=True, blank=True)


class Category(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=100, verbose_name=_('name'))
    image = models.ImageField(blank=True)
    online = models.BooleanField(default=True, verbose_name=_('online'))
    createdAt = models.DateTimeField(auto_now_add=True)
    updatedAt = models.DateTimeField(auto_now=True)

    def idString(self):
        return str(self.id)

    class Meta:
        verbose_name = _('Categorie')
        verbose_name_plural = _('Categories')

    def __str__(self):
        return str(self.name)
class Product(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=100, verbose_name=_('name'))
    mainImage = models.ImageField(blank=True)
    shortDescription = models.CharField(
        max_length=255, null=True, blank=True, verbose_name=_('shortDescription'))
    description = models.TextField(null=True, blank=True)
    refCategory = models.ForeignKey(
        Category, related_name='category_product', on_delete=models.CASCADE, verbose_name=_('refCategory'))
    priceWithoutVat = models.FloatField(
        default=0, verbose_name=_('price without vat'))
    online = models.BooleanField(default=True, verbose_name=_('online'))
    createdAt = models.DateTimeField(auto_now_add=True)
    updatedAt = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = _('Product')
        verbose_name_plural = _('Products')
        ordering = ['-createdAt']

    def __str__(self):
        return "%s - %s" % (self.refCategory, self.name)

    def image(self):
        return self.mainImage

    def price(self):
        return self.priceWithoutVat

    def getThumb(self):
        if self.mainImage:
            return mark_safe('<img src="%s" style="width:100px;"/>' % self.mainImage.url)
        else:
            return _('No Logo')
    getThumb.short_description = 'Thumbnail'

    def get_product_url(self):
        return reverse("backoffice:product", kwargs={
            'id': self.id
        })

    def idString(self):
        return str(self.id)
class Order(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    orderNumber = models.PositiveIntegerField(
        "Order Number", null=True, default=None, unique=True)
    refUser = models.ForeignKey(User, on_delete=models.PROTECT)
    totalPrice = models.FloatField(default=0, verbose_name=_('totalPrice'))
    status_choix = (
        (0, _("Ordered")),
        (1, _("Paid")),
        (2, _("Payment refused")),
        (3, _("Waiting confirmation")),
        (4, _("Accepted")),
        (5, _("Declined")),
        (6, _("To deliver")),
        (7, _("Delivered")),
        (8, _("Reception confirmed")),
        (9, _("To refund")),
        (10, _("Refunded")),
        (11, _("Canceled")),
        (12, _("Error Refund")),
        (13, _("Unknown technical error")),
        (14, _("Order aborted")),
        (15, _("Not delivered")),
        (16, _("Order modified")),
    )
    orderStatus = models.IntegerField(choices=status_choix, default=0)
    statusComment = models.TextField(null=True, blank=True)
    stripeChargeId = models.CharField(max_length=255, default='')
    createdAt = models.DateTimeField(auto_now_add=True)
    updatedAt = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = _('Order')
        verbose_name_plural = _('Orders')
        ordering = ['-createdAt', ]

    def __str__(self):
        return u'%s (%s - %d)' % (self.orderNumber, self.refUser, self.totalPrice)

    def idString(self):
        return str(self.id)

    def save(self, *args, **kwargs):
        if self.orderNumber is None:
            self.get_or_assign_number()
        super(Order, self).save(*args, **kwargs)

    def get_or_assign_number(self):
        if self.orderNumber is None:
            epoch = timezone.now()
            epoch = epoch.replace(epoch.year, 1, 1)
            qs = Order.objects.filter(
                orderNumber__isnull=False, createdAt__gt=epoch)
            qs = qs.aggregate(models.Max('orderNumber'))
            try:
                epoc_number = int(str(qs['orderNumber__max'])[5:]) + 1
                self.orderNumber = int(
                    '{0}{1:06d}'.format(epoch.year, epoc_number))
            except (KeyError, ValueError):
                # the first order this year
                self.orderNumber = int('{0}000001'.format(epoch.year))
        return self.get_orderNumber()

    def get_orderNumber(self):
        return '{0}-{1}'.format(str(self.orderNumber)[:5], str(self.orderNumber)[5:])

    @classmethod
    def resolve_number(cls, orderNumber):
        orderNumber = orderNumber[:4] + orderNumber[5:]
        return dict(orderNumber=orderNumber)

class OrderProduct(models.Model):
  
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    refOrder = models.ForeignKey(
        Order, on_delete=models.CASCADE, related_name='orderproducts')
    refProduct = models.ForeignKey(Product, on_delete=models.PROTECT)
    quantity = models.IntegerField(default=1, verbose_name=_('quantity'))
    pricePaid = models.FloatField(default=0, verbose_name=_('pricePaid'))
    createdAt = models.DateTimeField(auto_now_add=True)
    updatedAt = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = _('OrderProduct')
        verbose_name_plural = _('OrderProducts')
        ordering = ['refOrder', ]

    def __str__(self):
        return u'%s - %s' % (self.refOrder, self.refProduct)

    def idString(self):
        return str(self.id)

    def save(self, *args, **kwargs):
        super(OrderProduct, self).save(*args, **kwargs)

Nothing really difficult. I just define the entities that i will use in the application.

Notice: In the generated code we will find all methods to deal with user authentification: account creation, login page, forgotten password and payments with Stripe.

To be able to do that, we need to have a User class in our models ! This is a requirement otherwise the code generation will fail. But when uploading the models.py file with the tool, there is an option to let us specify if we want the tool to add this User class or not.

Upload a models.py file and scaffold your Ionic and Django application

So let's go on Ionic and Django kickoff website and choose a plan : Basic, Silver, Gold or Business. Then we will access to a form in which we can provide some details:

Ionic django kickoff form
Name of the applicationYour ionic and Django package will be named this value
Domain urlThe url on which you will deploy the Django project. You can enter http://127.0.0.1:8000 if you run the django server on your machine for development
Include user modelAs i said, the tool needs to have an User class to generate all the code related to authentification. If you haven't already incluced the User in your models.py file, check this option.
Specific requirementsThe Django project includes and uses some packages (that are specified below the form). You can use this box to enter your specific django packages (if any).
FileuploadJust browse to select your models.py file in your disk

Then you can click the upload button and after few minutes the scaffolded packages for Ionic and django, will be available and you will receive an email with a download link.

If an error occurs, an email with the error will be sent to investigate and check your models.py file.

The package (which is a zip) extracted will contain a directory named with the value choosen in the previous form : eShop.
This directory itself will contain two directories : Backend for Django code and Frontend for the Ionic code.


Let's first focus on the Django scaffolded project.

Django generated package

The Backend directory contains our Django project. The most important directories are api which contains the endpoints our Ionic application will use to get data, and a bo directory containing the models.py file and the admin.py file for our backoffice interface (silver and gold packages only).

Django Rest API generated

The API folder contains our generated API using Django Rest framework. A postman_collection.json file has also been generated so it's easy to import in Postman and have a quick look at all auto generated endpoints:

from django.conf.urls import include, url
from .views import *
from rest_framework.decorators import api_view
from rest_framework.routers import DefaultRouter
router = DefaultRouter()

urlpatterns = [
    url(r'^getSetupIntentMethod/$',setupIntentMethod,name='setupIntentMethod'),
    url(r'^createpaymentintent/$',createpaymentintent,name='createpaymentintent'),
    url(r'^createpaymentintentWithPaymentMethod/$',createpaymentintentWithPaymentMethod,name="createpaymentintentWithPaymentMethod"),
    url(r'^createpaymentmethod/$',createpaymentmethod,name='createpaymentmethod'),
    url(r'^createpaymentsubscriptionmethod/$',createpaymentsubscriptionmethod,name='createpaymentsubscriptionmethod'),
    url(r'^stripeSubscriptionStatus/$',stripeSubscriptionStatus,name='stripeSubscriptionStatus'),
    url(r'^endSubscription/$',endSubscription,name='endSubscription'),
    

  url(r'^user/(?P<pk>[0-9A-Fa-f-]+)/$', UserDetailView.as_view()),
  url(r'^user/$', UserListView.as_view()),

  url(r'^news/(?P<pk>[0-9A-Fa-f-]+)/$', NewsDetailView.as_view()),
  url(r'^news/$', NewsListView.as_view()),

  url(r'^shop/(?P<pk>[0-9A-Fa-f-]+)/$', ShopDetailView.as_view()),
  url(r'^shop/$', ShopListView.as_view()),

  url(r'^shopopeninghours/(?P<pk>[0-9A-Fa-f-]+)/$', ShopOpeningHoursDetailView.as_view()),
  url(r'^shopopeninghours/$', ShopOpeningHoursListView.as_view()),

  url(r'^shopclosingrules/(?P<pk>[0-9A-Fa-f-]+)/$', ShopClosingRulesDetailView.as_view()),
  url(r'^shopclosingrules/$', ShopClosingRulesListView.as_view()),

  url(r'^category/(?P<pk>[0-9A-Fa-f-]+)/$', CategoryDetailView.as_view()),
  url(r'^category/$', CategoryListView.as_view()),

  url(r'^product/(?P<pk>[0-9A-Fa-f-]+)/$', ProductDetailView.as_view()),
  url(r'^product/$', ProductListView.as_view()),

  url(r'^order/(?P<pk>[0-9A-Fa-f-]+)/$', OrderDetailView.as_view()),
  url(r'^order/$', OrderListView.as_view()),

  url(r'^orderproduct/(?P<pk>[0-9A-Fa-f-]+)/$', OrderProductDetailView.as_view()),
  url(r'^orderproduct/$', OrderProductListView.as_view()),

  url(r'^stripesubscription/(?P<pk>[0-9A-Fa-f-]+)/$', StripeSubscriptionDetailView.as_view()),
  url(r'^stripesubscription/$', StripeSubscriptionListView.as_view()),

  url(r'^appprofile/(?P<pk>[0-9A-Fa-f-]+)/$', AppProfileDetailView.as_view()),
  url(r'^appprofile/$', AppProfileListView.as_view()),

]

Of course views and serializers have been generated too. The Django project is ready to user and deploy.

Ionic application scaffolded

The Frontend directory contains the source code of the generated Ionic application.

You can install it with

npm install
ionic build
ionic serve

API and entities generated

The src/app/services directory contains a file apiservice.service.ts and entities.ts with all the required code to deals with the entities and the CRUD API methods. Other endpoints have been generated too to manage Stripe subscription, payments...

The Ionic and Django projects are also already configured to have a secure API with JWT authentication. You can learn more about this on this tutorial.

Generated code in Silver package

With Silver package, the Django admin contains an admin.py file with our models ready to use in the Django admin interface.

The ionic application contains two pages: one for registering and one for login with all required methods for social login too: Apple signinGoogle signin and Facebook login.

The project uses ngx-translate for managing internationalization so the application will be i18n ready.

Please notice the Ionic application uses Capacitor and each plugin needs some specific configuration (such as application identifier), so please consult the documentation of each plugin to finalize configuration:

Apple sign inDocumentation
Google sign inDocumentation
Facebook loginDocumentation

Ionic generated code with Gold and Business packages

From a generation code point of view, both packages Gold and Business are similar. The only difference is that you can generate as many project as you want with the Business package payment.

Here is the list of available screens/features

ScreenPurpose
Google MapsComponent which geolocates the user and display a Google Map
Google placesComponent to search Google places with an history of previous searchs
RatingA component which displays an Amazon like display view
SearchA page which implements a search
YouTube PlayerA page which displays a youtube video
List swipeA page which displays a swipeable list
List expandableA page which displays an expandable list
CardsA page which displays cards
GridA page which displays images as grid
ProfileA profile page
SettingAn example of setting page design
PaymentA page for payment (design only)
Save cardA page which uses Stripe (SCA Ready) to let user enters it's credit card detail and save it for later payment (real code, not a mock)
Pay A page example to show how to proceed with payment for a saved card (Stripe SCA ready)
Pay StripeA page example to show you how to pay with Stripe (without a saved card)
Stripe subscriptionA page to show how to subscribe / unsubscribe a User with Stripe

Christophe Surbier