Предисловие

Для написания данной статьи был изучен очень большой пласт материала, разбросанного по всему Интернету, по форумам, чатам, сайтам-блогам, stackoverflow. Я собрал все воедино, так как это пригодится и мне и очень надеюсь, что другие разработчики на Django, также, останутся довольны данным материалом. Если есть что добавить (улучшить) или поправить, пожалуйста, пишите в комментариях или в Диалоги ( личные сообщения ) Хабр.

Тестирование handler 404

Если мы попытаемся тестировать ошибку 404 при заданном debug = True, то будет получать стандартный для Django отчет об ошибке с указанием о причине, но используя следующий метод вы сможете проверить работоспособность отработки 404 ошибки без лишних забот. На работающем сайте настоятельно рекомендую использовать nginx.

  1. Открываем для редактирования файл settings.py, находящийся в каталоге проекта и устанавливаем значение debug = False

  2. В том же каталоге открываем для редактирования файл 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-файле мы видим вместо черных квадратиков на месте русских букв нормальные кириллические символы: