Показаны сообщения с ярлыком DjEmber. Показать все сообщения
Показаны сообщения с ярлыком DjEmber. Показать все сообщения

пятница, 17 февраля 2017 г.

Ember Django deploy v2

Ну, неугомонный я, хочется как попроще. Я уже сделал один вариант публикации Ember фронэнда на Django бакэнде. Теперь еще один вариант.

Структура разработки Django - Ember сайта такая:

SITENAME (это название сайта)
    |
    |-back (это Django проект, здесь внути и будет находится static)
    |
    |-front (это Ember проект)


1) На Django бакэнде добавить:

в settings.py

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]
STATIC_URL = '/static/'



в urls.py

from .views import index

urlpatterns = [
    ...

    url(r'^$', index, name='index'),
    url(r'^(?P<path>.*)/$', index),
]


во views.py

from django.shortcuts import render

def index(request, path=''):
    response = render(request, 'index.html')
    return response



2) На Ember фронтэнде

Просто запускаем в папке front:

$ ember serve --output-path ../back/static/

Используя ключ --output-path ../back/static/ можно заменить путь вывода скомпиленного фронэнда (по умолчанию папка dist).

После запуска фронтэнда, видим, что в папке back создалась папка static и внутри лежит файл index.html и папка assets (в ней лежат js скрипты и css фронтэнда).
Нам нужен файл index.html, копируем его в back/templates, открываем его и видим строки, которые ссылаются на assets

<link rel="stylesheet" href="/assets/vendor.css">
<link rel="stylesheet" href="/assets/front.css">


<script src="/assets/vendor.js"></script>
<script src="/assets/front.js"></script>


Т.к. django не знает ничего про папку assets, зато знает где находится static (в котором теперь есть assets), то надо просто добавить перед assets - static. Вот что должно получится:

<link rel="stylesheet" href="/static/assets/vendor.css">
<link rel="stylesheet" href="/static/assets/front.css">


<script src="/static/assets/vendor.js"></script>
<script src="/static/assets/front.js"></script>


Теперь и на бакэнде (localhost:8000) можно наблюдать фронтэнд.

Эти же настройки можно использовать не только при разработке, но и при деплое, используя команду:

$ ember build -prod --output-path ../back/static/
Ну и скопировать и отредактировать index.html, как было описано выше.

Можно добавить в файл ember-cli-build.js

  var app = new EmberApp(defaults, {
    fingerprint: {
      prepend: '/static/'
    }
  });


Строка prepend: '/static/' позволит не добавлять префикс static в index.html.
Теперь, после выполнения команды

$ ember build -prod --output-path ../back/static/

в index.html уже будут строки похожие на такие

    <link rel="stylesheet" href="/static/assets/ember-app-cd507f10f1eb6d96c8c44db034f28a4f.css">
    <script src="/static/assets/ember-app-03dac61f48f7ccdae5ae0aa2db976ec8.js"></script>


и, теперь, достаточно будет скопировать index.html в templates без всяких изменений.


На мой взгляд, второй вариант получился проще.

Чтобы на фронтэнде при запуске не вспоминать путь --output-path ../back/static/ я сделал shell скрипт в папке front:

- Создаю файл run.sh
$ vi run.sh

- Добавляю туда следующие строки:

#!/bin/bash
ember serve --output-path ../back/static/


- Делаю файл run.sh исполняемым

$ chmod +x run.sh

Теперь можно запускать фронтэнд  командой
./run.sh

четверг, 16 февраля 2017 г.

Публикуем (deploy) Ember фронтэнд в Django бакэнде

Разработка это, конечно, интересно, но приходит время деплоя (публикации фронтэнда). Перед публикацией на хостинге надо проверить работу на локальном компьютере.
Как и куда поместить данные из фронтэнда в бакэнд я сейчас расскажу.
Если непонятна структура связки Django - Ember, то смотрите статью про Создание среды для разработки веб приложения под Django framework и Ember js

Итак, делаем тестовый деплой:

1. На Django бакэнде

Приложение frontend можно и не создавать, но с ним элегантней смотрится (понятно, что и откуда берется).

- Создаем приложение - frontend:
$ python manage.py startapp frontend

-  В back/back/settings.py в INSTALLED_APPS добавляю приложение 'frontend' и сразу добавляю путь к статике фронэнда:

INSTALLED_APPS = [
    ...

    #local apps
    'frontend.apps.FrontendConfig',
]

 
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "frontend/templates"),
]



- создаем back/frontend/urls.py

# back/frontend/urls.py

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),

    url(r'^(?P<path>.*)/$', index),
]


- в back/back/urls.py добавляем ссылку на  'frontend.urls' 

# back/back/urls.py

from django.conf.urls import url, include
...

 
urlpatterns = [
    url(r'^', include('frontend.urls')),
    ...
]


- в back/frontend/views.py создаем функцию index  которая будем выводить index.html на главную страницу нашего Django бакэнда:

# back/frontend/views.py

from django.shortcuts import render


