Предисловие
Для написания данной статьи был изучен очень большой пласт материала, разбросанного по всему Интернету, по форумам, чатам, сайтам-блогам, stackoverflow. Я собрал все воедино, так как это пригодится и мне и очень надеюсь, что другие разработчики на Django, также, останутся довольны данным материалом. Если есть что добавить (улучшить) или поправить, пожалуйста, пишите в комментариях или в Диалоги ( личные сообщения ) Хабр.
Тестирование handler 404
Если мы попытаемся тестировать ошибку 404 при заданном debug = True, то будет получать стандартный для Django отчет об ошибке с указанием о причине, но используя следующий метод вы сможете проверить работоспособность отработки 404 ошибки без лишних забот. На работающем сайте настоятельно рекомендую использовать nginx.
-
Открываем для редактирования файл settings.py, находящийся в каталоге проекта и устанавливаем значение debug = False
-
В том же каталоге открываем для редактирования файл urls.py и добавляем следующие строки:
from django.urls import re_pathfrom 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_URL}),
При переключении 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_factoryfrom django.http import HttpResponseRedirectfrom .forms import ExperienceFormdef 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"
данные текущей формы сохраняются в базы
и снизу добавляется еще один, но пустой экземпляр формы:
{% 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 pisadef 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-файле мы видим вместо черных квадратиков на месте русских букв нормальные кириллические символы: