Testing Django When Using @cached_property

I use the @cached_property decorator quite a bit. It’s pretty straightforward. Usually, it’s set it and forget it. As noted in the docs, it persists as long as the instance persists.

However, it can cause problems during testing if you want to change it’s value. Here is how to change the value (from SO):

class SomeClass(object):

    @cached_property
    def expensive_property(self):
         return datetime.now()

obj = SomeClass()
print obj.expensive_property
print obj.expensive_property # outputs the same value as before
del obj.expensive_property
print obj.expensive_property # outputs new value
Advertisements

Django, Ajax and HTML Select

This happens all the time; one field in a form determines the options in another select field. One way to handle this is to do a Ajax post. It’s not difficult, but there are lots of parts to remember. Here is one way to do it (using jquery).

The Django Ajax handler:

from django.http import JsonResponse

def get_options(request):
    project_id = int(request.POST['project_id'])
    project = models.Project.objects.get(id=project_id)

    choices = get_select_choices(project)
    options = []
    for choice_id, choice_label in choices:
        option = '<option value="{}">{}</option>'.format(str(choice_id), choice_label)
        options.append(option)
    return JsonResponse({'options': options})

The javascript

<script type="text/javascript">
    var project_field, phase_select;

    var get_phases_for_project = function(){
        var project = project_field.val();

        if (project === ""){
            phase_select.empty();
            phase_select.append($("<option></option>").attr("value",'').text('--------------'));
        }
        
        $.post(
            "{% url 'my_url' %}",
            {project_id: project, csrfmiddlewaretoken: '{{ csrf_token }}'},
            /**
             * @param data
             * @param data.phases
             */
            function(data){
                phase_select.empty();
                $.each(data.phases, function (index2, value) {
                        phase_select.append(value);
                });
            }
        )
    };

    $(document).ready(function(){
        project_field = $('#id_project');
        project_field.change(get_phases_for_project);
        phase_select = $("#id_phase");
    });
</script>

SSL, Django Development Server and Chrome

I am developing a Django site that uses Stripe. Even for testing, Stripe requires HTTPS. In the past, I used django-sslserver version 0.19 and ignored the complaining Chrome made about the certificate being self signed. Today (Sept 2017), none of that worked.

First thing I did was upgrade django-sslserver to 0.20. This crashed with an error related to:

ssl.PROTOCOL_TLSv1_2

It turns out ssl is built into Python and that constant is not defined in version 2.7.6. Reverting back to django-sslserver to 0.19 solved that problem.

Next, Chrome/Stripe will no longer let you ignore the SSL certificate warnings. This blog post by Alexander Zeitler does a pretty good job explaining how solve this problem. If you run into this problem:

error on line -1 of /dev/fd/11
140736435860488:error:02001009:system library:fopen:Bad file descriptor:bss_file.c:175:fopen('/dev/fd/11','rb')
140736435860488:error:2006D002:BIO routines:BIO_new_file:system lib:bss_file.c:184:
140736435860488:error:0E078002:configuration file routines:DEF_LOAD:system lib:conf_def.c:197:

remove sudo from createselfsignedcertificate.sh and run the script using sudo.

When all of that is done, you need to tell Chrome to trust your Certificate Authority by going to “Advanced Settings -> Manage Certificates”, then “Authorities/Import. Select the rootCA.pem in the ssl directory created by the scripts above.

This probably already setup on your machine, but you need to check the file /etc/hosts to make sure localhost points to the IP address django-sslserver is using (most likely 127.0.0.1). Then in the browser go to:

https://localhost:8000/

Launch django-sslserver using something like:

python manage.py runsslserver --certificate ~/ssl/server.crt --key ~/ssl/server.key

 

Daemonizing Django-RQ using Supervisor

I was trying to set up a task to be run from Django-RQ. The task involved scrapping a webpage using Selenium and Google Chrome. It worked great in development, but not in production. The error message indicated that there were problems starting Chrome.

One big difference between dev and production was in production I was daemonizing Django-RQ using Supervisor. Some queued tasks would run. Just not the ones involving Selenium. The clue came when I stopped Django-RQ using supervisorctl and then started it from the command line. Now the Selenium tasks worked.

I solved the problem by adding this snippet to the top of the task that used Selenium:


import os
import json

json.dump(os.environ['PATH'].split(':'), open('debug_file.json', 'wb'))

This revealed that the environment PATH when running from the command line was much different that that when running Django-RQ from Supervisor. Adding some of those paths to the Supervisor config solved the problem.

[program:django_rq]
command= {{ virtualenv_path }}/bin/python manage.py rqworker high default low
stdout_logfile = /var/log/redis/redis_6379.log

numprocs=1

directory={{ django_manage_path }}
environment = DJANGO_SETTINGS_MODULE="{{ django_settings_import }}",PATH="{{ virtualenv_path }}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
user = vagrant
stopsignal=TERM

autostart=true
autorestart=true

Django HTTP 404 Not Found Error

So you’ve got everything working on the Django dev server. Almost everything is working on the staging server… except one URL that sometimes fails with an HTTP 404 Not Found error. What is going on here?

