Debian, Dojo, Django, Python

Делюсь опытом в описанных технологиях. Блог в первую очередь выполняет роль памяток для меня самого.

Схема БД или таблицы в PostgreSQL

Всё время забываю, как в PostgreSQL сделать дамп схемы БД, без данных. Команда на удивление проста:

pg_dump -s -d %DATABASE_NAME% -U %USER_NAME% > %FILE_NAME%
Параметр Назначение
-s Ключ указывает на то, что нужно сделать копию только схемы БД
-d Ключ, задающий имя БД, с которой нужно работать.
-U Ключ, указывающий пользователя, от имени которого будет делаться копия схемы. Пользователь должен иметь доступ хотя бы на чтение данной БД.
%FILE_NAME% Файл, в который следует сохранить вывод. В противном случае вся схема будет выведена на экран.

А если вместо -s указать -t и потом имя таблицы, то будет снята схема только с неё, например:

pg_dump -d project -U xphoenix -t auth_users > auth_users_shema.sql

Проблемы с кириллицей в имени пользователя Windows

При использовании Atom столкнулся с тем, что некоторые плагины отказывались работать, выдавая странные сообщения в консоль. Сначала я решал проблему тем, что правил переменную PATH на уровне системы и на уровне пользователя, однако, это особого эффекта не имело, т.к. стоило только поставить более новую версию io.js, как PATH тут же сбрасывалась к примерно такому виду:

    C:\Python34\;C:\Python34\Scripts;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files (x86)\Git\cmd;C:\Program Files (x86)\iojs\;C:\Users\%30o$321\AppData\Roaming\npm

Из этого видно, что портится путь к текущей системной папке пользователя. Изменение свойств папок "Видео", "Изображения", "Документы" и т.д. тут не может ничем помочь. Я использую учётную запись Microsoft со всеми её удобствами вроде OneDrive, WindowsPhone и т.д. и отказываться от неё в пользу локальной учётной записи не хотел (не за то деньги плачены). Поиск привёл меня к решению.

На первом этапе нам нужно создать дополнительную учётную запись типа "Администратор". Способ создания значения не имеет, я рекомендую через "Панель управления" (пользователи Windows 8.1 непрофессиональной редакции выбора не имеют). Теперь нужно загрузиться в безопасном режиме и войти в систему под новосозданной учётной записью. После этого нужно запустить редактор реестра - Regedit. Следует открыть раздел реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList. В нём содержится несколько подразделов. Если щёлкнуть на одном из них, то появится список ключей, одним из которых будет ProfileImagePath. Заменяем имеющееся значение на нужное, например, вот так:

C:\Users\Максим -> C:\Users\xPhoenix

Так же следует обязательно переименовать имеющуюся папку профиля. После этого следует перезагрузить компьютер и при необходимости удалить созданную ранее дополнительную учётную запись администратора.

Проблема с кириллицей в имени пользователя наблюдается и в некоторых других программах. Насколько помню, из-за неё среда MATLAB 7.5 вообще отказывалась сохранять файлы.

Собственная модель пользователя Django (>=1.8)

Введение

Статья была обновлена 5 июня 2015 года и содержит исправление некоторых ошибок и дополнительную информацию.
На GitHub был опубликован репозиторий с исходными кодами, которые содержат ряд исправлений и дополнений. Нашли ошибку? Создайте pull-request или issue, я обязательно посмотрю.

На данную тему в Интернете уже написано огромное множество статей, и моя станет лишь очередной попыткой описать то, что уже и так широко известно. Не претендуя на оригинальность, я попробую описать тот способ, которым пользуюсь сам. В статье пойдёт речь о расширении стандартной модели User, имеющейся в Django.

В настоящее время так же широко используются ещё два способа:

  • Создание связанной с пользователем 1 к 1 модели профиля.
  • Полная замена стандартной модели пользователя на свою с последующим переписыванием бэкэндов авторизации.

Первый способ лично мне не нравится своей устарелостью. Ещё в Django 1.5 была проделана огромная работа по рефакторингу существующих модулей, связанных с пользователями, что дало возможность расширять имеющуюся модель. К тому же, способ с дополнительной моделью заставляет вас писать дополнительный код, например, на отлов события создания или удаления модели пользователя.

Второй способ плох опять же массой дополнительной работы. Нужно переопределить методы-обработчики событий авторизации и многих других действий.

Создание расширенной модели

Создавать новую модель пользователя мы будем на основе уже имеющейся в Django, т.е. будем использовать наследование. Первым делом следует создать новое приложение Django:

djangoadmin.py startapp extuser

Мне нравится использовать имя extuser для решения поставленной задачи потому, что, оно полностью передаёт суть и назначение данного приложения - расширение стандартной модели пользователя.

Модифицируем файл models.py нового приложения.

extuser/models.py

from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.models import BaseUserManager
from django.db import models


class UserManager(BaseUserManager):

    def create_user(self, email, password=None):
        if not email:
            raise ValueError('Email непременно должен быть указан')

        user = self.model(
            email=UserManager.normalize_email(email),
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password):
        user = self.create_user(email, password)
        user.is_admin = True
        user.save(using=self._db)
        return user


class ExtUser(AbstractBaseUser, PermissionsMixin):

    email = models.EmailField(
        'Электронная почта',
        max_length=255,
        unique=True,
        db_index=True
    )
    avatar = models.ImageField(
        'Аватар',
        blank=True,
        null=True,
        upload_to="user/avatar"
    )
    firstname = models.CharField(
        'Фамилия',
        max_length=40,
        null=True,
        blank=True
    )
    lastname = models.CharField(
        'Имя',
        max_length=40,
        null=True,
        blank=True
    )
    middlename = models.CharField(
        'Отчество',
        max_length=40,
        null=True,
        blank=True
    )
    date_of_birth = models.DateField(
        'Дата рождения',
        null=True,
        blank=True
    )
    register_date = models.DateField(
        'Дата регистрации',
        auto_now_add=True
    )
    is_active = models.BooleanField(
        'Активен',
        default=True
    )
    is_admin = models.BooleanField(
        'Суперпользователь',
        default=False
    )

    # Этот метод обязательно должен быть определён
    def get_full_name(self):
        return self.email

    # Требуется для админки
    @property
    def is_staff(self):
        return self.is_admin

    def get_short_name(self):
        return self.email

    def __str__(self):
        return self.email

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = UserManager()

    class Meta:
        verbose_name = 'Пользователь'
        verbose_name_plural = 'Пользователи'

Рассмотрим этот код.

Менеджер моделей

Сначала идёт описание менеджера для данной модели. Описывать его нужно для того, чтобы правильно работали методы создания нового пользователя. Чуть ниже в коде будет указано, что для работы с объектами типа ExtUser нужно использовать именно его. Можно определить несколько менеджеров при необходимости, каждый из которых будет отвечать за свою часть работы.

Модель ExtUser

В этой модели как ключевое указано поле email. Я считаю, что это один из лучших способов для авторизации пользователей. Лучше может быть только двухфакторная авторизация через SMS. Создав ключевое поле, следуте обязательно указать, что оно используется в качестве имени пользователя:

USERNAME_FIELD = 'email'

Делали бы мы авторизацию через номер телефона - указали бы так:

phone = CharField(
    'Номер телефона'
    max_length=20,
    unique=True,
    db_index=True
)

#Чуть ниже в этом же классе:
USERNAME_FIELD = 'phone'

Надеюсь, общий принцип понятен. Дальше уже идёт отсебятина, которую можно не писать. Например, аватары, отчество и т.д. Список полей в каждом проекте будет разным. Не забудьте только описать методы проверки прав has_perm() и has_module_perms() соответственно.

Формы

Наш класс не будет работать, если не создать для него формы админки. Создадим в приложении файл forms.py.

extuser/forms.py

from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.auth import get_user_model


class UserCreationForm(forms.ModelForm):
    password1 = forms.CharField(
        label='Пароль',
        widget=forms.PasswordInput
    )
    password2 = forms.CharField(
        label='Подтверждение',
        widget=forms.PasswordInput
    )

    def clean_password2(self):
        password1 = self.cleaned_data.get('password1')
        password2 = self.cleaned_data.get('password2')
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError('Пароль и подтверждение не совпадают')
        return password2

    def save(self, commit=True):
        user = super(UserCreationForm, self).save(commit=False)
        user.set_password(self.cleaned_data['password1'])
        if commit:
            user.save()
        return user

    class Meta:
        model = get_user_model()
        fields = ('email',)


class UserChangeForm(forms.ModelForm):

    '''
    Форма для обновления данных пользователей. Нужна только для того, чтобы не
    видеть постоянных ошибок "Не заполнено поле password" при обновлении данных
    пользователя.
    '''
    password = ReadOnlyPasswordHashField(
        widget=forms.PasswordInput,
        required=False
    )

    def save(self, commit=True):
        user = super(UserChangeForm, self).save(commit=False)
        password = self.cleaned_data["password"]
        if password:
            user.set_password(password)
        if commit:
            user.save()
        return user

    class Meta:
        model = get_user_model()
        fields = ['email', ]


