Предисловие
Для написания данной статьи был изучен очень большой пласт материала, разбросанного по всему Интернету, по форумам, чатам, сайтам-блогам, stackoverflow. Я собрал все воедино, так как это пригодится и мне и очень надеюсь, что другие разработчики на Django, также, останутся довольны данным материалом. Если есть что добавить (улучшить) или поправить, пожалуйста, пишите в комментариях или в Диалоги ( личные сообщения ) Хабр.
Тестирование handler 404
Если мы попытаемся тестировать ошибку 404 при заданном debug = True, то будет получать стандартный для Django отчет об ошибке с указанием о причине, но используя следующий метод вы сможете проверить работоспособность отработки 404 ошибки без лишних забот. На работающем сайте настоятельно рекомендую использовать nginx.
Открываем для редактирования файл settings.py, находящийся в каталоге проекта и устанавливаем значение debug = False
В том же каталоге открываем для редактирования файл urls.py и добавляем следующие строки:
from django.urls import re_path
from django.views.static import serve #добавляем в заголовке
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
re_path(r'^static/(?P<path>.*)$', serve, {'document_root': settings.STATIC_ROOT}),
При переключении debug в значение false, мы по умолчанию теряем статику и медиа, но используя данный метод, django продолжить обрабатывать эти данные вместо nginx, к примеру, а также, позволяет проверить отработку 404 или других ошибок в Django при работе на localhost, например при python manage.py runserver .
Формсеты и динамическое добавление форм
Для подготовки этого материала ушло достаточно много времени, сотни незакрытых вкладок в поисках полезной информации, а так множество вопросов в чатах разработчиков на Python/Django и даже появился на свет сайт для создания резюме с динамическим добавлением полей формы, где представлен и используется данный функционал.
(Демо учетная запись:
Логин: habrhabr
Пароль: pp#6JZ2\a7y=
Стояла у меня такая задача: отображать форму, а по нажатию на кнопку добавлять дополнительные экземпляры данной формы.
Для этих целей создал несколько моделей вида, где Worker - это FK для Experience:
class Worker(models.Model):
public_cv = models.BooleanField(default=False, verbose_name='Can everyone see your resume ?')
cv_name = models.CharField(max_length=250, verbose_name='CV name', blank=True)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='Author', default=0)
# +много других полей
def __str__(self):
return self.name
def publish(self):
self.published_date = timezone.now()
self.save()
class Experience(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE)
title = models.CharField(max_length=200, verbose_name='Position name')
# +много других полей
def __str__(self):
return self.title
def publish(self):
self.published_date = timezone.now()
self.save()
# +много других моделей
Следующим шагом, который приближал меня к цели - реализовать желаемое сначала в административной панели django-admin, для этого я использовал StackedInline:
class ExperienceInstance(admin.StackedInline):
model = Experience
extra = 1
@admin.register(Worker)
class PublishWorkers(admin.ModelAdmin):
inlines = [
ExperienceInstance,]
И получим желаемый вид пока что в Django-admin, создается пустая форма Experience связанная с Worker и кнопка "Добавить форму Experience":
Теперь нужно добавить во views.py код, который позволит выводить форму Experience отдельно и по нажатии кнопки создавать дополнительный экземпляр формы Experience, будем использовать Formset:
from django.forms import inlineformset_factory
from django.http import HttpResponseRedirect
from .forms import ExperienceForm
def expformview(request, worker_uid):
worker = Worker.objects.get(uid=worker_uid)
ExperienceFormset = inlineformset_factory(
Worker, Experience, form=ExperienceForm, extra=1, max_num=15, can_delete=True
)
if request.method == 'POST':
formset = ExperienceFormset(request.POST, instance=worker)
if formset.is_valid():
formset.save()
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
formset = ExperienceFormset(instance=worker)
return render(request, 'site/expform.html',
{
'formset': formset,
'worker': worker,
}
)
Также, создадим Форму в forms.py ExperienceForm:
class ExperienceForm(forms.ModelForm):
started = forms.DateField(
required=False,
label='Start date',
widget=forms.TextInput(attrs={'placeholder': 'YYYY-MM-DD'})
)
ended = forms.DateField(
required=False,
label='End date',
widget=forms.TextInput(attrs={'placeholder': 'YYYY-MM-DD'})
)
class Meta:
model = Experience
fields = ('title',
'selfedu',
)
Далее шаблон HTML. Я использую Crispy для лучшего отображения полей форм. {{formset.media}}
нужен для вывода WYSIWYG-редактора ckeditor. При нажатии на кнопку с type="submit"
данные текущей формы сохраняются в базы и снизу добавляется еще один, но пустой экземпляр формы:
Шаблон HTML
{% extends 'site/base.html' %}
{% load crispy_forms_tags %}
{% block content %}
{% if worker.author == request.user%}
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Experience | {{worker}}</title>
</head>
<body>
<center>
<div class="col-lg-5" style="margin:1em;">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">Basic information</li>
<li class="breadcrumb-item active" aria-current="page"><b>Experience</b></li>
<li class="breadcrumb-item">Education</li>
<li class="breadcrumb-item">Certification</li>
<li class="breadcrumb-item">Awards</li>
<li class="breadcrumb-item">Projects</li>
</ol>
</nav>
</div>
</center>
<h2 align="center" style="margin:1em;">{{worker}}'s Experience form</h2>
<form method="post">
{% csrf_token %}
<div class="row" style="margin:2em 0 2em 0;">
<div class="col-lg-5 mx-auto">
{{formset.media}}
{{formset|crispy}}
</div>
<div class="col-lg-12">
<center><button type="submit" class="btn btn-outline-warning">Save & Add</button>
<a href="edu"><button type="button" class="btn btn-outline-success">Next > Education</button></a></center>
</div>
</div>
</form>
</body>
</html>
{%else%}
<div class="row">
<div class="col-lg-12" style="margin-top:6em;">
<center>
<h2>You have not access to this section</h2>
</center>
</div>
</div>
{%endif%}
{% endblock %}
Так выглядит это на работающем сайте:
Экспорт данных в PDF с поддержкой кириллицы (русских букв)
Для экспорта данных, в данном случае страницы HTML в PDF мы будем использовать XHTML2PDF; для его установки необходимо в venv запустить:
pip install xhtml2pdf
Далее добавляем следующий код в views.py:
from xhtml2pdf import pisa
def render_pdf_view(request, worker_uid):
template_path = 'site/pdf.html'
worker = Worker.objects.get(uid=worker_uid)
exp = Experience.objects.filter(worker=worker)
context = {
'worker': worker,
'exp': exp,
}
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'filename="%s_%s.pdf"' % (worker.name, worker.created_date.strftime('%Y-%m-%d')) # правлю название выходного файла PDF вида: Имя_Год-М-Д
# Найти шаблон и вывести его
template = get_template(template_path)
html = template.render(context)
# Создаем PDF
pisa_status = pisa.CreatePDF(html, dest=response, )
# Вывод ошибок
if pisa_status.err:
return HttpResponse('We had some errors <pre>' + html + '</pre>')
return response
Шаблон HTML заполняем как обычный шаблон, но нужно учитывать, что парсер PDF видит только локальные стили, поэтому их нужно объявить между тегами <style></style>
в данном шаблоне.
Чтобы русские символы корректно отображались в экспортируемом PDF необходимо загрузить шрифт с поддержкой кириллических (русских) букв и положить его в static/fonts/ , при этом указать до файла-шрифта полный путь с учетом системных каталогов, например в моем случае путь выглядит так: /var/www/cvmaker/static/fonts/arial.ttf
, а между тегами <style/>
добавляем следующее:
@font-face {
font-family: 'sans-serif';
src: url("/var/www/cvmaker/static/fonts/arial.ttf");
}
body{
font-family: "sans-serif";
}
Таким образом в экспортируемом PDF-файле мы видим вместо черных квадратиков на месте русских букв нормальные кириллические символы:
mavriq
вы хоть свой код дальше localhost-а проверяете?
тут даже django знать не надо, чтоб догадаться, что
document_root
не может бытьsettings.STATIC_URL
, а должно бытьsettings.STATIC_ROOT
Кроме того — захардкожены
settings.STATIC_URL
иsettings.MEDIA_URL
в регексах.В общем — не надо так
endlessnights Автор
Да, конечно я этот код проверял и использовал в нескольких проектах для отладки 404 ошибки. Понял, поправлю.
mavriq
значит вам "повезло", что статика лежит в
/static
, и переменныеsettings.STATIC_URL
иsettings.STATIC_ROOT
— идентичны (может за исключением завершающего слеша).Иначе — django искала бы статику по неверному адресу