def index(request, path=''):
    response = render(request, 'index.html')
    return response


- создаем папку templates в back/frontend/

- в back/frontend/templates/ создаем для проверки  index.html
- в back/frontend/templates/index.html добавляем строку

<h1>Главная страница Django бакэнда</h1>

- Запускаем django бакэнд
$ python manage.py runserver

и смотрим http://localhost:8000/ должна быть надпись: Главная страница Django бакэнда


2. На Ember фронтэнде

переходим в папку ember фронэнда - у меня называется front

- добавляю в файл front/ember-cli-build.js префикс 'static/'

//front/ember-cli-build.js
 
var EmberApp = require('ember-cli/lib/broccoli/ember-app');

module.exports = function(defaults) {
  var app = new EmberApp(defaults, {
    fingerprint: {
      prepend: 'static/'
    }
  });
  return app.toTree();
};


Смысл в том, что в Django файлы статики ищутся в 'static' ,а в Ember создается для статики папка assets, я пошел по простому пути и добавил префикс 'static/'

- теперь выполняем команду:

$ ember build -prod

все благополучно "собралось" в папку front/dist

- содержимоем папки front/dist копирую в back/frontend/templates/

и запустив в консоли Django бакэнд -
$ python manage.py runserver
смотрю в браузере http://localhost:8000/.

Таким образом, "собрав" исходники на фронэнде и опубликовав их на бакэнде можно посмотреть, предварительно, как это все работает на одном сервере, т.е. теперь не нужно запускать фронэнд.

Например, можно отключить CORS на бакэнде
CORS_ORIGIN_ALLOW_ALL = False
и посмотреть на результат.


Есть еще и второй вариант Ember Django deploy v2

среда, 15 февраля 2017 г.

Ember - Django session authentication

Имеется:
- Бакэнд Django.
- Фронтэнд Ember.js


Задача: При входе в Ember (фронтэнд) автоматически проходить аутентификацию (ember simple authentication), если в Django (бакэнд) аутентификацию уже прошла, т.е. получить access_token в Ember для имеющейся пользовательской Django сессии.

Зачем это нужно?
1) Если на бакэнде пользователь уже авторизован, то на фронэнде повторно проходить аутентификацию уже не нужно.
2) В Django реализована аутентификаци при помощи REMOTE_USER - https://docs.djangoproject.com/en/dev/howto/auth-remote-user/ (это когда используется kerberos аутентификация) и, в таком случае, зачем заставлять пользователя проходить повторную аутентификацию на фронэнде? Если простыми словами: пользователь входит в Windows -> запускает браузер и в браузере уже не нужно проходить повторную аутентификацию. Ранее, я уже описывал как сделать Django аутентификацию через NGINX kerberos (Active Directory) , поэтому повторяться здесь не буду.


У меня Django бакэнд лежит в папке back, соответсвенно настройки settings.py лежат в back/back/settings.py . Подробно про создание среды разработки Django Ember

На бакэнэде нужно дополнительно установить:

$ pip install djangorestframework
$ pip install djangorestframework-jwt
$ pip install django-cors-headers


и настроить settings.py:

- в INSTALLED_APPS добавляю 'rest_framework' и 'corsheaders'

INSTALLED_APPS = (
   ...
    # external apps
    'rest_framework',
    'corsheaders',
)


- в MIDDLEWARE_CLASSES добавляю 'corsheaders.middleware.CorsMiddleware',

MIDDLEWARE_CLASSES = [
   ...
   'corsheaders.middleware.CorsMiddleware',
   'django.middleware.common.CommonMiddleware',
   ...
]


- добавляю новую конфигурацию для REST_FRAMEWORK

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
# разрешаю подключение к api всем, ограничивать подключения буду во views
'rest_framework.permissions.AllowAny',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
# аутентификацию делаю на уровне сессий (для прямых веб подключений) и с использованием token для фронтэнда
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',

'rest_framework.authentication.SessionAuthentication', 
),
'PAGE_SIZE': 100,
}


Для REST_FRAMEWORK аутентификация на первом месте обязательно должна быть JSONWebTokenAuthentication. Это очень важно, потому что сначала делается попытка пройти аутентификацию по jwt, а если не получается, то идет аутентификация по сессии. И если поставить сначала аутентификацию по сессии, то на фронтэнде аутентификация будет выполнена по SessionAuthentication и при POST запросе будет вылетать ошибка, т.к. аутентификация по сессии использует CSRF.  Вообщем-то, когда будет все отлажено и запущено, можно вообще убрать аутентификацию SessionAuthentication.


- для CORS разрешаю все запросы во время разработки (DEBUG = True, а значит получится и CORS_ORIGIN_ALLOW_ALL = True), а также разрешаю принимать cookie в в заголоках - CORS_ALLOW_CREDENTIALS = True:

CORS_ORIGIN_ALLOW_ALL = DEBUG
 CORS_ALLOW_CREDENTIALS = True

- для djangorestframework-jwt (JSON Web Token) добавляю настройки:

import datetime

JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'back.views.jwt_response_payload_handler',
'JWT_AUTH_HEADER_PREFIX': 'Bearer',
# default JWT_EXPIRATION_DELTA - 5 minutes
# JWT_EXPIRATION_DELTA = datetime.timedelta(seconds=300)
'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=12),
}


Мне не хватает для разработки 5 минут активного токена jwt, которые стоят по умолчанию, поэтому я сделал 12 часов, не надо передергивать постояно. Надо понимать, что быстрое "протухание" токена сделано с целью безопасности, поэтому на рабочих проектах надо поставить оптимальное время.

- на этом настройки settings.py закончены, сохраняю.

Т.к. я сослался в настройках на  'JWT_RESPONSE_PAYLOAD_HANDLER': 'back.views.jwt_response_payload_handler', то надо теперь создать
back/back/views.py
и добавить функцию, которая меняет название ключа 'token' на 'access_token'. (по умолчанию djangorestframework-jwt выдает в json ответе ключ 'token', а ember-simple-auth хочет видеть 'access_token'):


def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'access_token': token,
    }


Чтобы не прыгать от настроек к настройкам, добавлю сразу в back/back/views.py функцию отдачи jwt ключа по POST запросу, который будет создаваться при наличии пользовательской сессии.

Привожу полный back/back/views.py

from django.contrib.auth.models import User
from django.contrib.sessions.models import Session
from rest_framework import serializers
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from rest_framework import status


#получаю пользователя из сессии

def get_user_from_session(session_key):
    try:
        session = Session.objects.get(session_key = session_key)
        uid = session.get_decoded().get('_auth_user_id')
        return User.objects.get(pk = uid)
    except:
        return None


@api_view(['POST'])
def SessionIdJSONWebToken(request):
    if request.method == 'POST':
        session_key = request.session.session_key
        user = get_user_from_session(session_key)
        if user:
            #Создаю jwt токен для отдачи
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            username = user.username
            response_data = jwt_response_payload_handler(token, user, request)
            return Response(response_data)
        else:

            #если нет пользовательской сесии выдаю ошибку 401
            response_data = {"error":"User session not found."}
            return Response(response_data, status=status.HTTP_401_UNAUTHORIZED)

#добавляю дополнительно user.id и user.username в jwt json ответ, 

# чтобы на фронтэнде через сессии можно было посмотреть 
# авторизованного пользователя без дополнительных запросов
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id','username',)


def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'access_token': token,
        'user': UserSerializer(user, context={'request': request}).data,
    }



Добавляю в back/back/urls.py


from django.conf.urls import url, include
from django.contrib import admin

from rest_framework_jwt.views import obtain_jwt_token, verify_jwt_token, refresh_jwt_token
from .views import SessionIdJSONWebToken


 

urlpatterns = [
    #url(r'^', include('front.urls')),
    url(r'^admin/', admin.site.urls),
    url(r'^api/auth/login/', obtain_jwt_token),
    url(r'^api/auth/verify-token/', verify_jwt_token),
    url(r'^api/auth/token-sessionid/', SessionIdJSONWebToken),
]



Проверяем отдачу 'access_token' на бакэнде.
- Если еще не создан супер пользователь, то создаем:
$ cd back
$ python manage.py createsuperuser

затем запускаю Django бакэнд:
$ python manage.py runserver

Захожу в админку - http://localhost:8000/admin/
После успешного входа иду по адресу - http://localhost:8000/api/auth/token-sessionid/ , где вижу
"detail": "Method \"GET\" not allowed."
все правильно, принимаются только POST запросы.

Ввожу в поле Content пустые фигурные скобки {} и нажимаю кнопку "POST". Получаю json ответ на экране

HTTP 200 OK
Allow: POST, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "user": {
        "id": 2,
        "username": "hairetdin"
    },
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhhaXJldGRpbiIsInVzZXJfaWQiOjIsImVtYWlsIjoiIiwiZXhwIjoxNDg3MTQ1MDExfQ.kcl2TKs3pCd8nk8DIme9tzzWW7UUuy6JM-P21yly7Ig"
}

Почему запрос не через curl? Вроде проще, а нет. В консоли нет пользовательской сессии. Можно попробывать и посмотреть

$ curl -X POST http://localhost:8000/api/auth/token-sessionid/
{"error":"User session not found."}


Проверяем json token при аутентификации - http://localhost:8000/api/auth/login/ и верификации - http://localhost:8000/api/auth/verify-token/. Соответсвенно ввожу логин и пароль или в поле Token вставляю 'access_token' из предыдущей страницы.
Или в командной строке:
$ curl -X POST -d "username=admin&password=парольсуперпользователя" http://127.0.0.1:8000/api/auth/login/


С бакэндом закончили, переходим к фронтэнду.

Фронтэнд - Ember js. Детали описывать не буду, если что-то непонятно, то Подробно про создание среды разработки Django Ember

$ cd front

- Устанавливаю EDA (Ember-Django-Adapter)
$ ember install ember-django-adapter

- Устанавливаю для аутентификации, авторизации - ember-simple-auth
$ ember install ember-simple-auth

- Устанавливаю ember-cli-js-cookie, чтобы можно было вызывать cookie 
$ ember install ember-cli-js-cookie

В настройках front/config/environment.js указываю API_HOST бакэнда и путь для ember-simple-auth:

if (environment === 'development') { 
  ENV.APP.API_HOST = 'http://localhost:8000';
  ENV['ember-simple-auth'] = {
    serverAuthEndpoint: ENV.APP.API_HOST + '/api/auth/'
  }
... 
}

- Хоть и ведутся какие-то неправильные разговоры про pods, я его все равно использую и далее планирую использовать, мне нравится когда каждое приложение лежит в отдельной папке. Поэтому, назначаю по умолчанию pods структуру в файле front/.ember-cli

{
  "usePods": true,

}

- Создаю аутентификатор
$ ember g authenticator django

Аутентификатор создался по пути front/app/authenticators/django.js
Вношу в front/app/authenticators/django.js следующий код:

//начало front/app/authenticators/django.js
import Ember from 'ember';
import Base from 'ember-simple-auth/authenticators/base';
import ENV from '../config/environment';
import Cookies from 'ember-cli-js-cookie';


function isSecureUrl(url) {
  var link  = document.createElement('a');
  link.href = url;
  link.href = link.href;
  return link.protocol === 'https:';
}

