Tag: django

Add Cancel and Delete buttons to django-crispy-forms

I've been using django-crispy-forms with Bootstrap 4 on a little Django project which has saved me a ton of manual Bootstrap formatting effort. One tiny issue I came across was crispy forms has nice Submit and Button objects to add buttons to forms. However I wanted a Cancel and Delete button on my UpdateView but using the Button object will cause the form to POST which isn't what I want.

The simplest solution seems to be directly using crispy's HTML object, described aptly in the docs:

HTML: A very powerful layout object. Use it to render pure html code. In fact it behaves as a Django template and it has access to the whole context of the page where the form is being rendered:

HTML("{% if success %} <p>Operation was successful</p> {% endif %}")

Access to the whole template context? Yes please! Specifically for the delete button we need to pass a parameter, the object.id of the current object, to the delete route. Additionally the delete button is wrapped in an {% if object %} tag to only display if there is an existing form to delete. For example if you reuse the form template for your CreateView then accessing object will throw an error, it doesn't exist yet!

myapp/forms.py

class TicketForm(forms.ModelForm):
helper = FormHelper()
helper.layout = Layout(
    Fieldset(
        # ... all your layout stuff
    ),
    FormActions(
        Submit('submit', 'Save', css_class="btn btn-outline-success"),
        HTML("""<a href="{% url "ticket-list" %}" class="btn btn-secondary">Cancel</a>"""),
        HTML("""{% if object %}
                <a href="{% url "ticket-delete" object.id %}"
                class="btn btn-outline-danger pull-right">
                Delete <i class="fa fa-trash-o" aria-hidden="true"></i></button></a>
                {% endif %}"""),
    )
)

myapp/urls.py

urlpatterns = [
    url(r'^ticket/$', views.TicketListView.as_view(), name='ticket-list'),
    url(r'^ticket/new/$', views.TicketCreateView.as_view(), name='ticket-new'),
    url(r'^ticket/(?P<pk>[0-9]+)/$', views.TicketUpdateView.as_view(), name='ticket-edit'),
    url(r'^ticket/(?P<pk>[0-9]+)/delete/$', views.TicketDeleteView.as_view(), name='ticket-delete'),
]

myapp/views.py

class TicketCreateView(CreateView):
    model = Ticket
    form_class = TicketForm
    # ...

class TicketUpdateView(UpdateView):
    model = Ticket
    form_class = TicketForm
    # ...

class TicketDeleteView(DeleteView):
    model = Ticket
    form_class = TicketForm
    # ...

templates/ticket/ticket_form.html

{% extends 'page_setup.html' %}

{% load crispy_forms_tags %}

{% block content %}
  <div class="row">
    <div class="col">
      {% crispy form %}
    </div>
  </div>
{% endblock %}

Rotating logs with multiple workers in Django

The default Django logging settings make use of FileHandler which writes to a single file that grows indefinitely, or at least until your server vomits. You'll probably first reach for RotatingFileHandler or even better TimedRotatingFileHandler to solve your problem, but alas you're heading down a blind alley.

The problem, as myriad Stack Overflow questions will tell you, is if you are serving your app with something like gunicorn or uwsgi you're probably using multiple workers, which means multiple processes simultaneously trying to write and rotate logs. This leads to unexpected results such as; multiple log files changing at once, log files containing the wrong timestamped data, truncated logs and missing data. Ouch.

Since Django/Python can't be relied on to rotate logs in this scenario we turn to the trusty sysadmin's tonic: logrotate. However logrotate has a couple pitfalls of its own, such as using the copytruncate directive which can also lead to data loss! So to avoid using that directive we'll settle on Python's WatchedFileHandler, which detects file changes on disk and can continue logging appropriately, whereas FileHandler would either continue writing to the old log file or just stop writing logs entirely.

In the end your settings/base.py logging setup should look something like this:

LOGGING = {
  'version': 1,
  'disable_existing_loggers': False,
  'formatters': {
      'verbose': {
          'format': "[%(asctime)s] %(levelname)s [line:%(lineno)s] %(message)s",
          'datefmt': "%d/%b/%Y %H:%M:%S"
      },
      'simple': {
          'format': '%(levelname)s %(message)s'
      },
  },
  'handlers': {
      'file': {
          'level': 'DEBUG',
          'class': 'logging.handlers.WatchedFileHandler',
          'filename': '/var/log/myapp/myapp.log',
          'formatter': 'verbose'
      },
  },
  'loggers': {
      'django': {
          'handlers': ['file'],
          'propagate': True,
          'level': 'DEBUG',
      },
  }
}

Then I created a basic logrotate config but doing a dry-run test reported this error:

$ logrotate -d /etc/logrotate.d/myapp

error: skipping "/var/log/myapp/myapp.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.

Turns out that error is because /var/log/myapp is owned by the gunicorn user (which serves the django app, and thus writes the logs, and thus owns the directory). The su directive lets you set the owner and group logrotate should run as to solve that problem.

I also used the dateyesterday directive to backdate the rotated log files. Otherwise since anacron runs at 3am (the default on RHEL/CentOS) the filename wouldn't match the timestamps inside.

My final logrotate config looks like:

/var/log/myapp/myapp.log {
  daily
  rotate 30
  create
  dateext
  dateyesterday
  compress
  delaycompress
  notifempty
  missingok
  su gunicorn web
}

If you're really set on letting Python handle log rotation you can look into the ConcurrentLogHandler package; however it only rotates based on size, not date.


Install mysqlclient for Django 1.10 on macOS

I was trying to get a fresh Django 1.10 project setup on macOS Sierra using MySQL and Python 3.5 in a venv but pip install mysqlclient was failing with the error:

ld: library not found for -lssl

clang: error: linker command failed with exit code 1 (use -v to see invocation)

error: command 'clang' failed with exit status 1

As is often the case after some searching I came into the solution on Stack Overflow. mysqlclient needs to link against the homebrew version of openssl I have installed:

env LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib" pip install mysqlclient

The solution post also mentions that installing the xcode command line tools via:

xcode-select --install

will provide the required ssl libs for the system installed version of python (2.7 on Sierra), it will not work for python in a virtual environment.

© Justin Montgomery. Built using Pelican. Theme is subtle by Carey Metcalfe. Based on svbhack by Giulio Fidente.