четверг, 30 марта 2017 г.

Ember js 404 Page Not Found

Если страница не найдена, то Ember js выдает сообщение в консоль:
UnrecognizedURLError
и при этом показывается пустая страница, как будто сайт не работает.
Я погуглил в интернете на эту тему и нашел решение в статье -

Все достаточно просто создаем роут not-found
$ ember g route not-found --path=/*path

В router должна появится строка

Router.map(function() {
  ...
  this.route('not-found', { path: '/*path' })
});


Т.е. все что не подходит под имеющиеся роуты будет отправлено на not-found.
В route not-found надо прописать:

export default Ember.Route.extend({
  redirect: function () {
    var url = this.router.location.formatURL('/not-found');
    if (window.location.pathname !== url) {
      this.transitionTo('/not-found');
    }
  }
});


В template not-found надо прописать:

<h1>404 Страница не найдена</h1>
<p>
  Возможно, ссылка была переделана. Обратитесь к администратору сайта.

</p>


понедельник, 20 февраля 2017 г.

Django Active Directory user backend

В статье Django аутентификация через NGINX kerberos (Active Directory) я описал как использовать прозрачную (sso) аутентификацию.
Django RemoteUserBackend работает так как надо, но мне понадобилось для создаваемого пользователя добавлять из Active Directory дополнительную информацию, а именно ФИО и email. Схема получилась такая:
1) Пользователь первый раз заходит на сайт example.com
2) RemoteUserBackend создает учетную запись пользователя (user.username) в Django
3) ... и дополнительно загружает информацию с использованием класса ADRemoteUserBackend

Ниже привожу код LDAPRemoteUserBackend.py с комментариями, что и куда добавлять.


"""
ADRemoteUserBackend добавляет фамилию, имя и email из Active Directory. Используется
как расширение RemoteUserBackend.
У меня данный LDAPRemoteUserBackend.py находится в папке back, там же где и settings.py
Для аутентификации надо убрать RemoteUserBackend и добавить в settings.py:
 
AUTHENTICATION_BACKENDS = [
    #'django.contrib.auth.backends.RemoteUserBackend',
    'back.LDAPRemoteUserBackend.ADRemoteUserBackend',
    'django.contrib.auth.backends.ModelBackend',
]
 
"""

from django.contrib.auth.backends import RemoteUserBackend
from django.conf import settings as s
from ldap3 import Server, Connection, SIMPLE, SYNC, ASYNC, SUBTREE, ALL


"""
Если созданному пользователю надо добавить права superuser (например, пользователь admin), то
 
python manage.py shell
 
from django.contrib.auth.models import User
user = User.objects.get(username=admin)
user.is_staff = True
user.is_superuser = True
user.save()
"""

# Можно прописать следующие Константы в settings.py, а можно и здесь (чтобы не засорять settings.py)
# DNS имя сервера Active Directory
s.AD_SERVER = "srv.example.com"
# Пользователь (логин) в Active Directory - нужно указать логин в AD в формате 'EXAMPLE\aduser' или 'aduser@example.com'
s.AD_USER = 'EXAMPLE\userad'
s.AD_PASSWORD = 'FADSfjkb,.^%'
s.AD_SEARCH_TREE = 'dc=exampe,dc=com'


class ADRemoteUserBackend(RemoteUserBackend):
    def clean_username(self,username):
        clean_username=username.split('@')[0]
        return clean_username

    def configure_user(self,user):
        #Если нужно пускать пользователей в админку, то раскоментировать следующую строку
        #user.is_staff=True

        #connect to ldap server
        server = Server(s.AD_SERVER)
        conn = Connection(server, user=s.AD_USER, password=s.AD_PASSWORD)
        conn.bind()
        AD_FILTER = '(&(objectCategory=Person)(sAMAccountName='+user.username+'))'
        # get user Common-Name, email, first name, last name, отчество, full Name - attributes ['cn', 'mail', 'givenName', 'sn', 'initials', 'displayName']
        conn.search(s.AD_SEARCH_TREE, AD_FILTER, SUBTREE, attributes =['cn', 'mail', 'givenName', 'sn', 'initials', 'displayName'])

        for entry in conn.entries:
            if entry.mail:
                user.email = entry.mail
            try:
                displayName = entry.displayName.value
            except:
                displayName = entry.cn.value
            if displayName:
                displayName = displayName.split(" ")
            else:
                displayName = ""
            if (displayName[1] and displayName[2]):
                user.first_name = displayName[1] + " " + displayName[2]
            elif (displayName[1]):
                user.first_name = displayName[1]
            if displayName[0]:
                user.last_name = displayName[0]
        #close LDAP Connection
        conn.unbind()
        user.save()

Как используя python получить информацию из Active Directory

Во многих локальных сетях используется Microsoft Active Directory.
Используя python с библиотекой ldap3 можно получить информацию для выгрузки в вебпроекты или просто посмотреть нужную информацию (например, когда под пользователем последний раз подключались).

Библиотека python ldap3 - http://ldap3.readthedocs.io
Установка:
pip install ldap3

Использование:
- Ниже привожу код, пример использования. В коде есть поясняющие комментарии.


from ldap3 import Server, Connection, SIMPLE, SYNC, ASYNC, SUBTREE, ALL

# домен - example.com
# DNS имя сервера Active Directory
AD_SERVER = 'srv.example.com'
# Пользователь (логин) в Active Directory - нужно указать логин в AD 

# в формате 'EXAMPLE\aduser' или 'aduser@example.com'
AD_USER = 'EXAMPLE\aduser'
AD_PASSWORD = 'FDfashv,.@#'
AD_SEARCH_TREE = 'dc=example,dc=com'

server = Server(AD_SERVER)
conn = Connection(server,user=AD_USER,password=AD_PASSWORD)
conn.bind()
# в ответ должно быть - True

# Поиск в Active Directory
# примеры ldap фильтров можно посмотреть здесь -
# https://social.technet.microsoft.com/wiki/contents/articles/8077.active-directory-ldap-ru-ru.aspx
# Я в нижеследующем фильтре:
# - исключаю всеx отключенных пользователей (!(UserAccountControl:1.2.840.113556.1.4.803:=2))
# - добавляю только тех пользователей у которых заполнено имя и фамилия
# - и вывожу атрибуты - attributes
# Все возможные атрибуты Active Directory можно посмотреть здесь -
# https://msdn.microsoft.com/en-us/library/ms675090%28v=vs.85%29.aspx
conn.search(AD_SEARCH_TREE,'(&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(givenName=*)(sn=*))',
    SUBTREE,
    attributes =['cn','proxyAddresses','department','sAMAccountName', 'displayName', 'telephoneNumber', 'ipPhone', 'streetAddress',
    'title','manager','objectGUID','company','lastLogon']
    )
# после этого запроса в ответ должно быть - True

# можно посмотреть на результат
print(conn.entries)
# или вывести только Common-Name - cn
for entry in conn.entries:
    print(entry.cn)

# Найти пользователя с логином admin (sAMAccountName=admin) и показать информацию по нему
conn.search(AD_SEARCH_TREE,'(&(objectCategory=Person)(sAMAccountName=admin))', SUBTREE,
    attributes =['cn','proxyAddresses','department','sAMAccountName', 'displayName', 'telephoneNumber', 'ipPhone', 'streetAddress',
    'title','manager','objectGUID','company','lastLogon']
    )

conn.entries

воскресенье, 19 февраля 2017 г.

Ember file upload to Django

Ни на сайте emberjs.com, ни в еще какой документации я так и не нашел описания - как загрузить файл с использование Ember js. И действительно, средствами Ember js - никак. Поэтому нужно использовать либо js XMLHttpRequest, либо jQuery ajax.

Я разрабатываю - фронтэнд на  Ember js, бакэнд на Django. Мне надо загрузить файлы на бакэнд.
Порядок загрузки файла на фронэнде следующий:

1) Находится файл на странице:
let fileUpload = document.getElementById('file-field').files[0]; 

2) Создается formData и в нее добавляются все необходимые для отправки данные, в том числе и файл:
let formData = new FormData();
formData.append('creator', creator_id);
formData.append('attachment', fileUpload); 

3) Собранная  formData отсылается (POST) на url бакэнда с добавлением дополнительных заголовков.

Сначала пример POST запроса с использованием XMLHttpRequest

//1
let fileUpload = document.getElementById('file-field').files[0];
//2
let formData = new FormData();
formData.append('creator', creator_id);
formData.append('attachment', fileUpload); 
//3
let req = new XMLHttpRequest();
req.open('POST', url);
let jwtoken = this.get('session.data.authenticated.access_token');
req.setRequestHeader('Authorization', 'Bearer ' + jwtoken);
req.send(formData)



А теперь пример отправки файла с использованием jQuery ajax, взятого из рабочего кода:

route.js

  actions: {
    saveFile() {
      let model = this.controller.get('model');
      let creator_id = this.get('session.data.authenticated.user.id');
      let fileUpload = document.getElementById('file-field').files[0];
 
      let host = this.store.adapterFor('application').get('host');
      let url = host + '/api/task/taskfiles/';
 
      let formData = new FormData();
      formData.append('task', model.get('task.id'));
      formData.append('creator', creator_id);
      formData.append('attachment', fileUpload);
 
      let jwtoken = this.get('session.data.authenticated.access_token');
 
      Ember.$.ajax({
        url: url,
        type: 'POST',
        beforeSend: function(request) {
          request.setRequestHeader('Authorization', 'Bearer ' + jwtoken);
        },
        data: formData,
        processData: false,
        contentType: false
      })
        .then((response)=>{
          console.log(response);
          this.store.findRecord('task/taskfile', response.id);
          this.transitionTo('task.taskfiles');
        })
        .catch(response => {
          //console.log(response);
          if (response.responseJSON.attachment) {
            this.controller.set('errorMessage', 'File do not selected');
          }
        });
 
    },

template.hbs

  <h3>Adding a new file</h3>
 
  <p>{{errorMessage}}</p>
  <input type='file' id='file-field' class="form-control" name="file"/>
  <button class="btn btn-success form-control" type="submit" {{action 'saveFile' model}}>{{t "Save"}}</button>

На Ember фронэнтде разобрались как отсылать.
Теперь настройка Django бакэнда, а точнее Django-rest-framework, для приема файла

В serializers.py все стандартно

class TaskFileSerializer(serializers.ModelSerializer):
    class Meta:
        model = TaskFile
        fields = ('id', 'creator', 'task','attachment')


А во views.py надо добавить:

from rest_framework.parsers import MultiPartParser, FormParser
 
TaskFileViewSet(viewsets.ModelViewSet):
    queryset = TaskFile.objects.all()
    serializer_class = TaskFileSerializer
    parser_classes = (MultiPartParser, FormParser)


Вроде и не сложно.

Ember document title

В Ember js для html head title я использую - 
https://github.com/kimroen/ember-cli-document-title

Поддерживает как статические, так и динамические title

Пример:

- В route - tasks добавляю статический title

// front/app/tasks/route.js 
export default Ember.Route.extend({
  title: 'Задачи',
});


- В route - application добавляю динамический title

// front/app/application/route.js
export default Ember.Route.extend({
  title: function(tokens) {
    return tokens.join(' - ') + ' Название сайта';
  },

});

суббота, 18 февраля 2017 г.

Django rest framework SessionAuthentication и JSONWebTokenAuthentication

Столкнулся с такой ситуацией:
- Опубликовал Ember фронтэнд на Django бакэнде. GET запросы проходят, а вот при POST запросе вылетает ошибка:

POST http://localhost:8000/api/tasks/ 403 (Forbidden)
Error: Adapter operation failed

Смотрю в отладчике хрома network responce:

{"detail":"CSRF Failed: CSRF token missing or incorrect."}

Ничего не понимаю. При разработке Ember фронтэнда POST запросы с localhost:4200 на localhost:8000 пролетают без проблем, а тут при POST запросе с localhost:8000 на localhost:8000 вылетает ошибка связанная с CSRF. Смотрю в Django debug, сравниваю POST запросы с localhost:4200 и localhost:8000 - в обоих случаях передается в request header - authorization - access_token, а вот втором случае дополнительно в request попадают Cookie и Session data. Ну и что с того, что дополнительная информация падает? Смотрю настройки settings.py бакэнда для REST_FRAMEWORK - DEFAULT_AUTHENTICATION_CLASSES прописано:

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),
}


и тут до меня доходит, что при POST запросах с localhost:4200 на localhost:8000 используется только  'rest_framework_jwt.authentication.JSONWebTokenAuthentication' , а при POST запросах с localhost:8000 на localhost:8000 сначала попадая на 'rest_framework.authentication.SessionAuthentication' проходит сразу аутентификация, потому что имеются session data, и до аутентификации JSONWebTokenAuthentication дело не доходит. Поэтому я взял и поменял порядок аутентификации - поставил сначала JSONWebTokenAuthentication, а потом SessionAuthentication:

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',    

    ),
}


Теперь все POST запросы проходят! Это тот случай, когда порядок имеет значание!

пятница, 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

django-oscar tinymce 4 filebrowser

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