Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
*/.#*
*egg-info
*.pyc
*~
example/db.sqlite
build/*
dist/*
example/db.sqlite
24 changes: 12 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ to run Django management commands from the admin.
Dependencies
============

- django-async
- django-sneak
- django-async (https://pypi.python.org/pypi/django-async)
- django-sneak (https://github.com/liberation/django-sneak)

Settings
========


You need to activate the Django admin in the settings and ``urls.py``
You need to activate the Django admin in the settings and ``urls.py``
depending on your needs the configuration may vary, refer
to the Django documentation related to the
to the Django documentation related to the
`admin application <https://docs.djangoproject.com/en/dev/ref/contrib/admin/>`_.

Don't forget to add the application where you defined management
Expand Down Expand Up @@ -46,8 +46,8 @@ Then you will have to create a configuration class for the command::

# ./music/admincommands.py

from admincommands.models import AdminCommand

from django import forms
from admincommand.models import AdminCommand

class Lyrics(AdminCommand):

Expand All @@ -57,17 +57,17 @@ Then you will have to create a configuration class for the command::
def get_command_arguments(self, forms_data):
return [forms_data['title']], {}

And all is well, the new admin command will be available under the
If all is well, the new admin command will be available under the
«Admin Command» area of the administration of the default admin site.

If you use custom admin site, don't forget to register
If you use a customized admin site (e.g. no autodiscover), then don't forget to register
``admincommand.models.AdminCommand`` to the admin site object.

Asynchronous tasks
==================

If you want to execute commands asynchronously you have to
specify it in the AdminCommand configuration class with the
If you want to execute commands asynchronously you have to
specify it in the AdminCommand configuration class with the
``asynchronous`` property set to ``True``::

# ./music/admincommands.py
Expand All @@ -91,7 +91,7 @@ You also need to run periodically ``flush_queue`` from ``django-async`` applicat
Permissions
===========

You MUST add to every user or groups that should have access to the list of commands
«Can change admincommand» permission. Every admin command gets it's own permission
You MUST add to every user or groups that should have access to the list of commands
«Can change admincommand» permission. Every admin command gets it's own permission
«Can Run AnAdminCommand», so you can add it to proper users or group. Users will
only see and be able to execute admin commands for which they have the permission.
13 changes: 9 additions & 4 deletions admincommand/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
from django.contrib.admin.options import csrf_protect_m
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from django.conf.urls.defaults import url
try:
from django.conf.urls.defaults import url, patterns
except ImportError:
from django.conf.urls import url, patterns
from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest
from django.conf.urls.defaults import patterns
from django.utils.encoding import force_unicode
from django.utils.functional import update_wrapper
try:
from django.utils.functional import update_wrapper
except:
from functools import update_wrapper
from django.http import HttpResponseForbidden
from django.utils.safestring import mark_safe
from django.contrib import messages
Expand All @@ -27,7 +32,7 @@ class AdminCommandAdmin(SneakAdmin):

def queryset(self, request):
# user current user to construct the queryset
# so that only commands the user can execute
# so that only commands the user can execute
# will be visible
return CommandQuerySet(request.user)

Expand Down
13 changes: 13 additions & 0 deletions admincommand/async/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
Django Async implementation.
"""


def schedule(*a, **kw): # pragma: no cover
"""Wrapper for async.schedule.schedule that allow coverage.
"""
# Redefining name 'schedule' from outer scope
# pylint: disable=W0621
from async.api import schedule
return schedule(*a, **kw)

48 changes: 48 additions & 0 deletions admincommand/async/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Django admin.
"""
from django.contrib import admin
from async.models import Error, Job, Group


class ErrorInline(admin.TabularInline):
"""Display the errors as part of the Job screen.
"""
model = Error
fields = ['traceback']
readonly_fields = ['traceback']
max_num = 0


def display_group(obj):
return obj.group.reference if obj.group else None

display_group.short_description = 'Group'


class JobAdmin(admin.ModelAdmin):
"""Allow us to manipulate jobs.
"""

list_display = ['__unicode__', 'scheduled', 'executed', display_group]
inlines = [ErrorInline]


class GroupAdmin(admin.ModelAdmin):
"""Allow us to see groups.
"""

def executed_jobs(obj):
return obj.jobs.filter(executed__isnull=False).count()
executed_jobs.short_description = 'Executed'

def unexecuted_jobs(obj):
return obj.jobs.filter(executed__isnull=True).count()
unexecuted_jobs.short_description = 'Unexecuted'

list_display = ['__unicode__', 'description', executed_jobs, unexecuted_jobs]

admin.site.register(Error)
admin.site.register(Job, JobAdmin)
admin.site.register(Group, GroupAdmin)

107 changes: 107 additions & 0 deletions admincommand/async/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Schedule the execution of an async task.
"""
from datetime import timedelta
# No name 'sha1' in module 'hashlib'
# pylint: disable=E0611
from hashlib import sha1
from simplejson import dumps

from django.db.models import Q
try:
# No name 'timezone' in module 'django.utils'
# pylint: disable=E0611
from django.utils import timezone
except ImportError: # pragma: no cover
from datetime import datetime as timezone

from async.models import Error, Job, Group
from async.utils import full_name


def _get_now():
"""Get today datetime, testing purpose.
"""
return timezone.now()


def schedule(function, args=None, kwargs=None,
priority=5, run_after=None, group=None, meta=None):
"""Schedule a tast for execution.
"""
# Too many arguments
# pylint: disable=R0913
if group:
if type(group) == Group:
expected_group = group
else:
expected_group = Group.latest_group_by_reference(group)
else:
expected_group = None
job = Job(
name=full_name(function),
args=dumps(args or []), kwargs=dumps(kwargs or {}),
meta=dumps(meta or {}), scheduled=run_after,
priority=priority,
group=expected_group)
job.save()
return job


def deschedule(function, args=None, kwargs=None):
"""Remove any instances of the job from the queue.
"""
job = Job(
name=full_name(function),
args=dumps(args or []), kwargs=dumps(kwargs or {}))
mark_cancelled = Job.objects.filter(executed=None,
identity=sha1(unicode(job)).hexdigest())
mark_cancelled.update(cancelled=_get_now())


def health():
"""Return information about the health of the queue in a format that
can be turned into JSON.
"""
output = {'queue': {}, 'errors': {}}
output['queue']['all-jobs'] = Job.objects.all().count()
output['queue']['not-executed'] = Job.objects.filter(executed=None).count()
output['queue']['executed'] = Job.objects.exclude(executed=None).count()
output['errors']['number'] = Error.objects.all().count()
return output


def remove_old_jobs(remove_jobs_before_days=30, resched_hours=8):
"""Remove old jobs start from these conditions

Removal date for jobs is `remove_jobs_before_days` days earlier
than when this is executed.

It will delete jobs and groups that meet the following:
- Jobs execute before the removal date and which are not in any group.
- Groups (and their jobs) where all jobs have executed before the removal
date.
"""
start_remove_jobs_before_dt = _get_now() - timedelta(
days=remove_jobs_before_days)

# Jobs not in a group that are old enough to delete
rm_job = (Q(executed__isnull=False) &
Q(executed__lt=start_remove_jobs_before_dt)) | \
(Q(cancelled__isnull=False) &
Q(cancelled__lt=start_remove_jobs_before_dt))
Job.objects.filter(Q(group__isnull=True), rm_job).delete()

# Groups with all executed jobs -- look for groups that qualify
groups = Group.objects.filter(Q(jobs__executed__isnull=False) |
Q(jobs__cancelled__isnull=False))
for group in groups.iterator():
if group.jobs.filter(rm_job).count() == group.jobs.all().count():
group.jobs.filter(rm_job).delete()
group.delete()

next_exec = _get_now() + timedelta(hours=resched_hours)

schedule(remove_old_jobs,
args=[remove_jobs_before_days, resched_hours],
run_after=next_exec)
6 changes: 6 additions & 0 deletions admincommand/async/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Django Async logger.
"""
#pylint: disable=unused-import
import logging as _logger

3 changes: 3 additions & 0 deletions admincommand/async/management/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Django Async.
"""
3 changes: 3 additions & 0 deletions admincommand/async/management/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Django Async management commands.
"""
Loading