class LoginForm(forms.Form):

    """Форма для входа в систему
    """
    username = forms.CharField()
    password = forms.CharField()

Здесь описаны две формы - для создания нового пользователя и для смены пароля. Т.к. я использую Django REST Framework, я сделал эти формы для того, чтобы корректно работало обновление модели пользователя, т.к. поле password является обязательным. Если отправить запрос на указание, например, нового отчества, будет возвращена ошибка, т.к. поле пароля должно быть обязательно заполнено. Дополнительная форма решает эту проблему.

Как правило, ошибка обновления модели в DRF происходит при полном обновлении модели. Для частитчного обновления нужно указывать в заголовке HTTP-запроса метод PATCH вместо PUT.

Пожалуй, единственное, на что тут нужно обратить внимание - это описание связи наших форм с моделью пользователя. Если в будущем понадобится создать новую модель или переименовать её, переписывать код не придётся, т.к. используется функция get_user_model(), возвращающая класс модели пользователя, используемый для авторизации. Если проще, то эта функция возвращает класс, указанный в параметре AUTH_USER_MODEL в файле settings.py нашего приложения.

Админка

Изменения придётся внести и в файл admin.py:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group

from .forms import UserChangeForm
from .forms import UserCreationForm
from .models import ExtUser


class UserAdmin(UserAdmin):
    form = UserChangeForm
    add_form = UserCreationForm

    list_display = [
        'date_of_birth',
        'email',
        'firstname',
        'is_admin',
        'lastname',
        'middlename',
    ]

    list_filter = ('is_admin',)

    fieldsets = (
                (None, {'fields': ('email', 'password')}),
                ('Personal info', {
                 'fields': (
                     'avatar',
                     'date_of_birth',
                     'firstname',
                     'lastname',
                     'middlename',
                 )}),
                ('Permissions', {'fields': ('is_admin',)}),
                ('Important dates', {'fields': ('last_login',)}),
    )

    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': (
                'date_of_birth',
                'email',
                'password1',
                'password2'
            )}
         ),
    )

    search_fields = ('email',)
    ordering = ('email',)
    filter_horizontal = ()

# Регистрация нашей модели
admin.site.register(ExtUser, UserAdmin)
admin.site.unregister(Group)

Настройка Django

Пришло время переустановить Windows внести изменения в файл settings.py.

settings.py

# Тут должен быть импорт нужных модулей.

SECRET_KEY = # Какой-то ключ, автоматически созданный Django

DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []

EXTERNAL_APPS = (
    # У меня, например, здесь разные внешние библиотеки
)

INTERNAL_APPS = (
    # Тут список наших приложений
    'extuser',
    # А тут его продолжение
)

DJANGO_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',
)

INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS + DJANGO_APPS

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.locale.LocaleMiddleware',
)

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.csrf',
    'django.contrib.auth.context_processors.auth',
    'django.core.context_processors.debug',
    'django.core.context_processors.request',
)

AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.backends.ModelBackend",
)

LOGIN_URL = r"/login/"

AUTH_USER_MODEL = 'extuser.ExtUser'

Миграции

Заключительный этап настройки нашей модели - проведение миграций. Запустите скрипт:

python manage.py migrate

Для создания нового пользователя-администратора используйте команду createsuperuser:

python manage.py createsuperuser

str object has no attribute _meta

Досадная ошибка, с которой пришлось столкнуться, вынесена в название статьи. Потратил два часа на поиски, просмотрел все файлы по нескольку раз - и ничего. Сегодня с утра (а утро вечера мудренее) обнаружил причину.

Было:
class UserChangeForm(forms.ModelForm):

'''
Форма для обновления данных пользователей. Нужна только для того, чтобы не
видеть постоянных ошибок "Не заполнено поле password" при обновлении данных
пользователя.
'''
password = ReadOnlyPasswordHashField(
    widget=forms.PasswordInput,
    required=False
)

def clean_password(self):
    return self.initial['password']

def save(self, commit=True):
    user = super(UserChangeForm, self).save(commit=False)
    password = self.cleaned_data["password"]
    if password:
        user.set_password(password)
    if commit:
        user.save()
    return user

class Meta:
    model = get_user_model()
    fields = ('email')
