Emma
Diverse decks of slide mainly about Django
By Emmanuelle Delescolle
Please, follow the instructions on http://bit.ly/djember_cc
Slides at http://bit.ly/djember
Code at http://bit.ly/djember_commits
sudo apt-get update
sudo apt-get install -y python-pip python-virtualenv build-essential g++ curl \
libssl-dev apache2-utils software-properties-common unzip wget sudo git
git config --global user.email "you@example.com"
git config --global user.name "Your name"
curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo npm install -g bower ember-cli phantomjs
sudo add-apt-repository ppa:fkrull/deadsnakes
sudo apt-get update
sudo apt-get install -y python3.5 python3.5-dev
sudo pip install cookiecutter
$ cookiecutter https://bitbucket.org/levit_scs/cc_django_ember_app.git
Cloning into 'cc_django_ember_app'...
remote: Counting objects: 407, done.
remote: Compressing objects: 100% (296/296), done.
remote: Total 407 (delta 202), reused 189 (delta 75)
Receiving objects: 100% (407/407), 58.77 KiB | 0 bytes/s, done.
Resolving deltas: 100% (202/202), done.
Checking connectivity... done.
project_name [Project name]: My Webshop
author [Your Name]: Emma
username [emma]:
email [you@domain.com]:
create_superuser [no]: yes
repo_name [my_webshop]:
module_prefix [my-webshop]:
python_executable [python3.5]:
bootstrap_version [3.3.6]:
fontawesome_version [4.6.2]:
{
"disableAnalytics": false,
"port": 3000,
"live-reload-port": 8089
}
front/.ember-cli
$ ./run.sh
In your shell (in back)
In another shell (in front)
$ ./run.sh
In a local environment, in your browser on http://localhost:4200
Used to:
Documentation:
Used to easily create filters with DRF
Used to allow access to Django from another domain
Used to access Django Debug Toolbar from and inspector panel
Used to create test and dummy data
Documentation on Read The Docs
Frontend equivalents of the Django ORM and its database backend
Frontend equivalent of django.contrib.auth
Used for displaying long lists
Rendering HTML takes a lot of resources, Smokes and mirrors allows to only render the elements which are on screen (occlusion)
SPA's need to feel snappy. A few animations will bring you a long way!
Kinda like Factory Boy but for Ember
They are used respectively to mock API calls and simulate data inside Ember's internal data store
Faster cache + offline browsing of already-visited pages
*Service workers are not currently supported by all browsers, that's why we need app cache as a fallbackThose are enabled in production environment only, therefore, in dev, you'll see:
$ ./manage.py startapp catalog
INSTALLED_APPS = (
...
'catalog',
)
In your shell (in back)
back/settings.py
*All commands (in back) should be run in a "venv-actiavted shell.
To activate a venv, from the back directory, run
$ source ../venv/bin/activate
You will know that you are in a venv-activated shell because your prompt will be prefixed with (venv) or [venv] depending on your computer.
For this workshop we will be creating 2 models: Product and Category
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=255)
category = models.ForeignKey(Category, related_name='products')
description = models.TextField()
image = models.URLField()
price = models.DecimalField(max_digits=6, decimal_places=2)
def __str__(self):
return self.name
back/catalog/models.py
$ ./manage.py makemigrations
Migrations for 'catalog':
0001_initial.py:
- Create model Category
- Create model Product
$ ./manage.py migrate
Operations to perform:
Apply all migrations: auth, sessions, catalog, kombu_transport_django,
admin, contenttypes
Running migrations:
Rendering model states... DONE
Applying catalog.0001_initial... OK
in your shell (in back)
from django.contrib import admin
from .models import Category, Product
admin.site.register(Category)
admin.site.register(Product)
back/catalog/admin.py
(Think of them as Django forms) We will be creating one for each model
from rest_framework import serializers
from .models import Category, Product
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = (
'id',
'name',
'products',
)
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = (
'id',
'name',
'category',
'description',
'image',
'price',
)
back/catalog/serializers.py
(Like views but they come in groups) We will also be creating one for each model
from rest_framework import viewsets
from .models import Category, Product
from .serializers import CategorySerializer, ProductSerializer
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CategorySerializer
queryset = Category.objects.all()
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ProductSerializer
queryset = Product.objects.all()
back/catalog/views.py
from django.conf.urls import include, url
from rest_framework import routers
from .views import UserViewSet
from catalog.views import CategoryViewSet, ProductViewSet
router = routers.DefaultRouter()
router.register(r'userinfos', UserViewSet)
router.register(r'categories', CategoryViewSet)
router.register(r'products', ProductViewSet)
urlpatterns = [
url(r'', include(router.urls)),
]
back/my_webshop/api_urls.py
In your browser on http://localhost:8000/api/v1/
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';
export default Model.extend({
name: attr('string'),
products: hasMany('product', {
async: true
})
});
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo } from 'ember-data/relationships';
export default Model.extend({
name: attr('string'),
category: belongsTo('category', {
async: true
}),
description: attr('string'),
image: attr('string'),
price: attr('number')
});
$ ember g model category
$ ember g model product
In your shell (in front)
front/app/models/category.js
front/app/models/product.js
We will be creating one FE Model for each BE API Endpoint
This is where we fetch the data from the backend (think of it as a Django View)
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('product');
}
});
front/app/index/route.js
A bit like Django templates while a bit different
{{#each model as |product|}}
<div class="col-lg-4 col-sm-6 col-xs-12 product" style="padding: 30px;">
<div class="material-card">
<h2>
{{product.name}}
</h2>
<br/>
<div class="product_image" style="width: 100%; text-align: center;">
<img src="{{product.image}}" style="max-width: 100%;">
</div>
<br/>
<span class="product_price" style="color: #cc2525;">
Only ${{product.price}}
</span>
</div>
</div>
{{/each}}
front/app/index/template.hbs
In your browser on http://localhost:4200
Your first list view in Ember
just like in Django
<small style="font-size: 80%; color: #999;">({{product.category.name}})</small>
front/app/index/template.hbs
As straighforward as in the admin
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
...
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ProductSerializer
queryset = Product.objects.all()
filter_backends = (DjangoFilterBackend, )
filter_fields = ('category_id', )
back/catalog/views.py
It's easier than it sounds
It's easier than it sounds
$ ember g route index/index --pod
$ ember g route index/category --pod
$ cp app/index/template.hbs \
app/index/index/template.hbs
$ cp app/index/route.js \
app/index/index/route.js
export default Router.map(function() {
sections.forEach((section) => {
const opts = section.opts || {};
if (section.route !== 'index') {
this.route(section.route, opts);
}
});
this.route('login');
this.route('logout');
this.route('index', {
path: '/',
}, function() {
this.route('category', {
path: '/category/:category_id'
});
});
});
{{outlet}}
front/app/router.js
front/app/index/template.hbs
In your shell (in front)
It's easier than it sounds
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('category');
}
});
<ul class="categories_wrapper col-md-2 col-sm-4 col-xs-12">
<li>{{#link-to 'index'}}All categories{{/link-to}}</li>
{{#each model as |category|}}
<li>{{#link-to 'index.category' category.id}}
{{category.name}}
{{/link-to}}</li>
{{/each}}
</ul>
<div class="products_wrapper col-md-10 col-sm-8 col-xs-12">
{{outlet}}
</div>
front/app/index/route.js
front/app/index/template.hbs
It's easier than it sounds
import Ember from 'ember';
export default Ember.Route.extend({
templateName: 'index/index',
model(params) {
return this.get('store').query('product', {category_id: params.category_id});
}
});
front/app/index/category/route.js
$ rm app/index/category/template.hbs
In your shell (in front)
{{searchable-products model=model}}
$ ember g component searchable-products --pod
$ cp app/index/index/template.hbs \
app/components/searchable-products/template.hbs
In your shell (in front)
front/app/index/index/template.hbs
import Ember from 'ember';
export default Ember.Component.extend({
searchTerm: '',
filteredProducts: Ember.computed(
'searchTerm',
'model.length',
function() {
const searchTerm = this.get('searchTerm')
.toLowerCase();
const products = this.get('model');
return products.filter((item) => {
return item.get('name').toLowerCase()
.indexOf(searchTerm) > -1;
});
}
)
});
<div class="col-xs-12 input-group">
<input class="form-control"
value="{{searchTerm}}"
onChange={{action (mut searchTerm)
value='target.value'}}>
<span class="input-group-addon">
<i class="fa fa-search"></i>
</span>
</div>
{{#each filteredProducts as |product|}}
...
front/app/components/searchable-products/component.js
front/app/components/searchable-products/template.hbs
you now have a fully searchable product catalog
Create a non-nested route product which takes a product_id
.
Here is what the template could look like
Questions welcome
{{#link-to 'index'}}
<i class="fa fa-chevron-left"></i> Back to list
{{/link-to}}<br/>
<div class="image_container col-sm-6 col-xs-12" style="text-align: center">
<img src="{{model.image}}" style="max-width: 100%">
</div>
<div class="product_container col-sm-6 col-xs-12">
<h2>{{model.name}}</h2>
<span class="product_price" style="color: #cc2525;">
Only ${{model.price}}
</span>
<br/>
{{model.description}}
</div>
{{#link-to 'product' product.id}}{{product.name}}{{/link-to}}
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return this.get('store').findRecord('product', params.product_id);
}
});
...
this.route('product', {
path: '/product/:product_id'
});
});
front/app/components/searchable-products/template.hbs
front/app/product/route.js
front/app/router.js
export default function() {
this.transition(
this.toRoute('product'),
this.use('explode', {
matchBy: 'data-product-id',
use: ['fly-to', { duration: 300 }]
}, {
matchBy: 'data-image-id',
use: ['fly-to', { duration: 300 }]
}, {
use: ['fade', { duration: 150 }]
})
);
this.transition(
this.use('fade', { duration: 150 }),
this.debug()
);
}
...
<div class="products_wrapper
col-md-10 col-sm-8 col-xs-12">
{{liquid-outlet}}
</div>
front/app/index/template.hbs
front/app/transitions.js
...
<div class="image_container col-sm-6 col-xs-12"
style="text-align: center">
<img src="{{model.image}}" style="max-width: 100%" data-image-id="{{model.id}}">
</div>
<div class="product_container col-sm-6 col-xs-12">
<h2 data-product-id="{{model.id}}">{{model.name}}</h2>
...
...
<h2 data-product-id="{{product.id}}">
{{#link-to 'product' product.id}}{{product.name}}{{/link-to}}
</h2>
<br/>
<div class="product_image" style="width: 100%; text-align: center;">
<img src="{{product.image}}" style="max-width: 100%;"
data-image-id="{{product.id}}">
</div>
...
front/app/component/searchable-products/template.hbs
front/app/product/template.hbs
<div class="col-xs-12 input-group">
<input class="form-control" value="{{searchTerm}}"
onChange={{action (mut searchTerm)
value='target.value'}}>
<span class="input-group-addon">
<i class="fa fa-search"></i>
</span>
</div>
{{#each filteredProducts as |product|}}
<div class="col-lg-4 col-sm-6 col-xs-12 product">
<h2 data-product-id="{{product.id}}">
{{#link-to 'product' product.id}}
{{product.name}}
{{/link-to}}
</h2>
<br/>
<div class="product_image">
<img src="{{product.image}}"
data-image-id="{{product.id}}">
</div>
<br/>
<span class="product_price">
Only ${{product.price}}
</span>
</div>
{{/each}}
Let's remove all inline styles from front/app/components/searchable-products/template.hbs
& {
.product {
padding: 30px;
h2 a {
color: $text-color;
&:hover {
text-decoration: none;
}
}
}
.product_image {
width: 100%;
text-align: center;
img {
max-width: 100%;
}
}
.product_price {
color: #cc2525;
}
}
And add them to a new front/app/components/searchable-products/styles.scss
export default function() {
this.transition(
this.fromRoute('product'),
this.use('explode', {
matchBy: 'data-product-id',
use: ['fly-to', {duration: 300}]
}, {
matchBy: 'data-image-id',
use: ['fly-to', {duration: 300}]
}, {
use: ['fade', {duration: 150}]
})
);
this.transition(
this.toRoute('product'),
...
front/app/transitions.js
By Emma
DjangoCon Europe 2016 workshop