Собственная модель пользователя Django (>=1.8)
Введение
На данную тему в Интернете уже написано огромное множество статей, и моя станет лишь очередной попыткой описать то, что уже и так широко известно. Не претендуя на оригинальность, я попробую описать тот способ, которым пользуюсь сам. В статье пойдёт речь о расширении стандартной модели 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 является обязательным. Если отправить запрос на указание, например, нового отчества, будет возвращена ошибка, т.к. поле пароля должно быть обязательно заполнено. Дополнительная форма решает эту проблему.
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
Введение
Так как я последнее время сильно увлечён фреймворком 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. Подумываю о переустановке винды, т.к. работать с шарой моей конторы периодически приходится, а сейчас к ней доступа нет.
Ошибка при запуске Eclipse Luna в Debian
Проблема
Я на работе окончательно переехал на Debian. После двух дней ненависти к Ubuntu понял, что лучше иметь старые проверенные пакеты, чем новые и постоянно глючащие. Как обычно, после установки системы поставил JRE:
apt-get update && apt-get install openjdk-7-jre -y
Скачал и распаковал последнюю стабильную версию Eclipse с официального сайта. Попробовал запустить:
./eclipse
Получил кучу ошибок, среди которых была такая: # C [libgdk-x11-2.0.so.0+0x5173f] gdk_display_open+0x3f.
Решение
Решение оказалось на редкость простым. Нужно импортировать в окружение переменную SWT_GTK3=0, тогда всё будет работать. Прописал в конце файла /etc/profile:
export SWT_GTK3=0
Чтобы не перезагружать систему, сделал то же самое для текущего пользователя, т.е. выполнил указанную выше команду. Всё работает!
Django и $resource
Суть проблемы
Решил использовать для реализации RESTful API своего приложения такой модуль Angular, как angular-resource. Проблем с установной и подключением, как обычно, не было. Создал свой сервис:
(function (A, U) { //U === undefined
"use strict";
A.module('DesktopApplication').factory('Category', function ($resource) {
var instance = {
selectedRow: U, //Используется в моём проекте для обмена данными между контроллерами,
//не является обязательным элементом
resource: $resource('/category/rows/:id', {id: '@id'})
};
return instance;
});
}(this.angular));
Казалось бы, всё должно работать. И ведь правда, вот такое работает (GET-запрос на получение данных):
//Где-то в коде, использующем сервис Category
Category.resource.query(function (items){
$scope.rows = items;
});
А вот такое уже нет:
//Чуть ниже
Category.save({'id': 4, 'name': 'Username'}, function (result) {
$scope.success = result.success;
});
Проблема оказалась в том, что даже явно указав слэш в конце строки ресурса, я получил POST-запросы, идущие к адресам без слэша, и Django такие запросы тут же банит. Начался поиск и чтение StackOverflow.
Решение
Решение оказалось довольно простым. Нужно указать в настройках приложения, что удалять концевой слэш для URL в AJAX-запросах не надо (а по-умолчанию включено). Кроме того, для корректной работы запросов методом PUT и DELETE нужно дополнительно конфигурировать $http. Иначе декоратор @csrf_protect из комплекта Django будет резать такие запросы. С учётом вышесказанного, скрипт настройки приложения стал выглядеть так (для сокращения размера выброшены прочие модули, которые я на самом деле использую в своём проекте):
(function(A) {
"use strict";
A.module('DesktopApplication', [ 'ngCookies', 'ngResource')
.config(function ($interpolateProvider, $resourceProvider) {
$interpolateProvider.startSymbol('{$');
$interpolateProvider.endSymbol('$}');
$resourceProvider.defaults.stripTrailingSlashes = false;
})
.run(function ($http, $cookies) {
$http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
$http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
$http.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
});
}(this.angular, this.jQuery));