Стало:
class UserChangeForm(forms.ModelForm):

'''
Форма для обновления данных пользователей. Нужна только для того, чтобы не
видеть постоянных ошибок "Не заполнено поле password" при обновлении данных
пользователя.
'''
password = ReadOnlyPasswordHashField(
    widget=forms.PasswordInput,
    required=False
)

def clean_password(self):
    return self.initial['password']

def save(self, commit=True):
    user = super(UserChangeForm, self).save(commit=False)
    password = self.cleaned_data["password"]
    if password:
        user.set_password(password)
    if commit:
        user.save()
    return user

class Meta:
    model = get_user_model()
    fields = ['email', ]
СУТЬ:

Если делаете перечисление полей в виде кортежа, после последнего элемента обязательно должна быть поставлена запятая. А лучше перечисляйте в квадратных скобках, как список - тогда точно не ошибётесь. Грабли, на которые наступают все новички, и я в том числе тоже.

Потенциальная возможность ошибки:
class Meta:
    model = get_user_model()
    fields = ('email',)
Ошибки исключены
class Meta:
    model = get_user_model()
    fields = ['email']

Работа с REST API в Angular JS

Предупреждение! Статья устарела, часть описанных в ней вещей касательно $resource не верна, планируется к удалению.

Введение

Так как я последнее время сильно увлечён фреймворком Angular, я стараюсь писать на нём настолько хорошо, насколько могу. Одна из последних интересных задач, с которыми мне пришлось столкнуться - работа с ресурсами. Для своего проекта я решил использовать так же Django REST Framework, т.к. видел о нём много положительных отзывов.

Ресурсом в данном контексте я понимаю экземпляр объекта, созданного Angular с помощью модуля $resource. Он позволяет легко обмениваться данными с сервером, полностью реализуя клиентскую часть архитектуры REST. В статье будут рассмотрены популярные решения и моё собственное.

Обзор REST

Рассматривать решение проблемы будем на примерах. Допустим, имеется новостной сайт. Каждая новость - это объект со своими свойствами. Кроме того, на сайте есть возможность оставлять комментарии. Итак, мы уже можем создать три взаимосвязанных класса объектов:

Пользователь
  • Логин
  • Дата регистрации
  • ФИО (для удобства объединим в одно поле, но на самом деле лучше сделать 3 отдельных)
  • Дата рождения
  • Блокировка
Новость
  • Автор - ссылка на пользователя
  • Дата публикации
  • Заголовок
  • Анонс
  • Полный текст
  • Опубликована (если нет, значит, находится в состоянии черновика)
  • Количество просмотров (автоматически увеличивается с каждым уникальным пользователем)
Комментарий
  • Автор - аналогично новости, ссылка на пользователя
  • Текст комментария
  • Новость - ссылка на описанную выше модель
  • Дата написания
  • Рейтинг - обновляется пользователями (автоматически вычисляется на сервере на основе анализа данных таблицы с плюсами и минусами)

Итак, у нас уже три сущности. Спроектируем API. Тут, в принципе, ничего сложного. Весь API уместится в несколько строк:

# Пользователи
/api/user/
/api/user/:id/
/api/user/:id/news/
/api/user/:id/comments/

# Новости
/api/news/
/api/news/:id/
/api/news/:id/comments/

# Комментарии
/api/comment/
/api/comment/:id/

Как видно, API получился очень простым. Сделаем допущение, что по ссылкам без указания версии используется последняя версия, а конкретную можно использовать, например, таким образом:

/api/v1/user/

Так же отметим, что здесь под :id имеется ввиду реальный id записи. Такое обозначение вдвойне удобно, учитывая, что подобным образом формируются ссылки при использовании модуля Angular Resource.

Так же важно понять, как именно работает данный API и почему так мало URL. Нижеприведённая таблица указывает, какого типа запросы отправляются на какой адрес и к чему это приводит:

URL Метод Результат
/api/user/ GET Возвращает список всех пользователей
POST Создаёт нового пользователя, возвращает его объект
/api/user/:id/ GET Возвращает информацию об указанном пользователе
POST Выполняет обновление информации об указанном пользователе
DELETE Удаляет указанного пользователя
/api/user/:id/news/ GET Возвращает все новости, автором которых является указанный пользователь

Сразу скажу, что при использовании Django REST Framework обновление записи методом POST запрещено, вместо неё следует использовать метод PUT. Для этого нужно будет особым образом сконфигурировать $resourceManager, о чём будет сказано ниже.

