tag:blogger.com,1999:blog-15795030895298628162024-03-16T12:21:22.262+05:00dark_barker: технические заметкиДополнительный блог, куда я пишу разные статьи, заметки и решения каких-либо технических вопросов. Основная тематика блога: программирование, операционные системы, алгоритмы, java, linux, технологии.Anonymoushttp://www.blogger.com/profile/03220456202144299440noreply@blogger.comBlogger90125tag:blogger.com,1999:blog-1579503089529862816.post-19297171870572029532018-12-24T16:30:00.002+05:002018-12-24T16:34:38.074+05:00django: фильтр нужных разрешений юзера в админкеВ админке Django всегда делаю чтобы в настройках профиля и настройках группы светились только нужные, используемые в логике пермишены (например, свои или какие-то отдельные стандартные).<br />
Иначе их обычно слишком много и непонятно какие в реальности имеют смысл.<br />
Просто перегружаются Admin-классы и формы в них, где фильтруется ModelForm.queryset у нужного поля формы.<br />
<br />
<code>admin.py</code><br />
<pre class="brush:python">from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.forms import UserChangeForm
from django.db.models import Q
from django import forms
USED_PERMS = [
'reports.add_report',
'reports.change_report',
'app2.custom_perm',
]
def qs_perm_filter(qs):
ups = [up.split('.') for up in USED_PERMS]
q_expressions = [Q(content_type__app_label=up[0], codename=up[1]) for up in ups]
return qs.filter(reduce(operator.or_, q_expressions))
# наследуем форму (у меня там ещё много помимо этого), также см. нужную model
class UserProfileChangeForm(UserChangeForm):
class Meta:
model = UserProfile
fields = '__all__'
def __init__(self, *args, **kwargs):
super(UserProfileChangeForm, self).__init__(*args, **kwargs)
f = self.fields.get('user_permissions')
if f is not None:
f.queryset = qs_perm_filter(f.queryset)
# наследуем кастомный UserAdmin
class UserProfileAdmin(UserAdmin):
...
form = UserProfileChangeForm
...
# показываем профиль, родной User не показываем
admin.site.unregister(User)
admin.site.register(UserProfile, UserProfileAdmin)
# для групп тоже аналогично всё
class MyGroupAdminForm(forms.ModelForm):
class Meta:
model = Group
fields = '__all__'
def __init__(self, *args, **kwargs):
super(MyGroupAdminForm, self).__init__(*args, **kwargs)
f = self.fields.get('permissions')
if f is not None:
f.queryset = qs_perm_filter(f.queryset)
class MyGroupAdmin(GroupAdmin):
form = MyGroupAdminForm
admin.site.unregister(Group)
admin.site.register(Group, MyGroupAdmin)
</pre><br />
Также для бонуса - вынос пермишенов пользователя в таблицу с юзерами в админке. Из списка видно кто и каким правами обладает, там все группы, персональные права, указание на доступ к админке и т.д:<br />
<a name='more'></a><br />
<pre class="brush:python">class UserProfileAdmin(UserAdmin):
...
list_display = (..., 'custom_groups', ...)
...
# список групп юзера через запятую, указание что персонал итд
def custom_groups(self, obj):
cg = [g.name for g in obj.groups.all()]
if obj.is_superuser:
cg.insert(0, '[all perms]')
if obj.is_staff:
cg.insert(0, '[staff]')
if obj.user_permissions.count() > 0:
listperm = ',\n'.join(up.__str__() for up in obj.user_permissions.all())
cg.append('[<abbr title="Personal perms: %s">Personal perms</abbr>]' % listperm)
return ', '.join(cg)
custom_groups.short_description = 'User perms'
custom_groups.allow_tags = True
</pre>Anonymoushttp://www.blogger.com/profile/03220456202144299440noreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-65438179551496217992018-11-21T19:16:00.000+05:002018-11-21T19:23:52.682+05:00Django не давать логиниться одновременно с двух разных местДругими словами, не разрешаются две параллельные сессии. При новом входе старый разлогинивается.<br />
Может понадобиться для какого-нибудь портала в интранете или чего-то такого секюрного.<br />
Решается всё довольно банально, есть специальный сигнал <code>user_logged_in</code>.<br />
Ищем сессию для этого юзера, если нашли - сбрасываем в базе, сбрасываем в текущем engine (если это применимо).<br />
Если что, у меня настроен один из стандартных:<br />
<pre class="brush:text">SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
</pre>Код обработчика сигнала. Всё заполонил комментариями, думаю, там всё понятно.<br />
<pre class="brush:python">from django.contrib.auth.signals import user_logged_in
from django.contrib import messages
from django.contrib.sessions.models import Session
from django.utils import timezone
def user_logged_in_handler(sender, request, user, **kwargs):
# текущая сессия: request.session.session_key
for session in Session.objects.filter(expire_date__gte=timezone.now()).exclude(session_key=request.session.session_key):
if session.get_decoded().get('_auth_user_id', None) == str(user.id):
# нашли какую-то сессию, делаем её expired
session.expire_date = timezone.now()
session.save()
# далее нам надо очистить все данные сессии
# сначала я пробовал так
# SessionStore = session.get_session_store_class()
# sesstor = SessionStore(session.session_key)
# sesstor.flush()
# где flush - это метод именно для моего бекенда cached_db
# я ожидал, что для модели вернётся текущий настроенный в settings.SESSION_ENGINE бекенд, но в модели Session захардкожено (как ни странно?) db.SessionStore
# в принципе можно напрямую тупо импортировать cached_db.SessionStore, но сделал проще и универсальнее:
# request.session у нас уже содержит установленный мидлварей нужный SessionStore
# с помощью "чужой" нашей новой SessionStore удаляем левый наш старый session_key, там удачно есть такой даже метод
request.session.delete(session.session_key)
# раз у нас доступен request, то при желании показываем юзеру сообщение
messages.add_message(request, messages.INFO, 'Была закрыта дублирующаяся сессия на каком-то другом компьютере')
user_logged_in.connect(user_logged_in_handler)
</pre>Anonymoushttp://www.blogger.com/profile/03220456202144299440noreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-60271105965857830212016-04-14T22:07:00.001+05:002016-04-14T22:12:55.809+05:00turn-сервер coturn webrtc error 401: UnauthorisedСервер coturn по дефолту не вполне годный для использования в качестве turn-сервера для webrtc-клиентов. На stun-запросы нормально отвечает, но при попытке использования в качестве turn всё время проблемы в клиенте (не вполне очевидно отлаживаемые), а в логах либо ошибки подключения, либо что-то типа "401: Unauthorised".<br />
<br />
WebRtc не станет работать нормально без авторизации с turn-сервером, потому там надо настроить и авторизацию. Используем long-term механизм с предопределённым логином-паролем. Там есть более хитрые механизмы, а также использование ключей в кач-ве паролей итд, но суть задачи не в этом.<br />
<br />
в <code>/etc/turnuserdb.conf</code> прописывается логин-пароль:<br />
<pre class="brush:text">qwerty:asdfgh</pre><br />
в конфиге <code>/etc/turnserver.conf</code> прописываются/раскоменчиваются минимально необходимые настройки:<br />
<pre class="brush:text"># использование fingerprint, обычно webrtc его хочет
fingerprint
# включение long-term авторизации (хотя вроде автоматически должен включаться, если прописан хоть один аккаунт походящий)
lt-cred-mech
# файл с логинами-паролями (можно прописать напрямую в этом же конфиге, но не очень красиво)
userdb=/etc/turnuserdb.conf
# дефолтрый реалм тоже нужно
realm=qwerty</pre>После этого сервер откликается на настройки из webrtc типа<pre class="brush:text">{urls:'turn:IP:3478',username:'qwerty',credential:'asdfgh',}</pre>Anonymoushttp://www.blogger.com/profile/03220456202144299440noreply@blogger.com1tag:blogger.com,1999:blog-1579503089529862816.post-3734207202287707122016-02-08T11:42:00.004+05:002016-02-08T11:47:29.776+05:00dlango: autocomplete_light дополнительный рендер в jsonЕсли цель - просто изменить рендер не в готовый html (для родных виджетов приложения), а в другой вид, в том числе json, то всё просто. Здесь же речь о том, чтобы добавить просто новый способ параллельно с полноценно работающим web-способом. В итоге будет и точно так же работающие автодополнения, рендерящие не в html, а в json, и индексная справка по корню, с корректными новыми url, указывающими на наши новые методы из api. <br />
<br />
urls.py<br />
<pre class="brush:python">from autocomplete_light.views import RegistryView
...
url(r'^api/autocomplete/(?P<autocomplete>[-\w]+)/$', views.ApiAutocompleteView.as_view(), name='api_autocomplete_light_autocomplete'),
url(r'^api/autocomplete/$', RegistryView.as_view(template_name='autocomplete_light/api_registry.html'), name='api_autocomplete_light_registry'),
...</pre><br />
Вторая задача (вывод списка зарегистрированных автодополнений) решается вообще без переопределения, используем стандартную RegistryView, которая вполне подходит, нужно только переопределить template_name и в новом шаблоне вызвать get_absolute_url_api вместо get_absolute_url.<br />
<br />
api_registry.html<br />
<pre class="brush:html">{% if registry|length %}
<h2>List of your {{ registry_items|length }} registered api-autocompletes</h2>
<table> {% for name, autocomplete in registry_items %}
<tr>
<td><br />
{{ name }}<br />
</td>
<td><br />
<a href="{{ autocomplete.get_absolute_url_api }}">{{ autocomplete.get_absolute_url_api }}</a><br />
</td>
</tr>
{% endfor %}
</table>{% else %}
<p>You have not registered any api-autocomplete</p>{% endif %}
</pre><a name='more'></a><br />
Для всех autocomplete из registry приложения добавим новый миксин, который и содержит этот метод:<br />
<br />
<pre class="brush:python">class AutocompleteApiMixin(object):
def get_absolute_url_api(self):
# NoReverseMatch не будем обрабатывать для простоты
return urlresolvers.reverse('mysite:api_autocomplete_light_autocomplete', args=(self.__class__.__name__,))</pre><br />
и добавим во все модельки (autocomplete_light_registry.py)<br />
<pre class="brush:python">class UserAutocomplete(AutocompleteApiMixin, autocomplete_light.AutocompleteModelBase):
search_fields = ['username', 'first_name', 'last_name']
model = MyUser
autocomplete_light.register(UserAutocomplete)</pre><br />
Первая же задача (собственно, автодополнение) решается либо написанием некоего ApiAutocompleteView по мотивам родного (см. в примере в urls.py выше):<br />
<br />
<pre class="brush:python">class ApiAutocompleteView(GetRegistryMixin, generic.View):
def get(self, request, *args, **kwargs):
try:
autocomplete_class = self.get_registry()[kwargs['autocomplete']]
except AutocompleteNotRegistered:
return HttpResponseBadRequest()
autocomplete = autocomplete_class(request=request)
# далее вместо autocomplete.autocomplete_html() используем autocomplete.choices_for_request()
# и делаем с ним что хотим - рендерим в json, например...
return HttpResponse(...)</pre><br />
Либо в одном из вариантов я делал так: вообще ничего не переопределял, а перегрузил в вышеупомянутом миксине AutocompleteApiMixin метод autocomplete_html, который рендерил в нужный мне вид в зависимости от условий (но для этого у меня удачно оказалось в request пропертис is_api, по которому я определял отдавать родной выход или рендерить кастомно), например, так:<br />
<br />
<pre class="brush:python">class AutocompleteApiMixin(object):
...
def autocomplete_html(self):
if not self.request.is_api:
return super(AutocompleteApiMixin, self).autocomplete_html()
# здесь делаем по другому
</pre>Anonymoushttp://www.blogger.com/profile/03220456202144299440noreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-39351757800834923352015-09-28T17:14:00.001+05:002015-09-28T17:19:12.300+05:00 ${0%${0##*/}}Есть небольшой трюк в bash, который мне давно нравился — <b>получение текущей директории запущенного скрипта</b>, используя только <code>$0</code> и операции над строками bash-а. Это то, что в заголовке. Как вариант, его можно использовать в виде:<br />
<pre class="brush:bash">cd ${0%${0##*/}}</pre>Исходные данные: <code>$0</code> - полный путь запущенного скрипта. Понятно, что скрипт надо выполнять по полному пути, иначе использование метода лишено смысла.<br />
<br />
Используются последовательно две операции над строками:<br />
<i>${string##substring} - удаление самой длинной, из найденных, подстроки $substring в строке $string. Поиск ведется с начала строки.<br />
${string%substring} - удаление самой короткой, из найденных, подстроки $substring в строке $string. Поиск ведется с конца строки.</i><br />
<br />
<code>${0%<u>${0##*/}</u>}</code> - самая длинная из найденных строк <code>*/</code> — это весь путь до последнего слеша включительно. Если удалить это из полного пути, то получится просто имя файла самого скрипта (без пути). <br />
<br />
<code><u>${0%</u>${0##*/}<u>}</u></code> - далее, если из <code>$0</code> («полный путь») удалить «имя файла» (получено выше), то получится как раз «путь без имени файла».Anonymoushttp://www.blogger.com/profile/03220456202144299440noreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-87504383549411421322014-03-05T20:28:00.002+06:002014-03-05T20:29:42.093+06:00django: самодельный if-else тегЗдесь пример самодельного тега шаблонизатора django, выполняющего роль if-else-endif. Мой код подразумевает переменную 'current_statuskey' в контексте (кладётся напрямую или с помощью middleware). Писал для очень сложной страницы с кучей частей, имеющих разный вид в зависимости от статуса. Вообще там ещё у меня дополнительная логика, но это минимальный пример здесь. Использование в шаблоне:
<pre class="brush:text">{% ifstatus one,five,blabla %}
...
{% else %}
...
{% endifstatus %}</pre>
Можно было описать в десяток строк, но здесь больше и код очень сильно похож на стандартные теги ifequals и сопутствующие, откуда я его и адаптировал, чтобы получить более правильную и канонiчную реализацию.<a name='more'></a>
<pre class="brush:python">from django.template.base import Node, NodeList, TemplateSyntaxError, Library
register = Library()
class IfStatusNode(Node):
child_nodelists = ('nodelist_true', 'nodelist_false')
def __init__(self, varlist, nodelist_true, nodelist_false, negate):
self.varlist = varlist
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
self.negate = negate
def __repr__(self):
return "<IfStatusNode>"
def render(self, context):
current_statuskey = context.get('current_statuskey', None)
if (self.negate and current_statuskey not in self.varlist) or (not self.negate and current_statuskey in self.varlist):
return self.nodelist_true.render(context)
return self.nodelist_false.render(context)
def do_ifequal(parser, token, negate):
bits = list(token.split_contents())
if len(bits) != 2:
raise TemplateSyntaxError("%r takes one arguments" % bits[0])
end_tag = 'end' + bits[0]
nodelist_true = parser.parse(('else', end_tag))
token = parser.next_token()
if token.contents == 'else':
nodelist_false = parser.parse((end_tag,))
parser.delete_first_token()
else:
nodelist_false = NodeList()
#status1,status2
#"status1,status2, status3"
stats = bits[1]
if stats.startswith('"') and stats.endswith('"'):
stats = stats[1:-1]
varlist = [x.strip() for x in stats.split(',')]
return IfStatusNode(varlist, nodelist_true, nodelist_false, negate)
@register.tag
def ifstatus(parser, token):
"""
current_statuskey must be in request.
check that the status is one of the values separated by commas.
"""
return do_ifequal(parser, token, False)
@register.tag
def ifnotstatus(parser, token):
return do_ifequal(parser, token, True)</pre>
Anonymoushttp://www.blogger.com/profile/03220456202144299440noreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-67498954543531652612014-02-04T18:10:00.001+06:002014-02-05T13:56:26.042+06:00ant: компиляция и сборка jar прямиком с gitПояснять особо нечего тут. Используются только стандартные таски. Для git clone используется exec, далее компилируется и собирается как обычно. Ниже пример моего ant-скрипта для сборки с git либы json.jar.<a name='more'></a><br />
<pre class="brush:xml"><?xml version="1.0" encoding="UTF-8"?>
<project name="json.jar" default="build" basedir=".">
<description>build json.jar (org.json.*) from github</description>
<property name="gitrep" value="https://github.com/douglascrockford/JSON-java"/>
<property name="projname" value="JSON-java"/>
<property name="classesdir" value="classes"/>
<property name="destjar" value="json.jar"/>
<property name="srcdir" value="src"/>
<property name="srcreldir" value="org/json"/>
<property name="srcdestjar" value="json-src.jar"/>
<target name="gitpull">
<exec executable="git">
<arg value="clone"/>
<arg value="${gitrep}"/>
</exec>
</target>
<target name="compile" depends="gitpull">
<mkdir dir="${classesdir}"/>
<javac srcdir="${projname}" destdir="${classesdir}" includes="**/*.java" target="1.6" source="1.6" includeantruntime="false">
<!-- ... /-->
</javac>
</target>
<target name="createjar" depends="compile">
<jar destfile="${destjar}" basedir="${classesdir}"/>
</target>
<target name="preparesrc" depends="gitpull">
<copy todir="${srcdir}/${srcreldir}">
<fileset dir="${projname}"/>
</copy>
</target>
<target name="createsrcjar" depends="preparesrc">
<jar destfile="${srcdestjar}" basedir="${srcdir}"/>
</target>
<target name="clean">
<delete dir="${projname}"/>
<delete dir="${classesdir}"/>
<delete dir="${srcdir}"/>
</target>
<target name="build" depends="createjar, createsrcjar, clean"/>
</project>
</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-9199138371318470802013-12-15T19:29:00.000+06:002014-05-06T19:34:35.221+06:00проект django project на shared hosting хостинге mod_wsgiИсторическая заметка по большей части. Черновик схемы настройки проекта на django 1.4 на хостинге с mod_wsgi.<a name='more'></a>
<h2>Установка virtualenv</h2>
<pre class="brush:bash">wget https://raw.github.com/pypa/virtualenv/master/virtualenv.py
python ./virtualenv.py ~/env
source ~/env/bin/activate</pre>
Это устанавливает в home нам virtualenv. Можно добавить в ~/.bashrc или аналог, если других окружений нету и после захода по ssh сразу было активировано.
Проверяем:
<pre class="brush:bash">$ which python
/home/hosting_user/env/bin/python</pre>
Всякие модули будут установлены в ~/env/lib/python-xx/site-packages/
<h2>Установка и настройка django</h2>
Либо wget, либо логичнее устанавливать pip-ом.
<pre class="brush:bash">pip install Django==1.4.7</pre>
Проверяем:
<pre class="brush:bash">$ python
Python 2.6.6 (r266:84292, Dec 26 2010, 22:31:48)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import django
>>> django.VERSION
(1, 4, 7, 'final', 0)
>>> quit()</pre>
Хотя и django и остальные пакеты/либы я переношу через pip и зависимости (типа pip install -r req.txt).
Само приложение в каждом конкретном хостинге работает через файл с определённым именем (после создания проекта на хостинге через панель, например), в моём случае это был django.wsgi, у меня он был примерно таким в итоге:
<pre class="brush:python">#!/home/hosting_user/env/bin/python
# -*- coding: utf-8 -*-
import os
import sys
activate_this = '/home/hosting_user/env/bin/activate_this.py'
execfile(activate_this, dict(__file__=activate_this))
sys.path.insert(0, '/home/hosting_user/env/lib/python2.6/site-packages')
sys.path.insert(0, '/home/hosting_user/projects/mysite')
os.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()</pre>
Далее, статика собирается обычно:
<pre class="brush:bash">python manage.py collectstatic</pre>
Остальное тоже:
<pre class="brush:bash">python manage.py syncdb</pre>
<h2>Как установить другой python</h2>
На шаред-хостинге с mod_wsgi это бесполезно, т.к. приложение всё равно под ним запустить не получится по очевидным причинам. Но вообще всё просто, на всякий случай (у меня под ним собирались кое-какие пакеты):
<pre class="brush:bash">wget http://www.python.org/ftp/python/2.7.3/Python-2.7.3.tgz
tar xzf ./Python-2.7.3.tgz
cd Python-2.7.3
./configure --prefix=$HOME/Python27
make
make install</pre>
Если надо под ним virtualenv, то его надо пересоздать (но добавить в .bashrc например export PATH="$HOME/Python27/bin:$PATH").
Anonymoushttp://www.blogger.com/profile/03220456202144299440noreply@blogger.com1tag:blogger.com,1999:blog-1579503089529862816.post-37942021221135959122013-11-13T22:56:00.002+06:002013-11-13T23:17:13.016+06:00django: contrib.comments работаем через ajaxТут рассказ о том, как заставить стандартный django.contrib.comments работать через ajax. Цель была такова: минимальные изменения в клиентских view, чтобы работали стандартные templates перегружаемые, полноценная админка (обычная от приложения comments), да и вообще максимальное использование оригинального кода. Короче, в итоге после нескольких самопроизвольных переписываний (вместе с использованием подобного решения в проектах) получилась минимальная обёртка, которую по идее тоже можно вынести в отдельный app.<br />
<br />
Да, скоро это уже не совсем актуально станет (django.contrib.comments устаревший и с версии 1.6 вынесен в отдельный проект), но решение отточилось на третьей версии и мне жалко его терять просто :) Пример тут для версии 1.5.<a name='more'></a><br />
<br />
Вьюшки получают нужный экземпляр модели и передают в шаблоны для рендера их через стандартные теги (такова задумка оригинального приложения), см. ниже. Вьюшка postcomment помимо этого делает небольшой хак, чтобы отловить валидацию формы и постинг (используется почти полностью родной код). Я накомментил очень богато, всё должно быть понятно.<br />
<br />
views.py<br />
<pre class="brush:python">from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import render
# Здесь мы намеренно не получаем напрямую список для рендера, т.к. хотит больше отдать
# стандартному рендеру и стандартным шаблонам контриб-аппа 'comments'
@login_required
def listcomments(request, eid, model):
# получаем объект сущности
try:
ieid = int(eid)
# получаем сущность
ct = ContentType.objects.get_for_model(model)
entity = ct.get_object_for_this_type(pk=ieid)
except (ValueError, KeyError):
return HttpResponseBadRequest('error params')
except ObjectDoesNotExist:
return HttpResponseNotFound('entity %s id=%i not found' % (model.__name__, ieid))
return render(request, 'commentsajax/list.html', {"entity": entity, })
@login_required
def postcomment(request, eid, model):
# получаем объект сущности
try:
ieid = int(eid)
# получаем сущность
ct = ContentType.objects.get_for_model(model)
entity = ct.get_object_for_this_type(pk=ieid)
except (ValueError, KeyError):
return HttpResponseBadRequest('error params')
except ObjectDoesNotExist:
return HttpResponseNotFound('entity %s id=%i not found' % (model.__name__, ieid))
# работа непосредственно
if request.method == 'POST':
# надо проверить юзера и мыло, чтобы форма свалидировалась
# у нас не используется имя и мыло и не заполняется в форме так что считаем что пришли пустые.
# но юзер проверится и поставится в оригинальном методе из полного имени или юзернема, а мыло может остаться пустым
# юзер у нас всегда залогинен по определению.
if not request.user.email:
request.POST = request.POST.copy()
request.POST["email"] = "none@none.none"
# передаём управление оригинальному коду
from django.contrib.comments.views.comments import post_comment
response = post_comment(request)
# проверяем вернутый статус
if response.status_code == 302:
# если редирект, то было норм
return HttpResponse('ok')
else:
# иначе там нам рисуется ошибка, мы переопределили comments/preview.html как раз чтобы туда оно и вывелось точь-в-точь как наша форма
return response
# запрос формы
else:
return render(request, 'commentsajax/form.html', {"entity": entity, })</pre><br />
Эти шаблоны внутри нашего приложения рендерят через стандартные теги список комментариев и форму:<br />
<br />
templates/commentsajax/form.html<br />
<pre class="brush:text">{% load comments %}
{% render_comment_form for entity %}</pre><br />
templates/commentsajax/list.html<br />
<pre class="brush:text">{% load comments %}
{% render_comment_list for entity %}</pre><br />
Эти шаблоны менять не нужно по моей задумке. Они нужны, чтобы максимизировать количество используемого кода оригинального приложения. Чтобы нарисовать свой список комментариев и свою форму надо менять стандартные шаблоны, например такие минимальные могут быть:<br />
<br />
templates/comments/form.html<br />
<pre class="brush:text">{% include "comments/preview.html" %}</pre><br />
templates/comments/list.html<br />
<pre class="brush:text">{% for comment in comment_list %}
<blockquote><p>{{ comment.comment }}</p><small>{{ comment.name }}, {{ comment.submit_date }}</small>
</blockquote>{% endfor %}</pre><br />
templates/comments/preview.html<br />
<pre class="brush:text">{% load comments %}
<form action="ajax_postcomment/" method="post">{% csrf_token %}
<textarea id="id_comment" name="comment" rows="5"></textarea>
<div style='display:none'>{{ form.honeypot }}</div>{{ form.content_type }}
{{ form.object_pk }}
{{ form.timestamp }}
{{ form.security_hash }}
</form></pre><br />
Здесь form.html повторяет preview.html, который мы перегрузили чтобы отловить ошибки от формы не меняя ничего во view, ведь туда приходит ошибки по задумке оригинального приложения, напоминаю. Кнопки в форме нет, т.к. я использовал в своих примерах бутстраповские диалоги с родной их кнопкой, но суть понятна - по нажатию на неё отправляем ажаксом эту форму. Вообще с клиентской стороны запрашивается список каментов через GET. Сама форма через GET запрашивается и через POST отдаются данные, если вернулось не "ok", то значит вернулась форма с отрендереными ошибками, которые надо заново перепоказать.<br />
<br />
Такая сложная схема в view с eid и model нужна для того, чтобы настраивать привязку комментариев на уровне url, т.к. у меня комментарии привязывались к нескольким сущностям, а в случае ajax мне лично удобнее тупо слать пост на ajax_listcomments/ и получать комментарии именно для этой сущности. Например, так у меня url настроены:<br />
<br />
<pre class="brush:python">...
# это обязательно надо при сохранении камента, для резолва 'comments-comment-done' внутри views оригинального аппа
(r'^comments/', include('django.contrib.comments.urls')),
# комментарии к проекту
url(r'^project/(?P<eid>[^/]+)/ajax_listcomments/$',
views_comment.listcomments,
{'model': Project},
name='project-ajax-listcomments',
),
url(r'^project/(?P<eid>[^/]+)/ajax_postcomment/$',
views_comment.postcomment,
{'model': Project},
name='project-ajax-postcomment',
),
# комментарии к контрагенту
url(r'^contragent/(?P<eid>[^/]+)/ajax_listcomments/$',
views_comment.listcomments,
{'model': Contragent},
name='contragent-ajax-listcomments',
),
url(r'^contragent/(?P<eid>[^/]+)/ajax_postcomment/$',
views_comment.postcomment,
{'model': Contragent},
name='contragent-ajax-postcomment',
),
...</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-1911503404158626152013-11-02T22:43:00.001+06:002013-11-02T22:52:24.296+06:00django: текущий пользователь в логахУдобно в логах иметь текущего залогиненого юзера, чтобы видно было на ком падает, удобнее разбираться. Для логгера "django.request" <a href="https://docs.djangoproject.com/en/dev/topics/logging/#django-request">передаётся</a> экстра-параметр request. Его можно вытащить и залоггировать. Например, с помощью кастомного фильтра. Фильтр добавляет в вывод форматтера поле "user" с именем (строковое представление#id) юзера если оно есть. Фильтр вешается на нужные handlers. А в соответствующие formatters на этих handlers добавляется поле "user". В фильтре проверка на существование атрибутов request и request.user обязательна, если текущий formatter повешен на что-либо, кроме логгера "django.request", иначе будет падать в попытке найти поле с ид "user".<br />
<br />
<pre class="brush:python">class RequestPushUserFilter(logging.Filter):
def filter(self, record):
if hasattr(record, 'request'):
if hasattr(record.request, 'user'):
record.user = u'%s#%s' % (record.request.user, record.request.user.pk)
else:
record.user = '?'
else:
record.user = '-'
return True
LOGGING = {
...
'filters': {
...
'request_pushuser_filter': {
'()': 'project.settings.RequestPushUserFilter',
},
},
...
'formatters': {
'standard': {
'format': "[%(asctime)s] %(levelname)s [%(name)s:%(module)s:%(lineno)s] [%(user)s] %(message)s",
'datefmt': '%d.%m.%Y %H:%M:%S',
},
},
...
'handlers': {
'logfile': {
...
'filters': ['request_pushuser_filter'],
...
'formatter': 'standard',
},
...</pre>В итоге в логах будет примерно так:<br />
<pre class="brush:text">[31.10.2013 00:43:20] ERROR [django.request:base:212] [Driver John#1] Internal Server Error: блабла</pre><br />
См. также про <a href="http://dark-barker.blogspot.ru/2013/10/django-logging-file-capture-warnings.html">логгирование в файл django</a>.Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-74258996878429168252013-10-31T23:59:00.000+06:002013-11-01T00:01:29.760+06:00django: csrf при ajax-запросахНесколько раз встречал вопросы на счёт csrf при самодельных ajax-запросах (например, через jquery как в примере ниже), и несколько вариантов странных даже видел. Я делаю проще - в основную страницу (например, в шаблон base.html или как он там у вас назван) выношу стандартный csrf_token в качестве javascript-переменной и во всех последующих скриптах удобно его использую.<br />
<pre class="brush:html"><script type="text/javascript">
...
var csrf_token = '{{ csrf_token }}';
</script></pre>Секюрность не страдает, конечно, т.к. сам токен не представляет интереса. Потом при запросах, например, через jquery.ajax можно просто передавать как параметр с именем "csrfmiddlewaretoken" (аналогично тому как он передаётся через скрытый input в обычных формах)<br />
<br />
<pre class="brush:javascript">$.ajax({
type: 'POST',
url: '...',
data : { ..., 'csrfmiddlewaretoken' : csrf_token, },
...
});</pre>Unknownnoreply@blogger.com5tag:blogger.com,1999:blog-1579503089529862816.post-53296726046755668772013-10-21T18:23:00.000+06:002013-10-21T18:30:53.192+06:00linux: русский в консоли archlinuxВозникла проблема со шрифтами в виртуальной консоли. Квадратики вместо кириллицы. В графическом эмуляторе терминала, разумеется, всё нормально, а в /dev/ttyX беда. Выяснилось, что всё портит systemd, сначала загружая шрифты и настраивая их согласно vconsole.conf как и положено, а потом подгружая drm-модуль видеокарты, который создаёт новый фреймбуфер (например, у меня <code>/dev/fb0</code>), в котором уже никаких настроек не делается.<a name='more'></a><br />
<br />
Считаю, что это косяк. Когда-нибудь это должны поправить, вероятно. Это давно было уже, до сих пор актуально, сначала долго копался, потом появилось несколько материалов в сети. Примерный механизм описан <a href="https://wiki.archlinux.org/index.php/%D0%A8%D1%80%D0%B8%D1%84%D1%82%D1%8B#.D0.A8.D1.80.D0.B8.D1.84.D1.82_.D0.B2_.D0.BA.D0.BE.D0.BD.D1.81.D0.BE.D0.BB.D0.B8">здесь</a>. Основная тема на archlinux.org.ru <a href="http://archlinux.org.ru/forum/topic/1090/">здесь</a>.<br />
<br />
Т.к. у меня грузится с локальной ФС, то я ограничился способом добавления <br />
<pre class="brush:text">MODULES="i915"</pre>(нужно подставить свой модуль: i915, nouveau, radeon итд) в <code>/etc/mkinitcpio.conf</code><br />
и перегенерацией initramfs:<br />
<pre class="brush:bash">mkinitcpio -p linux</pre><br />
Проверить в этом ли дело можно руками поставив нужный шрифт (они лежат в <code>/usr/share/kbd/consolefonts/</code>):<br />
<pre class="brush:bash">setfont cyr-sun16[.psfu.gz]</pre>Unknownnoreply@blogger.com1tag:blogger.com,1999:blog-1579503089529862816.post-59482324772299513752013-10-07T22:11:00.002+06:002013-10-07T22:13:11.587+06:00django: логгирование в файл и перехват warningsВ django иногда удобно вести файловые логи в приложении. По умолчанию настроен только один handler — mail_admins (отправка на мыло админам), что не всегда хорошо, а иногда невозможно (в принципиальном интранете, например). Вторая задача возникла — хорошо бы отлавливать туда же и все варнинги из warnings.warn, которые в том числе и django иногда вываливает.<br />
Первая задача тривиальна и описана в документации. Создаём новый handler RotatingFileHandler и добавляем в нужные логгеры (очевидно, что речь про settings.py):<br />
<pre class="brush:text">LOGGING = {
...
'handlers': {
...
'logfile': {
'level': 'WARNING',
'class': 'logging.handlers.RotatingFileHandler',
'filename': rel('log', 'logfile.log'),
'maxBytes': 1000000,
'backupCount': 666,
'formatter': 'standard',
},
...
},
'loggers': {
'django.request': {
'handlers': ['mail_admins', 'logfile'],
'level': 'WARNING',
'propagate': True,
},
...
},
}</pre>вторая задача тоже просто решается. Делается <br />
<pre class="brush:python">import logging
logging.captureWarnings(True)</pre>После этого все варнинги начинаются писаться также в логгер с именем "py.warnings", откуда их и логируем:<br />
<pre class="brush:text">...
'loggers': {
...
'py.warnings': {
'handlers': ['console', 'logfile'],
'level': 'WARNING',
'propagate': True,
},
...
},</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-9015761361092926862013-10-06T23:28:00.000+06:002013-10-06T23:28:57.809+06:00django: удобные относительные пути в settingsВ settings-файле джанги удобно придумать какой-то порядок, потому что рутинных записей всяких копится целая куча. Например, для работы с путями (которые почти все относительны) использую небольшой трюк.<br />
<pre class="brush:python">import os
# путь корня этого проекта (там где manage лежит)
PROJECT_DEPLOY_PATH = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
rel = lambda *path: os.path.join(PROJECT_DEPLOY_PATH, *path)</pre>Без лямбды функция rel может выглядеть как-то так:<br />
<pre class="brush:python">def rel(*path):
return os.path.join(PROJECT_DEPLOY_PATH, *path)</pre>И далее просто используется:<br />
<pre class="brush:python">STATIC_ROOT = rel('static')</pre>Или для путей из нескольких подпапок (/static/files/)<br />
<pre class="brush:python">STATIC_ROOT = rel('static','files')</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-43201405143578323942013-09-21T22:29:00.000+06:002013-09-21T22:30:25.827+06:00django: нормальные bootstrap3 инпуты в autocomplete_lightВ bootstrap 3 поля автозаполнения от django-модуля autocomplete_light выглядят непотребно из-за требования иметь красивым инпутам формы обязательный класс "form-control". Никаких возможностей кастомизации через autocomplete_light_registry.py и т.п. нету, т.к. класс намертво захардкожен (widget.html):<br />
<pre class="brush:html">{% block input %}
{# a text input, that is the 'autocomplete input' #}
<input type="text" class="autocomplete" name="{{ name }}-autocomplete" id="{{ widget.html_id }}_text" value="" {{ extra_attrs }} />
{% endblock %}</pre>Пришлось сделать патчик в js и всем полям с class="autocomplete" добавить ещё и класс "form-control" (используется jquery):<br />
<pre class="brush:javascript">if ($(".autocomplete").length) {
$(".autocomplete").addClass( "form-control" );
}</pre>Если используется другой вариант хардкода (типа насильное назначение вообще всем input, либо хардкодом в css), то неактуально. Я люблю чистые решения, но чище этого ничего не смог придумать.Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-89647879884032147172013-09-16T20:01:00.002+06:002013-09-16T20:01:16.583+06:00swt jface button dropdown popup menuХочу я, чтобы при нажатии на кнопку вываливалось меню. Ну типа как Start-меню в винде. Всё очень просто - по нажатию кнопки создаём и разворачиваем в месте тыкания мышкой.<br />
<pre class="brush:java">import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Menu;
...
final Button mainMenuButton = new Button(composite, SWT.PUSH);
mainMenuButton.setText("¿?");
mainMenuButton.setLayoutData( ... );
mainMenuButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
Menu dropMenu = createMainMenu( getShell() );
Point point = mainMenuButton.toDisplay(new Point(event.x, event.y));
dropMenu.setLocation( point.x, point.y+mainMenuButton.getBounds().height );
dropMenu.setVisible(true);
}
});</pre>Далее на чистом SWT как-то так:<br />
<pre class="brush:java">public static Menu createMainMenu( final Shell shell )
{
Menu dropMenu = new Menu(shell, SWT.POP_UP);
...
MenuItem item0 = new MenuItem(dropMenu, SWT.PUSH);
item0.setText(...);
item0.addListener(SWT.Selection, ...);
....
return dropMenu;
}</pre>На JFace как-то так:<br />
<pre class="brush:java">public Menu createMainMenu( final Shell shell )
{
MenuManager popManager = new MenuManager();
popManager.add(new ActionOne(shell));
...
popManager.add(new Separator());
...
Menu dropMenu = popManager.createContextMenu(shell);
return dropMenu;
}</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-63735163654644095192013-08-29T22:19:00.003+06:002013-08-29T22:19:58.617+06:00django upload_to windows 123 bad charsИспользовал свою реализацию upload_to-метода для формирования пути сохранения файлов в FileField-поле модели на основании заголовка сущности (чтобы файлики сохранялись в подпапки контрагентов). При разработке под linux всё было отлично, но в продакшене на одной windows-машине периодически валилась загрузка файлов<br />
<pre class="brush:python">File "C:\python27\lib\site-packages\django\core\files\storage.py", line 168, in _save
os.makedirs(directory)
File "C:\python27\lib\os.py", line 150, in makedirs
makedirs(head, mode)
File "C:\python27\lib\os.py", line 157, in makedirs
mkdir(name, mode)
WindowsError: [Error 123] Синтаксическая ошибка в имени файла,: u'C:\\inetpub\\wwwroot\\ZooDjangoProject\\media\\file\\\blabla "blabla"'</pre>Выяснилось, что дело в кавычках в имени файлов, который есть запретный символ в винде. Пришлось вспомнить и остальные ограничения, после чего родилось прижившееся экспресс-решение.<br />
<pre class="brush:python">def removebadchars(value):
for c in r'\/:*?"<>|':
value = value.replace(c, '')
return value
...
filename = removebadchars(filename)</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-79948937045510250062013-08-27T18:18:00.000+06:002013-08-27T18:27:25.110+06:00Версия приложения через makefile прямиком из gitТак как код я держу в той или иной VCS (последнее время чаще всего в git) я подумал, что было бы круто версию приложений брать из репозитория, а конкретно — из тегов. Для небольших утилит особенно удобно. Некоторые похожие решения из интернета натолкнули меня на такое решение. Всё довольно просто, пример для обычного gnu-makefile и исходника на Си.<a name='more'></a><br />
Makefile<br />
<pre class="brush:bash">...
ifeq ($(origin VER), command line)
VERSION := $(VER)
else
VERSION := $(shell git describe --abbrev=4 --dirty --always)
endif
CFLAGS += -DVERSION=\"$(VERSION)\"
...</pre>И, собственно, всё. Потом в исходнике используем предопределённый уже для этого исходника макрос VERSION. Чтобы слишком умные IDE не ругались и чтобы на всякий случай компилировалось и без -DVERSION, желательно защититься через ifndef:<br />
<pre class="brush:c">...
#ifndef VERSION
#define VERSION "unknown"
#endif
...
printf("%s \n", VERSION);
...</pre>Версия берётся из последнего доступного тега текущей ветки. По умолчанию git describe берёт только аннотированные теги. Если же тег не на последнем коммите в ветке, то версия будет в формате "0.1-3-g828ba", для грязных исходников версия выглядит так (см. ключ --dirty): "0.1-3-g828ba-dirty". Короче, смотреть справку по git describe. Можно задать версию из командной строки при сборке через make:<pre class="brush:bash">make VER=0.666</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-10315429394448119872013-08-26T12:16:00.000+06:002013-08-26T12:17:06.194+06:00django admin запрет редактирования моделиЗапрещение в админке django редактирования какой-либо сущности. В моём случае это оповещения об оплатах платёжной системы. В ModelAdmin есть методы <code>has_add_permission</code>, <code>has_change_permission</code>, <code>has_delete_permission</code> с очевидным предназначением. Правда, если все они вернут в каком-то случае False, то модель вообще не отобразится в списке сущностей админки и по прямой ссылке тоже не будет работать. Так что все поля вместо has_change_permission надо сделать readonly.<pre class="brush:python">class SuccessNotificationAdmin(admin.ModelAdmin):
...
readonly_fields = ('order', 'sum', )
def has_add_permission(self, request):
return False
#def has_change_permission(self, request, obj=None):
# return False
def has_delete_permission(self, request, obj=None):
return False
admin.site.register(SuccessNotification, SuccessNotificationAdmin)</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-68975622821884457072013-08-25T23:46:00.003+06:002013-08-25T23:50:07.628+06:00django favicon.ico robots.txtЕсть некоторая проблема с такого рода файлами в django. Они предполагаются находящимися в корне, а статика как правило раздаётся из «подкаталога» (например, /static/). Да, в случае favicon.ico его можно указать в meta-тегах страницы, но в некоторых случаях, например, при отдаче 500 ошибки или в любом другом случае без учёта корневого шаблона с meta-тегами, некоторые браузеры всё равно запрашивают /favicon.ico из корня, напрягая логи WARN-ошибками 404. Потому удобно в urls.py перенаправлять редиректом на реальные расположения файлов.<br />
<pre class="brush: python">url(r'^favicon\.ico$', RedirectView.as_view(url=settings.STATIC_URL + 'images/favicon.ico')),
url(r'^robots\.txt$', RedirectView.as_view(url=settings.STATIC_URL + 'robots.txt')),</pre>В случае robots.txt можно отдавать как шаблон, если удобнее (можно и mime задать, начиная с 1.5, емнип)<br />
<pre class="brush: python">url(r'^robots\.txt$', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')),</pre>Для старых версий django это будут процедурные вьюшки redirect_to и direct_to_template соответственно, приводить код тут не буду за бессмысленностью.Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-18282870451991702982013-07-12T01:26:00.001+06:002013-07-12T01:28:23.641+06:00linux eclipse crash libsoupEclipse крашится вместе с JRE (причём любой, опен или сановской) на новом libsoup в момент попытки вывода всплывающего окошка почти в любом редакторе.<br />
<pre class="brush:bash">C [libsoup-2.4.so.1+0x6d9b1] soup_session_feature_detach+0x11</pre>Пришлось порыться и перепробовать несколько вариантов, но нагуглил решение. Помогает добавление<br />
<pre class="brush:bash">-Dorg.eclipse.swt.browser.DefaultType=mozilla</pre>в eclipse.ini или параметры запуска.<br />
<br />
з.ы. баг на багтрекере: <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=405786">https://bugs.eclipse.org/bugs/show_bug.cgi?id=405786</a>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-63415376388231572252013-07-11T16:25:00.000+06:002013-09-06T02:52:45.910+06:00svn-проект в git-проект (с googlecode на github)Просто памятка. Переносил несколько проектов с разных svn-репозиториев (googlecode, например). Делается просто, да и материал в сети есть на этот счёт. Выкачиваем репозиторий и превращаем его в git-svn репозиторий:<br />
<pre class="brush:bash">git svn clone https://projectsvn.googlecode.com/svn/trunk --authors-file=/home/user/authors --no-metadata</pre>Файл authors нужен, чтобы сопоставить svn-юзеров с git-юзерами в новом репозитории, у меня он был такой:<br />
<pre class="brush:text">user@gmail.com = darkbarker <user@gmail.com>
(no author) = darkbarker <user@gmail.com></pre>"(no author)" нужен для коммитов без автора (например, инициальный коммит svn-сервера).<br />
Дальше можно отфильтровать дерево коммитов, исключив пустые коммиты, которые при конвертации образуются по разным причинам, т.к. не всем изменениям в svn-коммитах соответствуют изменения в git-коммитах (изменения в директориях, например).<br />
<pre class="brush:bash">git filter-branch --commit-filter 'git_commit_non_empty_tree "$@"' HEAD</pre>Это отпочкует ветку от низа дерева и перенесёт туда master, оставив старые коммиты в старой ветке git-svn. Ну а дальше как обычно, заливаем в новый существующий git-репозиторий (надо создать его, конечно).<br />
<pre class="brush:bash">git remote add origin git@github.com:user/projectgit.git
git push origin master</pre>Для bitbucket то же самое потребуется сделать:<br />
<pre class="brush:bash">git remote add origin https://user@bitbucket.org/user/projectgit.git
git push origin master</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-26699760381426111342013-07-09T00:15:00.002+06:002013-07-09T00:54:49.789+06:00midnight commander user menu hardlinkОчень бесит создание хардлинков в midnight commander -- запрашивает пустым окошком имя линка и кладёт его в текущую папку (актуальная версия 4.8.7). Так как я довольно часто использую хардлинки для общих файлов разных проектов, написал мелкий скриптик на user menu. Редактируется через "редактировать файл маню -> пользовательский".<br />
<pre class="brush:bash">+ t t
H Hardlink to other panel
set %t
while [ -n "$1" ]; do
ln "$1" "%D"
shift
done</pre>Теперь выделяем файлы, нажимаем F2, "Hardlink to other panel" и линки с теми же именами оказываются на противоположной панели. Круто.<br />
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-83004871166240332662013-06-27T22:37:00.000+06:002013-06-27T22:42:23.156+06:00django: что-то типа select_related для m2mЗаметка о проблемах со множественными выборками из БД при обходе списка сущностей в случае many-to-many связей.<br />
<h4>Проблема</h4>У нас есть такая схема:<br />
<pre class="brush:python"># категория афиши
class CategoryPlaybill(models.Model):
...
# афиша
class Playbill(models.Model):
categories = models.ManyToManyField(CategoryPlaybill, ...)
...
# проведение афиши
class ConductingPlaybill(models.Model):
playbill = models.ForeignKey(Playbill, ...)
...</pre>Итак, есть типы событий. Есть события по многие-ко-многим (событие может входить в несколько категорий). А есть их так называемые проведения — событие с конкретной датой, временем, местом. Мне надо получить проведения событий с разбивкой по категориям: категория1=>(список проведений), категория2 =>(список проведений).<br />
<br />
Приходит в голову решение в лоб:<br />
<pre class="brush:python">cond_playbill_list = ConductingPlaybill.objects.select_related().all()
cond_playbill_map = {}
for cond_playbill in cond_playbill_list:
cats = cond_playbill.playbill.categories.all()
for cat in cats:
if cat not in cond_playbill_map:
cond_playbill_map[cat] = []
cond_playbill_map[cat].append(cond_playbill)</pre>Но это решение ожидаемо даёт кучу запросов в БД на предмет <code>SELECT FROM categoryplaybill WHERE playbill_categories.playbill_id = N</code> для каждой итерации внешнего цикла. Дело в том, что select_related не тянет playbill_categories и ниже. <a name='more'></a><br />
<h4>Мысли</h4>Далее думаем: чтобы сделать одним запросом, надо в каждой строке resulset иметь 'playbill__categories__*', но при условии m2m получится дубли по числу связей playbill<->categories, очевидно. django orm такое не умеет, походу.<br />
<br />
На raw-sql я бы написал что-то типа такого:<br />
<pre class="brush:sql">SELECT
clazz_conductingplaybill.id as 'conducting-id', clazz_conductingplaybill.date_from as 'conducting-date_from',
clazz_playbill.id as 'playbill-id', clazz_playbill.title as 'playbill-title',
clazz_playbill_categories.id as 'playbill-categories-id',
clazz_categoryplaybill.title as 'categoryplaybill-title'
FROM clazz_conductingplaybill, clazz_playbill_categories
JOIN clazz_playbill ON clazz_playbill.id=clazz_conductingplaybill.playbill_id
LEFT JOIN clazz_categoryplaybill ON clazz_categoryplaybill.id=clazz_playbill_categories.categoryplaybill_id
WHERE clazz_playbill_categories.playbill_id=clazz_playbill.id
ORDER BY clazz_playbill.id, clazz_conductingplaybill.id</pre>Потом прошёл и собрал просто в map. В том числе все дубли clazz_conductingplaybill для каждой категории.<br />
<h4>Решение</h4>А решение тут простое: сначала собрать все связки playbill__categories, а потом обойти ConductingPlaybill, не вытягивая cond_playbill.playbill.categories и всё что дальше. Заодно используем defaultdict, крайне удобный и эффективный в нашем случае.<br />
<pre class="brush:sql">cond_playbill_map = defaultdict(list)
# связи выбираем категория-событие
byplaybill = defaultdict(list)
for pr in Playbill.categories.through.objects.select_related().all():
byplaybill[pr.playbill.id].append(pr.categoryplaybill)
# выбираем проведения
cond_playbill_list = ConductingPlaybill.objects.select_related().all()
for cond_playbill in cond_playbill_list:
cats = byplaybill[cond_playbill.playbill.id]
for cat in cats:
cond_playbill_map[cat].append(cond_playbill)</pre>Если Playbill.categories.through - явно указанная модель, то можно использовать её. Не забудьте принять меры <a href="http://dark-barker.blogspot.ru/2013/06/django-template-defaultdict.html">перед использованием defaultdict в django шаблонах</a> :)Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-1579503089529862816.post-64671623123687302052013-06-27T00:05:00.002+06:002013-06-27T00:10:25.297+06:00django: шаблонизатор не отображает defaultdict<h4>Проблема</h4>Шаблонизатор Django некорректно отображает словарь, созданный через <a href="http://docs.python.org/2/library/collections.html#collections.defaultdict">defaultdict</a>. На этот счёт есть тикет <a href="https://code.djangoproject.com/ticket/16335">#16335</a>.<br />
<a name='more'></a><h4>Почему</h4>В конструкции<br />
<pre class="brush:python">{% for key, value in defdict.items %}</pre>django сначала пытается найти <code>defdict['items']</code>, на что ему defaultdict возвращает пустой список.<br />
<h4>Решение</h4>Превращать во вьюшке defaultdict в dict:<br />
<pre class="brush:python">dict(defdict)</pre>Либо попортить default_factory ему (после окончательного заполнения, разумеется), что выглядит более оптимальным способом:<br />
<pre class="brush:python">defdict.default_factory = None</pre>Unknownnoreply@blogger.com0