export default Base.extend({

  init() {
    //var apiHost = ENV.APP.API_HOST;
    var apiAuthentication = ENV['ember-simple-auth'] || {};
    this.serverAuthEndpoint = apiAuthentication.serverAuthEndpoint;

  },

  authenticate(identification,password) {

    return new Ember.RSVP.Promise((resolve, reject) => {
      let remoteResponse;
      if (!(identification && password)) {
        remoteResponse = this.requestBackendSession();
      } else {
        const data = { username: identification, password: password };
        let host = ENV.APP.API_HOST;
        let login_url = host + '/api/auth/login/';
        //let login_url = this.serverAuthEndpoint + 'login/';
        remoteResponse = this.makeRequest(login_url, data);
      }
      remoteResponse
      .then((response) => {
        Ember.run(() => {
          console.log('django authenticate response:',response);
          resolve(response);
        });
      }, (xhr /*, status, error */) => {
        Ember.run(() => {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },

  restore(data) {
    return new Ember.RSVP.Promise((resolve, reject) => {
      console.log('authenticator django restore data', data);
      let host = ENV.APP.API_HOST;
      let verifyTokenUrl = host + '/api/auth/verify-token/';
      let access_token = {token:data.access_token};
      this.makeRequest(verifyTokenUrl, access_token).then((response) => {
        Ember.run(() => {
          resolve(response);
        });
      }, (xhr /*, status, error */) => {
        Ember.run(() => {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },


  invalidate(/* data */) {
    function success(resolve) {
      resolve();
    }
    return new Ember.RSVP.Promise((resolve /*, reject */) => {
      //let logout_url = this.serverAuthEndpoint + 'logout/';
      let host = ENV.APP.API_HOST;
      let logout_url = host + '/api/auth/logout/';
      this.makeRequest(logout_url, {}).then((/* response */) => {
        Ember.run(() => {
          success(resolve);
        });
      }, (/* xhr, status, error */) => {
        Ember.run(() => {
          success(resolve);
        });
      });
    });
  },

  makeRequest(url, data) {
    if (!isSecureUrl(url)) {
      Ember.Logger.warn('Credentials are transmitted via an insecure connection - use HTTPS to keep them secure.');
    }

    return new Ember.RSVP.Promise((resolve, reject) => {
      Ember.$.ajax({
        url: url,
        type: 'POST',
        beforeSend: function(request) {
          request.setRequestHeader("X-CSRFToken", Cookies.get('csrftoken'));
        },
        data: data,
      })
      .then((response) => {
        if (response.status === 400) {
          response.json().then((json) => {
            reject(json);
          });

        } else if (response.status > 400) {
          reject(response);
        } else {
          resolve(response);
        }
      }).catch((err) => {
        reject(err);
      });
    });
  },

  requestBackendSession(){

    let host = ENV.APP.API_HOST;
    let url = host + '/api/auth/token-sessionid/';
    let csrftoken = Cookies.get('csrftoken');
    return new Ember.RSVP.Promise((resolve, reject) => {
      Ember.$.ajax({
        url: url,
        type: 'POST',
        beforeSend: function(xhr/*, settings*/) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        },
        xhrFields: {
            withCredentials: true
        },
      })
      .then((response) => {
        if (response.status === 400) {
          response.json().then((json) => {
            reject(json);
          });

        } else if (response.status > 400) {
          reject(response);
        } else {
          resolve(response);
        }
      }).catch((err) => {
        reject(err);
      });
    });
  }
});

//конец front/app/authenticators/django.js


- Генерирую ресурс и контроллер  application:

$ ember g resource application
$ ember g controller application

- Добавляю в темплейт front/app/application/template.hbs следующий код:

{{!-- front/app/application/template.hbs --}}
<div class="menu">
  {{#if session.isAuthenticated}}
    {{session.data.authenticated.user.username}}
    <a href {{action 'invalidateSession'}}>Выход</a>
  {{else}}
    {{#link-to 'login'}}Вход{{/link-to}}
  {{/if}}
</div>
<div class="main">
  {{outlet}}
</div>


- Добавляю в контроллер front/app/application/controller.js следующий код:

// front/app/application/controller.js
import Ember from 'ember';

export default Ember.Controller.extend({
  session: Ember.inject.service('session'),





  sessionAuthenticated: Ember.observer('session.isAuthenticated', function () {
    //reload application if session isAuthenticated
    window.location.href = '/';
  }),

  actions: {
    invalidateSession() {
      this.get('session').invalidate();
    }
  }
});



- Добавляю в роут front/app/application/route.js следующий код:

// front/app/application/route.js
import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';

export default Ember.Route.extend(ApplicationRouteMixin,{
  session: Ember.inject.service('session'),
  beforeModel: function(){
    if (!(this.get('session.isAuthenticated'))){
      //try to authenticate with backend's session, if session is not authenticated
      this.get('session').authenticate('authenticator:django');
    }
  },

});


Вообщем-то на этом этапе уже можно наблюдать результат:
- Запускаем бакэнд - python manage.py runserver
- Запускаем фронтэнд - ember s
На бакэнде проходим аутентификацию на странице http://localhost:8000/admin/login/, а на странице фронтэнда - http://localhost:4200/ смотрим результат в консоли (кнопка F12):

django authenticate response: Object {user: Object, access_token: ....

а еще выводится ошибка

Error: There is no route named login

это потому, что в темплейте front/app/application/template.hbs имеется ссылка {{#link-to 'login'}}Вход{{/link-to}} на роут login, а такого роута пока нет. Исправим эту ошибку.
- Создаю роут и контроллер для login
$ ember g route login
$ ember g controller login

- Редактирую login темлейт front/app/login/template.hbs:

{{!-- front/app/login/template.hbs --}}
<form {{action 'authenticate' on='submit'}}>
  <label for="identification">Пользователь</label>
  {{input id='identification' placeholder='Enter Login' value=identification}}
  <label for="password">Пароль</label>
  {{input id='password' placeholder='Enter Password' type='password' value=password}}
  <button type="submit">Вход</button>
  {{#if errorMessage}}
    <p>{{errorMessage}}</p>
  {{/if}}
</form>


- Редактирую login контроллер front/app/login/controller.js:

// front/app/login/controller.js
import Ember from 'ember';

export default Ember.Controller.extend({
  session: Ember.inject.service('session'),

  actions: {
    authenticate() {
      let { identification, password } = this.getProperties('identification', 'password');

      this.get('session').authenticate('authenticator:django', identification, password).
      then(() => {
       console.log('Успешна аутентификация с токеном: ' + this.get('session.data.authenticated.access_token'));
      }, (err) => {
       alert('Ошибка токена: ' + err.responseText);
      }).
      catch((reason) => {
      this.set('errorMessage', reason.error || reason);
      });
    }
  }
});


Теперь в http://localhost:4200/ ошибок нет и возможна аутентификация через фронэнд, если на бакэнде аутентификации не было.


А вот авторизатор я изобретать не буду, использую стандартный.
Создаю авторизатор:
$ ember g authorizer django

и добавлю в него (front/app/authorizers/django.js) следующие строки

// front/app/authorizers/django.js
import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';

export default OAuth2Bearer.extend();


Теперь нужен еще адаптер, который будет использовать созданный django авторизатор

$ ember g adapter application

и добавляю в него (front/app/application/adapter.js) следующий код:

// front/app/application/adapter.js
import DRFAdapter from '../adapters/drf';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

export default DRFAdapter.extend(DataAdapterMixin , {
  authorizer: 'authorizer:django'
});





среда, 26 октября 2016 г.

Создание среды для разработки веб приложения под Django framework и Ember js

В этом howto будет расказано, каким образом создать среду для разработки веб приложения под Django framework и Ember js (DjEmber).
Права суперпользователя linux нужны только для того, чтобы установить Development Tools и node js, остальные процессы установки делаются под простым пользователем (в этом примере пользователь example).

I. Проект DjEmber - example.com
II. Проект Django - back
III. Проект Ember js - front

I. Проект DjEmber - example.com


- Пользователь - example
- Пользовательская директория /home/example/
- Проект DjEmber - example.com - будет размещаться в /home/example/Projects/example.com/
- Проект Django - back (от слова backend) - будет размещаться в /home/example/Projects/example.com/back/
- Проект Ember - front (от слова frontend) - будет размещаться в /home/example/Projects/example.com/front/
- В Django проекте - back - будет использоваться виртуальное окружение с Python версии 3
- Python 3, установленный из исходников, будет в /home/example/python3/
- Виртуальное окружение для Django проекта - back - /home/example/virtualenv/example.com
- Путь для загруженных программ - /home/example/Downloads/

1. Установка дополнительных программ

$ su -c 'yum groupinstall "Development Tools"'
$ sudo yum install wget

2. Создаю корневой каталог для проекта example.com

$ mkdir -p ~/Projects/example.com

Готовые файлы настроек можно посмотреть и скачать с https://github.com/hairetdin/djember

II. Проект Django - back


1. Установка Python 3 в домашнюю директорию пользователя /home/example/python3

- скачиваю исходники стабильной версии python 3, выбираю в https://www.python.org/downloads/source/

$ cd ~/Downloads/
$ wget https://www.python.org/ftp/python/3.5.2/Python-3.5.2.tar.xz

- распаковываю

$ tar xvf Python-3.5.2.tar.xz

- собираю конфигурацию, указываю путь куда будет установен python3 - /home/example/python3/ и устанавливаю

$ cd Python-3.5.2/
$ $ ./configure --prefix=$HOME/python3
$ make
$ make install

- теперь python3 установлен в /home/example/python3/, проверяю

$ ~/python3/bin/python3 -V
Python 3.5.2

2. Создание виртуального окружения для проекта example.com - back (виртуальные окружения у меня находяться в ~/virtualenv)

$ cd ~
$ mkdir -p virtualenv
$ ~/python3/bin/pyvenv ~/virtualenv/example.com

- проверяю работу созданного виртуального окружения - virtualenv/example.com

$ source ~/virtualenv/example.com/bin/activate

в терминале должна появиться строка начинающаяся с - (example.com). Теперь запущу python и посмотрю версию

$ python -V
Python 3.5.2

- деактивация виртуального окружения:

$ deactivate


3. Установка python приложений в виртуальное окружение virtualenv/example.com

- Активирую виртуальное окружение и устанавливаю python приложения

$ source ~/virtualenv/example.com/bin/activate
$ pip install django
$ pip install djangorestframework
$ pip install djangorestframework-jwt
$ pip install django-filter
$ pip install django-cors-headers

Если не будете использовать django-filter, то ставить необязательно.


4. Создаю Django бакэнд проект для example.com

Виртуальное окружение virtualenv/example.com дожно быть активировано.

- перехожу в корень проекта example.com

$ cd ~/Projects/example.com/

- создаю проект back

$ django-admin startproject back

- проверяю созданные Django проект

$ cd ~/Projects/example.com/back/
$ python manage.py runserver

Смотрю в браузере http://127.0.0.1:8000/ - работает, показывает страницу приветствия - "Congratulations on your first Django-powered page."

Хорошо, останавливаю запущенный back проект - Ctrl+C

5. Делаю настройки для ранее установленных python приложений в ~/Projects/example.com/back/back/settings.py

Я для этого я использую редактор Atom, открываю на редактирование settings.py и вношу следующие изменения:

- в INSTALLED_APPS добавляю 'rest_framework' и 'corsheaders'

INSTALLED_APPS = (
   ...
   # external apps
   'rest_framework',
   'corsheaders',
)

- в MIDDLEWARE_CLASSES добавляю 'corsheaders.middleware.CorsMiddleware',

MIDDLEWARE_CLASSES = [
   ...
   'corsheaders.middleware.CorsMiddleware',
   'django.middleware.common.CommonMiddleware',
   ...
]

- добавляю новую конфигурацию для REST_FRAMEWORK

REST_FRAMEWORK = {
   'DEFAULT_PERMISSION_CLASSES': [
# разрешаю подключение к api всем, ограничивать подключения буду во view
'rest_framework.permissions.AllowAny',
   ],
   'DEFAULT_AUTHENTICATION_CLASSES': (
# аутентификацию делаю на уровне сессий (для прямых веб подключений) и с использованием token для фронтэнда
'rest_framework.authentication.SessionAuthentication',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
   ),
   'PAGE_SIZE': 1000,
}

- для CORS разрешаю все запросы во время разработки (DEBUG = True, а значит получится и CORS_ORIGIN_ALLOW_ALL = True):

CORS_ORIGIN_ALLOW_ALL = DEBUG

- для djangorestframework-jwt (JSON Web Token) добавляю настройки:

JWT_AUTH = {
   'JWT_RESPONSE_PAYLOAD_HANDLER': 'back.views.jwt_response_payload_handler',
   'JWT_AUTH_HEADER_PREFIX': 'Bearer',
}

- настройки settings.py закончены, сохраняю.

6. Создаю ~/Projects/example.com/back/back/views.py, открываю на редактирование и добавляю функцию:

def jwt_response_payload_handler(token, user=None, request=None):
   return {
'access_token': token,
   }

Для чего нужна эта функция? Все дело в том, что djangorestframework-jwt, по умолчанию, при запросе ключа возвращает 'token': длинныйдлинныйключ. А ember-simple-auth хочет получить 'access_token': длинныйдлинныйключ , вот поэтому и необходимо сделать эту функцию.

7. В ~/Projects/example.com/back/back/urls.py добавляю api для аутентификации по ключу:

from rest_framework_jwt.views import obtain_jwt_token
#...

urlpatterns = patterns(
   '',
   # ...

   url(r'^api/auth/login/', obtain_jwt_token),
)

8. Делаю миграцию, создаю суперпользователя - admin, запускаю проект back

$ source ~/virtualenv/example.com/bin/activate
$ cd ~/Projects/example.com/back/
$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py runserver

- Теперь через браузер проверяю аутентификацию по адресу http://127.0.0.1:8000/api/auth/login/ , после ввода логина и пароля и нажатии на кнопку "POST", должен появиться ключ - "access_token":"длинныйдлинныйключ".
- Не останавливая проект, запускаю новое терминальное окно и проверяю аутентификацию через утилиту командной строки - curl:

$ curl -X POST -d "username=admin&password=парольсуперпользователя" http://127.0.0.1:8000/api/auth/login/

В ответ должен вернуться - {"access_token":"длинныйдлинныйключ"} . Если все так, значит все настроено правильно.

- На этом настройка бакэнда закончена. Приступаю к запуску и настройке фронтэнда.


III. Проект Ember js - front


1. Создаю ember проект - example.com/front

$ cd ~/Projects/example.com
$ sudo npm install -g ember-cli
$ ember new front

2. Запускаю вновь созданный ember проект - front (команда "ember server" или, сокращенно, "ember s")

$ cd ~/Projects/example.com/front
$ ember s

- При запуске вылетает ошибка "Could not start watchman; falling back to NodeWatcher for file system events.", безопасно игнорируем ее как советует https://ember-cli.com/user-guide/#watchman. Хоть я и установил watchman по инструкции - https://facebook.github.io/watchman/docs/install.html, но при запуске команды "ember -v" под пользователем example получаю ошибку, а при запуске от root вышеприведенной ошибки нет. Если знаете как исправить - подскажите.

- смотрю в браузере http://localhost:4200/ - работает

3. Устанавливаю ember-django-adapter адаптер. front (ember) должен понимать как стучаться в back (django)

$ cd ~/Projects/example.com/front
$ ember install ember-django-adapter

- Добавляю в настройки фронтэнда строку - ENV.APP.API_HOST = 'http://localhost:8000';
Редактирую файл настроек Ember проекта: ~/Projects/example.com/front/config/environment.js

if (environment === 'development') {
 ENV.APP.API_HOST = 'http://localhost:8000';
 ...
}

 Т.е. при разработке, когда django проект back будет запущен командой "python manage.py runserver", ember проект front будет обращаться к http://localhost:8000/. Наименование api можно изменить допустим на my-api, подробности в документации http://dustinfarris.com/ember-django-adapter/ .

4. Устанавливаю аутентификацию для Ember фронэнда

$ cd ~/Projects/example.com/front
$ ember install ember-simple-auth


5. Создаю фронтэнд приложение - application

- Т.к. в Ember js, по умолчанию, роуты начинаются с приложения application, то и аутентификацию прилеплю на странице application (application хоть и не виден в главном router.js, но при запуске ember проекта сначала обрабатываются именно application). Простыми словами, application - это базовые настройки для Ember js проекта.
Я буду использовать привычный для Django порядок - каждое приложение складывается в отдельную папку, для этого используется в команде "ember generate" с дополнительным ключом "--pod". Команду "ember generate" можно сократить до "ember g".

- Создаю template, controller, adapter для application.

$ cd ~/Projects/example.com/front/
$ ember g route application --pod
$ ember g controller application --pod
$ ember g adapter application --pod

После выполнения этих команд во front/app создалась папка приложения - application и внутри нее файлы route.js, controller.js, adapter.js и template.hbs. Таким образом используя ключ --pod создалась отдельная папка apllication в которой разместились роут, контроллер, адаптер и темплейт. Для меня такая структура более приемлема, потому что в дальнейшем можно быстро копировать приложения в другие проекты.


6. Редактирую темплейт app/application/template.hbs, добавляю:

{{!-- app/application/template.hbs --}}
<div class="menu">
 {{#if session.isAuthenticated}}
   <a href {{action 'invalidateSession'}}>Выход</a>
 {{else}}
   {{#link-to 'login'}}Вход{{/link-to}}
 {{/if}}
</div>
<div class="main">
 {{outlet}}
</div>

Таким образом, для "Выход" навешиваю действие - action 'invalidateSession',а для "Вход" ссылку переход на страницу login - #link-to 'login'. Строка {{outlet}} показывает, что весь вышеприведенный код созданного темплейта application будет наследоваться нижестоящими темплейтами, т.е всеми остальными темплейтами, которые будут созданы в проекте.

7. Редактирую контроллер app/application/controller.js, внедряю в проект сессии и создаю action(действие) - invalidateSession():

// app/application/controller.js
import Ember from 'ember';

export default Ember.Controller.extend({
 session: Ember.inject.service('session'),

 actions: {
   invalidateSession() {
     this.get('session').invalidate();
   }
 }
});

8. Добавляю аутентификацию в applicaton route

- Сервис сессий имеет события authenticationSucceeded и invalidationSucceeded, которые показывают прошла аутентификация или нет. Чтобы эти события были доступны в сесии на всех страницах сайта добавляю в app/application/route.js:

// app/application/route.js
import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';

export default Ember.Route.extend(ApplicationRouteMixin);

9. Добавляю authorizer(авторизатор) в adapter

- после того как аутентификация будет успешно выполнена, для повторного доступа к веб api бакэнда нужен авторизатор. Т.к. адаптер я использую уже готовый - ember-django-adapter, то в application я его расширю указав на авторизатор 'authorizer:oauth2', который создам далее. А пока редактирую app/application/adapter.js :

// app/application/adapter.js
import DRFAdapter from '../adapters/drf';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

export default DRFAdapter.extend(DataAdapterMixin , {
 authorizer: 'authorizer:oauth2'
});

10. Создаю аутентификатор и авторизатор oauth2

$ cd ~/Projects/example.com/front/
$ ember g authenticator oauth2
$ ember g authorizer oauth2

- редактирую app/authenticators/oauth2.js (использую стандартную аутентификацию OAuth2PasswordGrant из ember-simple-auth)

// app/authenticators/oauth2.js
import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant';

export default OAuth2PasswordGrant.extend({

 serverTokenEndpoint: 'http://127.0.0.1:8000/api/auth/login/',

});


- редактирую app/authorizers/oauth2.js (использую стандартный авторизатор - OAuth2Bearer из ember-simple-auth)

// app/authorizers/oauth2.js
import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';

export default OAuth2Bearer.extend();


11. Приложение login

В app/application/template.hbs я указал использовать для "Вход" переход на login страницу.

- Для приложение login создаю route, controller (template создается автоматом при создании route):

$ cd ~/Projects/example.com/front/
$ ember g route login --pod
$ ember g controller login --pod

При создании роута для login в основном роуте проекта - app/router.js - добавились строки для маршрутизации login приложения:

Router.map(function() {
 this.route('login');
});

теперь проект front знает о приложении login.

- Создаю логин форму, редактирую app/login/template.hbs:

{{!-- app/login/template.hbs --}}
<form {{action 'authenticate' on='submit'}}>
 <label for="identification">Login</label>
 {{input id='identification' placeholder='Enter Login' value=identification}}
 <label for="password">Password</label>
 {{input id='password' placeholder='Enter Password' type='password' value=password}}
 <button type="submit">Вход</button>
 {{#if errorMessage}}
   <p>{{errorMessage}}</p>
 {{/if}}
</form>

У этой формы имеется действие (action) - {{action 'authenticate' on='submit'}} - при нажатии на кнопку "Вход".

- Описываю в login контроллере действие authenticate() при нажатии на кнопку "Вход" (так как при аутентификации используются сессия, то надо и ее объявить через переменную session), редактирую app/login/controller.js:

// app/login/controller.js
import Ember from 'ember';

export default Ember.Controller.extend({
 session: Ember.inject.service('session'),

 actions: {
   authenticate() {
     let { identification, password } = this.getProperties('identification', 'password');
     this.get('session').authenticate('authenticator:oauth2', identification, password).
then(() => {
 console.log('Success! authenticated with token: ' + this.get('session.data.authenticated.access_token'));
}, (err) => {
 alert('Error obtaining token: ' + err.responseText);
}).
catch((reason) => {
this.set('errorMessage', reason.error || reason);
     });
   }
 }
});


При выполнении authenticate() получаем логин и пароль из формы, затем для session выполняем метод authenticate, для которого в качестве аргументов передаем ранее созданный authenticator:oauth2, а также логин и пароль.


- На этом настройка ember js фронтэнда - example.com/front завершена.


Проверяю работу среды разработки DjEmber для example.com

- запускаю бакэнд в 1 терминальном окне

$ source ~/virtualenv/example.com/bin/activate
$ cd ~/Projects/example.com/back/
$ python manage.py runserver


- запускаю фронтэнд во 2 терминальном окне

$ cd ~/Projects/example.com/front/
$ ember s

- запускаю Google Chrome, открываю http://localhost:4200/, запускаю консоль - F12 -> Console.
- нажимаю кнопку вход, ввожу login и password (использую пользователя суперюзер созданного в Django back), нажимаю вход и смотрю в консоль, появилось сообщение:

Success! authenticated with token: длинныйдлинныйключ

DjEmber окружение для разработки готово. Теперь можно создавать api на бакэнде под django-rest-framework и подключаться к нему из ember фронтэнда.
Удачных вам проектов!!!

django-oscar tinymce 4 filebrowser

Задача: в дашборде django-oscar загружать изображения 1. Установка django-filebrowser-no-grappelli - Открываем проект, загружаем виртуа...