Установка $resource и его настройка

Для установки зависимостей в проектах я предпочитаю использовать bower. На указанном сайте рассмотрена установка данного менеджера пакетов, здесь я на ней останавливаться не буду. Помимо angular-resource понадобится так же пакет angular-cookie. Без правильной конфигурации печенек Django будет блокировать любые обращения к URL нашего API.

Итак, ставим пакеты:

bower install angular-resource angular-cookie --save

Если Angular не был установлен ранее, он будет подтянут как зависимость. Подключите все нужные библиотеки к странице.

Теперь рассмотрим скрипт инициализации нашего приложения (как правило, это файл, имеющий название app.js)

(function (A) {
    "use strict";
    var app = A.module('MyApp', ['ngResource', 'ngCookie']);

    app.config(['$resourceProvider', function($resourceProvider){
        $resourceProvider.defaults.stripTrailingSlashes = false;
    }]);

    app.run(['$http', '$cookies', function($http, $cookies) {
        $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
        $http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
    }]);

}(this.angular));

Собственно, что происходит в этих нескольких строках.

  • Указываем модуль angular-resource как зависимость для нашего приложения. Без этого ничего не заработает.
  • Конфигурируем провайдер ресурсов. Говоря человеческим языком, делаем фундаментальные настройки, которые повлияют на всё приложение целиком.
  • На этапе запуска приложения указываем, что любые HTTP-запросы должны содержать в себе поле X-CSRFToken. Значение берётся из Cookies, которые формируются на сервере в момент первого обращения к странице.

Работа с ресурсами

Здесь рассмотрим, как разработчики модуля $resource предлагают нам работать с ресурсами.

Получить нужную запись, изменить её данные и отправить изменения на сервер:

var News, //Ресурс
    newsInstance; //Экземпляр новости

function updateNewsItem(){
    newsInstance.published = false;
    newsInstance.$save();
}

News = $resource('/api/news/:id/');
newsInstance = News.get({id: 3}, updateNewsItem); //Новость с id = 3, просто для примера

А как создать новую запись? Вот так

newsInstance = new News({text: 'Примерный текст новости'});
newsInstance.$save();

Что насчёт обновления? Метод $update в свойствах объекта? Нет, не угадали.

newsInstance = News.get({id: 3});
newsInstance.author = 4; //Какой-то другой пользователь станет автором

News.update({id: 3}, newsInstance);

Немного странно, не правда ли? Скажу лишь, что скудность документации - одна из самых многочисленных жалоб на Angular.

Но это частности. Если вкратце, бОльшая часть решений, найденных мной в интернете, предполагает создание фабрики или сервиса, возвращающих созданный объект $resource, например, так мог бы выглядеть наш ресурс для работы с новостями:

(function (A){
    "use strict";
    var app = A.module('MyApp');

    app.factory('News', ['$resource', function ($resource) {
        return $resource('/api/news/:id/', {id: '@id'}, {
            update:{
                method: 'PUT' //Без этого не будет работать обновление объектов на стороне сервера,
                //если используется Django REST Framework
            }
        });
    }]);
}(this.angular));

Ладно, а что насчёт комментариев?


(function (A){
    "use strict";
    var app = A.module('MyApp');

    app.factory('Comment', ['$resource', function ($resource) {
        return $resource('/api/comment/:id/', {id: '@id'}, {
            update:{
                method: 'PUT' //Без этого не будет работать обновление объектов на стороне сервера,
                //если используется Django REST Framework
            }
        });
    }]);
}(this.angular));

Итак, налицо уже идёт дублирование кода! Значит, можно написать нечто такое:

(function (A){
    "use strict";
    var app = A.module('MyApp'),
        resources = {
            'User': '/api/user/:id/',
            'UserComments': '/api/user/:id/comments/',
            'UserNews': '/api/user/:id/news/',
            'News': '/api/news/:id/',
            'NewsCimments': '/api/news/:id/comments/',
            'Comment': '/api/comment/:id/'
        },
        idConfig = {id: '@id'},
        putSettings = {
            update: {
                method: 'PUT'
            }
        },
        i;

    for (i in resources){
        if (resources.hasOwnProperty(i)){ //Стандартная проверка, что это свойство собственное,
                                          //а не унаследовано от прототипа
            app.factory(i, ['$resource', function ($resource){
                return $resource(resources[i], idConfig, putSettings);
            }]);
        }
    }
}(this.angular));