More details; it happens to be a Django form page. The page loads with a problem. The problem occurs after you submit the form. The URL that causes the 404 error is the URL of the form. What is going on here?

In my case, the form_valid() was doing a lot of processing of the form data and the server was timing out. Maybe part of the reason I did not catch this earlier is my dev server is much faster than the Digital Ocean droplet I was using.

Django Long Running Processes

The most commonly suggested solution for long running processes is to use Celery. I suspect that if you need scalabilty or high volume, etc… Celery is the best solution. That said,  I have been down the Celery rabbit hole more than once. It has never been pleasant. Since my needs are more modest, maybe there is a better alternative?

My needs involve running process that might run for 15 minutes or so. The process might run a dozen times/day and be launched by as many users. The process must be launch-able from the website by authorized users.

I have solved this problem by going to the polar opposite of Celery – cron. Every minute cron would launch a custom django command. That command would look in a database table for tasks and get input data from the database. When a task was completed, that fact was written to the database table. Honestly, this approach has worked well. Never-the-less, I always wonder if there is a stable, simple, robust solution that lies somewhere between cron and Celery.

Maybe RedisRQ and Django RQ? These are my notes so that a year from now, when this issue comes up again, I can get up to speed quickly.

Step 1: Install Redis and Start Redis Server

These instructions are pretty good.

Step 2: Is Redis Server Working?

Maybe you installed Redis Server a long time ago and you want to see if it’s still working? Go here.

Or you could type:

$ redis-cli ping
PONG

Step 3: Install RQ

pip install rq

Step 4: Install and Configure django-rq

Go here.

Step 5: Read the RQ Docs

Seriously – read the RQ docs. They are brief and to-the-point.

Step 6: Daemonize the RQ Workers

If you use supervisord , here is the Ansible template I use to do that:

[program:django_rq]
command= {{ virtualenv_path }}/bin/python manage.py rqworker high default low
stdout_logfile = /var/log/redis/redis_6379.log

numprocs=1

directory={{ django_manage_path }}
environment = DJANGO_SETTINGS_MODULE="{{ django_settings_import }}",PATH="{{ virtualenv_path }}/bin"
user = vagrant
stopsignal=TERM

autostart=true
autorestart=true


[program:rqscheduler]
command={{ virtualenv_path }}/bin/python manage.py rqscheduler
stdout_logfile = /var/log/redis/rq_scheduler.log

numprocs=1

directory={{ django_manage_path }}
environment = DJANGO_SETTINGS_MODULE="{{ django_settings_import }}",PATH="{{ virtualenv_path }}/bin"
user = vagrant
stopsignal=TERM

autostart=true
autorestart=true

Reflections

I do not recall all the problems I had with Celery. After reviewing the RQ solution above, it is clear that one of the advantages of that solution is the documentation is really good. Or at least it clearly and directly addressed what I was trying to do.

Additionally, I wish I would have implemented this a long time ago. It is so easy to use. And it’s so freeing to to be able to run long processes.

Reflections part Deux, Troubleshooting and Gotchas

It’s coming back to me. The supervisor config in the original post started the daemon OK. But it turns out there was an error in the config that caused the queued processes to fail. Finding and fixing that bug was a pain in the ass. Maybe my troubles with Celery were really troubles with supervisor? Down the rabbit hole we go!

It turns out that the Django RQ Queue Statistics are helpful for debugging. They show failed tasks along with a Python traceback! Very nice. In my case, I was getting the error:

ImportError: No module named XXXX

Clearly one of my paths in the supervisor conf file was wrong. Time to start hacking:

  1. Edit conf file
  2. Run supervisorctl stop django_rq
  3. Run supervisorctl start django_rq
  4. Queue a task
  5. It failed again? How is that possible? Back to step 1

GOTCHA! After a while you notice the changes you are making are not having any effect. And then you recall that to reload the config file you must run:

service supervisor restart

Now my config file works. All I have to do is figure out which of the ever cludgier hacks I made can be removed. The config file above has been updated.

Son of Reflections part Deux – Adding PATH to Supervisor Config

I thought I had it working. Then when I added a slightly more complex task that interacted with the database, it failed with an ImportError. After flailing around for a while, I found that adding a PATH to the supervisor environment variable solved the problem.

During my flailing, I found this blog post. Lots of great ideas.

Still Falling Down the Rabbit Hole – Logging to the Rescue

Everything was working almost every where… except with the daemonized workers on the server. Luckily, Django-RQ now comes with logging. I implemented the logging in the Django settings files as per the docs, restarted the dev server, and… no logging. Turns out you have to restart the workers.

Also, although the docs show the use of a logging class made for rq (rq.utils.ColorizingStreamHandler), it turns out you can use logging.FileHandler, which is what you want for debugging the code when running from a daemonized worker.

For what it’s worth, it turns out the problem was with the python locale module. The docs say something about it not being thread safe. The function locale.getlocale() returned a value when the workers were run via the dev server, but it returned None when run from a daemonized worker.