Ну что ж, неплохо, если не считать предупреждения статического анализатора, что опасно создавать функции в цикле. Но по-прежнему остаётся несколько проблем:

  • Размножение сущностей. Почему для получения новости и комментариев к ней используются два разных ресурса?
  • Фабрика. Внутри Angular работает так, что при каждом вызове фабрики создаётся новый объект. Конечно, в JavaScript работает автоматический сборщик мусора, но сам подход не очень хорош.
  • В чём разница между $save и $update? В том, что во втором случае передаётся значение id? Почему бы тогда не вызывать нужный метод в зависимости от того, есть в объекте это свойство или нет?
  • Так и не решилась проблема с обновлением записей. Конечно, у созданного объекта есть все нужные методы, но выглядят они уродливо (это моё личное мнение), плюс приходится работать на уровне, достаточно близком к Pure JS, а хотелось бы чего-то более абстрагированного.
  • Что, если нам потребуются какие-то дополнительные методы для каждого из ресурсов? Например, автоматически выполнять локализацию дату публикации каждого комментария и каждой новости?
  • Безобразие с реализацией обещаний. В некоторых случаях методы ресурсов возвращают объект, а в некоторых - promise. Опять же, путаница.

Пытаясь избавиться от множества описанных проблем, я пришёл к тому, что всё надо делать самому и написал модуль, лишённый описанных выше недостатков.

В процессе написания модуля пришлось прибегнуть к ряду различных ресурсов, самым полезным из которых оказался MDN, конкретно - вот эта статья. Для имитации классического наследования я использовал описанный в стандарте ECMAScript 5 метод Object.create.

(function (A){
    "use strict";
    var app = A.module('MyApp'),
        idParams = {id: '@id'}, //Эта часть без изменений
        updParams = {
            update: {
                method: 'PUT'
            }
        };

    app.service('Managers', [
        '$q',
        '$resource',
        function($q, $resource){

            /*
             * Функция создаёт отложенный объект, т.е. тот, который
             * имеет свойство promise и может находиться только в
             * трех состояниях - разрешён, отклонён, в работе.
             * Как правило, такие объекты используются вместе с AJAX
             */
            function defer(){
                return $q.defer();
            }

            /*
             * Функция выполняет обращение к указанному ресурсу
             * указанным методом. При необходимости передаются
             * нужные параметры, например, экземпляр объекта.
             */
            function execQuery(method, data, resource){
                var d = defer();

                // Ниже выполняется обращение к нужному методу ресурса
                resource[method](
                    data,
                    function(response){ //Успешное выполнение запроса
                        d.resolve(response);
                    },
                    function (response){ //Запрос отклонён (ошибки)
                        d.reject(response);
                    }
                );

                return d.promise;
            }

            /*
             * Универсальная функция для создания ресурсов. Принимает
             * на вход два параметра - URL для обращения к API и
             * объект конфигурации. Если он не указан, используется
             * объект по-умолчанию, который будет брать id из
             * свойств самого объекта, передаваемого на вход ресурса.
             */
            function createResource(url, params){
                return $resource(url, params || idParams, updParams);
            }

            function BaseManager(url, params){
                this.sources = {
                    main: createResource(url, params);
                };
            }

            // А теперь к прототипу добавим нужные методы

            // Получить объект по id
            BaseManager.prototype.getById = function(id){
                return execQuery('get', {id: id}, this.sources.main);
            };

            // Получить все объекты
            BaseManager.prototype.getAll = function(){
                return execQuery('query', {}, this.sources.main);
            };

            //Удалить объект
            BaseManager.prototype.remove = function(id){
                return execQuery('remove', {id: id}, this.sources.main);
            };

            //Сохранить или обновить объект
            BaseManager.prototype.save = function(item){
                var method = item.hasOwnProperty('id') ? 'update' : 'save';

                return execQuery(method, item, this.sources.main);
            };



            // Создадим объект новостей, используя шаблон классического
            // наследования средствами ECMAScript 5

            function News(){
                //Вызов "предка" применительно к нашему "классу"
                BaseManager.apply(this, ['/api/news/:id/']);

                //Создадим дополнительный источник данных
                this.sources.comments = createResource('/api/news/:id/comments/');
            }

            // В статье на MDN описано, что здесь происходит
            News.prototype = Object.create(BaseManager.prototype);
            News.prototype.constructor = News;

            // А теперь - собственный метод
            News.prototype.getComments = function (id){
                return execQuery('query', {id: id}, this.sources.comments);
            };


            // Комментарии
            function Comment(){
                BaseManager.apply(this, ['/api/comment/:id/']);
            }

            Comment.prototype = Object.create(BaseManager);
            Comment.prototype.constructor = Comment;


            // Создаём объект, который будет хранить по одному экземпляру
            // Описанных выше классов

            return {
                News: new News(),
                Comment: new Comment()
            };
    }]);
}(this.angular));

Поскольку service реализует в Angular паттерн Одиночка, объект с экземплярами наших классов будет создан всего лишь один раз. Затем при каждом следующем вызове будет возвращено ранее сформированное значение. Посмотрим на практический пример применения описанного выше сервиса:

(function (A){
    "use strict";
    var app = A.module('MyApp');

    app.controller('NewsListController', ['$scope', 'Managers', function ($scope, Managers){

        // Загрузка всех новостей
        function loadNews(){
            $scope.loading = true;
            $scope.news = [];
            Managers.News.getAll().then(
                function (rows){
                    $scope.news = rows;
                },
                function (response){
                    $scope.errors = response.data; // В DRF - именно так
                }
            );
        }

        loadNews();

        // Загрузка комментариев при нажатии на ссылку "Комментарии" для
        // выбранной новости
        $scope.loadNewsComments = function(newsInstance){
            newsInstance.commentsLoading = true;

            Managers.News.getComments(newsInstance.id).then(
                function(rows){
                    newsInstance.commentsLoading = false;
                    newsInstance.comments = rows;
                },
                function (response){
                    newsInstance.commentsLoading = false;
                    newsInstance.errors = response.data;
                }
            );
        }
    }]);
}(this.angular));

Как тот же код мог бы работать в личном кабинете администратора:

(function (A){
    "use strict";
    var app = A.module('MyApp');

    app.controller('NewsCreateController', ['$scope', 'Managers', function ($scope, Managers){
        $scope.save = function(){
            Managers.News.save($scope.news_data).then(
                function (response){
                    $scope.news_data = response; //Теперь у нашей новости
                    // появился id, и вообще все данные теперь - с сервера
                },
                function (response){
                    $scope.errors = response.data;
                }
            );
        };
    }]);

    app.controller('NewsEditController', [
        '$scope',
        '$routeParams', // Взят для примера, для работы требует angular-route
        'Managers',
        function ($scope, $routeParams, Managers){
            var newsId = $routeParams.id;

            $scope.loaging = true;
            Managers.News.getById(newsId).then(
                function (response){
                    $scope.loading = false;
                    $scope.news_item = response;
                },
                function (response){
                    $scope.loading = false;
                    $scope.errors = response.data;
                }
            );

            $scope.save = function(){
                $scope.saving = true;
                Managers.News.save($scope.news_item).then(
                    function (response){
                        $scope.saving = false;
                        $scope.news_item = response; //Строго говоря, это делать
                        //необязательно
                    },
                    function (response){
                        $scope.saving = false;
                        $scope.errors = response.data;
                    }
                );
            };

    }]);
}(this.angular));

Как вы заметили, отличия в этих контроллерах практически минимальны. Перед нами - унифицированный способ работы с данными. Одним из важнейших преимуществ описанного подхода является так же использование наследования, что приводит к уменьшению количества кода.

Преветствуется конструктивная критика рассмотренных решений

P. S. Далее пойдёт описание того, как я создавал REST API на сервере.

Анонс статьи по Django REST Framework и Angular

В сети крайне мало информации по работе с Django REST Framework, а на русском можно сказать что нет вообще. Учитывая сложность вопросов на Toster и устарелость статей на Хабре, решил написать довольно объёмную статью по работе с данным фреймворком. Сам не являюсь профессионалом в нём и WEB-разработке вообще, но поделиться накопленными знаниями будет не лишне.

Примерное оглавление

  • Зачем нужен DRF?
  • Установка, подключение к проекту.
  • Сериализаторы
  • Виды-функции и виды-классы
  • Типовые задачи
  • Подключение Angular Resource
  • Несколько советов по API
  • Исходный код моего модуля Angular для работы с API с объяснением написанного

Пишите в комментариях, какие вопросы дополнительно следует включить в статью, постараюсь рассмотреть их все.

Перекат на OpenSUSE с возвратом к Debian'у

Причины

Долгое время на рабочем ноуте у меня стояла Windows 7. Работала она там несколько лет и всем, в общем-то, устраивала, однако, со временем я пришёл к мысли, что так дальше продолжать нельзя и для повышения уровня следует полностью перейти на Linux, не ограничиваясь только поддержкой серверов, на которых стоит ПО, которое поддерживает контора, в которой я работаю (хотел написать "моя", но вспомнил, что учредитель не я).

Первым делом я попробовал поставить на ноутбук Ubuntu. "Раз уж она для серверов не очень, то может быть, будет работать на лэптопе?" - подумлал я, скачал установочный образ и приступил к установке.

Прямо тут и начались проблемы. Напоминаю, на ноутбуке стояла Windows 7, которая при создании разделов использует MBR. Однако, для загрузки системы в UEFI необходимо наличие раздела EFI на жёстком диске, а с MBR это сделать невозможно. Опуская подробности скажу, что даже GParted выдавал информацию о том, что диск не имеет никакой разметки.

Я перенёс все данные на съёмный накопитель, а особо ценные продублировал в облаке, и приступил к дальнейшим поискам решения. Поиски завершились получением следующего знания:

Для корректной работы Linux на машине с UEFI необходимо, чтобы таблица разделов жёсткого диска была организована через GPT, а не MBR.

Удалил все разделы, пересоздал таблицу и успешно установил систему. Дальше начались будни. Через несколько дней я почувствовал, что работа Ubuntu и её навязчивый сервис вызывает во мне отторжение. Взял ноутбук домой и успешно установил Debian 7 Wheezy. Добавил репозитории NodeJS, PostgreSQL и некоторых других проектов. Проблема пришла, откуда не ждали. Не было звука. После многочисленных опытов скачал исходные коды с сайта производителя, скомпилировал из них драйверы и добавил их в ядро. Звук в наушниках появился, в динамиках - нет. OOOOOOK.

Пришло время подключиться к AD. Помучив помощника админа, ноут и систему, я так и не смог довести конфиги до рабочего состояния, поэтому шара фирмы была для меня закрыта. Тут вдруг наступили праздники, и я решил, что можно будет не умирать от безделья, а попробовать пожить в OpenSUSE, благо один знакомый уже давно пользуется этим дистрибутивом и, теоретически, к нему можно будет обратиться за помощью.

Удобства

Что порадовало в этом дистрибутиве:

  • Звук "из коробки" - неудивительно, ядро свежее и содержит все необходимые драйверы
  • Большинство пакетов, в т.ч. всякая проприетарщина - в стандартных репозиториях
  • Почти официальный репозиторий драйверов nVidia. На рабочем ноуте я никогда не играл, но сам факт наличия таких репок порадовал.
  • "Скользящие" обновления. По-моему, это большой шаг не только для OpenSUSE, но и для Ubuntu. Кратко суть: при формировании дистрибутива последняя версия Gnome была, например, 3.11. Всё время поддержки дистрибутива версии 7.0 она не будет меняться, будут выходить лишь патчи и исправления уязвимостей. При скользящих же обновлениях в репозиторий помещается последняя стабильная версия программы, и обновить Gnome до новой версии довольно просто, нужно лишь выполнить обычное обновление системы. В итоге имеем систему в состоянии Stable, но со свежими версиями пакетов.

Что не нравится

Тем не менее, некоторые вещи мне не очень понравились (ну ещё бы)

  • Во-первых, нет жёстких зависимостей между пакетами. После удаления какой-либо программы куча пакетов, которые ставились вместе с ней, остаётся в системе. Хотя некоторые пользователи данного дистрибутива позиционируют это как огромное преимущество. Я пока не вкусил всех прелестей, но мне уже не нравится.
  • Во-вторых, проблемы с кодеками. Да, тут с этим сложнее, чем в Debian. Там было достаточно прописать в свойствах репозитория non-free, и можно было ставить кодеки mp3 и h264. Все советуют использовать репозиторий Packman. Подключил, обновил полсистемы, никакого результата. Проблему с mp3 решил, h264 играет только системный плеер, VLC мимо кассы.
  • В-третьих, ещё неизвестно, будет ли работать здесь AD. До выхода на работу ещё 4 дня, и проверить не смогу.

Возможно, мне этот дистрибутив придётся по душе и я на нём пока остановлюсь. Посмотрим, что нам принесёт Debian 8, который сейчас в стадии заморозки.

UPD: к домену так и не смог подключиться, также при появлении уведомлений Skype приостанавливается воспроизведение музыки. Работа с виртуальными окружениями Python значительно хуже, чем в Debian. Подумываю о переустановке винды, т.к. работать с шарой моей конторы периодически приходится, а сейчас к ней доступа нет.