Merge lp:~elachuni/ubuntu-webcatalog/celery into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
Status: Merged
Approved by: Anthony Lenton
Approved revision: 157
Merged at revision: 156
Proposed branch: lp:~elachuni/ubuntu-webcatalog/celery
Merge into: lp:ubuntu-webcatalog
Diff against target: 1262 lines (+551/-136)
35 files modified
django_project/config/main.cfg (+10/-1)
django_project/settings.py (+4/-1)
setup.py (+2/-0)
src/webcatalog/admin.py (+2/-4)
src/webcatalog/decorators.py (+32/-0)
src/webcatalog/department_filters.py (+1/-4)
src/webcatalog/forms.py (+1/-4)
src/webcatalog/management/commands/cleanup.py (+24/-16)
src/webcatalog/management/commands/import_all_app_install_data.py (+3/-5)
src/webcatalog/management/commands/import_all_ratings_stats.py (+3/-5)
src/webcatalog/management/commands/import_app_install_data.py (+1/-4)
src/webcatalog/management/commands/import_sca_apps.py (+1/-4)
src/webcatalog/managers.py (+1/-5)
src/webcatalog/models/applications.py (+25/-5)
src/webcatalog/preflight.py (+1/-4)
src/webcatalog/schema.py (+11/-0)
src/webcatalog/static/css/webcatalog.css (+1/-1)
src/webcatalog/tasks.py (+54/-0)
src/webcatalog/templates/forbidden.html (+23/-0)
src/webcatalog/templates/webcatalog/task_list.html (+84/-0)
src/webcatalog/templatetags/webcatalog.py (+1/-4)
src/webcatalog/tests/__init__.py (+1/-0)
src/webcatalog/tests/factory.py (+27/-6)
src/webcatalog/tests/helpers.py (+1/-4)
src/webcatalog/tests/test_commands.py (+4/-6)
src/webcatalog/tests/test_department_filters.py (+1/-5)
src/webcatalog/tests/test_forms.py (+1/-4)
src/webcatalog/tests/test_managers.py (+1/-4)
src/webcatalog/tests/test_models.py (+1/-4)
src/webcatalog/tests/test_preflight.py (+3/-16)
src/webcatalog/tests/test_tasks.py (+42/-0)
src/webcatalog/tests/test_templatetags.py (+1/-5)
src/webcatalog/tests/test_views.py (+116/-4)
src/webcatalog/urls.py (+6/-4)
src/webcatalog/views.py (+61/-7)
To merge this branch: bzr merge lp:~elachuni/ubuntu-webcatalog/celery
Reviewer Review Type Date Requested Status
Danny Tamez (community) Approve
Review via email: mp+112398@code.launchpad.net

Commit message

Added a view to schedule asynchronous tasks via Celery.

Description of the change

Overview
========
This branch adds a view to schedule asynchronous tasks via Celery. Only one basic task is provided to import exhibits (plus one that always fails, for testing), the full set of tasks will be in a separate MP, now that the groundwork is in place. A brief video demo can be found on http://people.canonical.com/~anthony/celery-uwc.ogv

Details
=======
The current branch counts on using CELERY_RESULT_BACKEND='database', as it fetches the result for each task from the DB. Any BROKER_BACKEND can be used, I specifically tested with djkombu and amqp locally.

The TaskMeta model was registered in the admin so that results can be browsed and searched in the admin UI after a task has run. The ModelAdmin class was improved a bit after grabbing the video, you can see the final list view on http://people.canonical.com/~anthony/TaskMetaAdmin.png

The list of tasks available is put together by introspecting webcatalog.tasks, so adding new tasks available will be picked up and added automatically.

The task list template uses YUI.io to retrieve the task's status, but submits a POST form and reloads the whole page when you schedule a new task; I considered doing everything with ajax, but POST via ajax is a bit tricky, specially when csrf tokens are involved, so I went for this simpler solution that seems to be responsive enough.

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

30 -openid_launchpad_staff_teams = canonical-ca-hackers
31 +openid_launchpad_staff_teams = canonical-isd-hackers

That looks backwards?

332 + if (message.hasClass('success')) {

That looks as if it will loop forever on successful tasks?

Most of this branch looks pretty good to me.

Thanks,

James

152. By Anthony Lenton

Added a few tasks.

153. By Anthony Lenton

Merged in latest changes from trunk.

154. By Anthony Lenton

Query TaskState model instead of TaskMeta.

Revision history for this message
Anthony Lenton (elachuni) wrote :

> 30 -openid_launchpad_staff_teams = canonical-ca-hackers
> 31 +openid_launchpad_staff_teams = canonical-isd-hackers
>
> That looks backwards?

True, for some reason staging sso *still* doesn't report me as being part of canonical-ca-hackers, so I use canonical-isd-hackers instead during dev, I've put it back now, thanks!

> 332 + if (message.hasClass('success')) {
>
> That looks as if it will loop forever on successful tasks?

Hm it shouldn't, that call is outside of check_status so it's just to trigger the first call. Maybe moving this into an function called init() or onload() would make it clearer.

155. By Anthony Lenton

Pulled back config change per code review.

156. By Anthony Lenton

Merged in latest changes from trunk. Made all tests pass.

Revision history for this message
Danny Tamez (zematynnad) wrote :

line 111 - we don't need this any longer as we're now on 2.6
looks like line 115 is not needed
at 158, that looks like that could have stayed static but I guess it makes more sense to make it a a regular method as that's how the class will now be used...
same for 183

screencast was great! Is there any way to have something more useful than the task id to help find the task you want to look at? I'm not sure of the use cases for searching for a task so that id may be the only way that makes sense...

Approved - feel free to act on or ignore the comments.

review: Approve
157. By Anthony Lenton

Removed pointless imported with_statement

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'django_project/config/main.cfg'
--- django_project/config/main.cfg 2012-06-08 08:24:11 +0000
+++ django_project/config/main.cfg 2012-07-02 21:27:20 +0000
@@ -25,6 +25,7 @@
25 south25 south
26 preflight26 preflight
27 pgtools27 pgtools
28 djcelery
28login_url = /openid/login/29login_url = /openid/login/
29managers = %(admins)s30managers = %(admins)s
30middleware_classes = django.middleware.cache.UpdateCacheMiddleware31middleware_classes = django.middleware.cache.UpdateCacheMiddleware
@@ -55,12 +56,12 @@
55static_root = ./django_project/static/56static_root = ./django_project/static/
56static_url = /assets/57static_url = /assets/
57admin_media_prefix = /assets/admin/58admin_media_prefix = /assets/admin/
59test_runner = djcelery.contrib.test_runner.CeleryTestSuiteRunner
5860
59# Django-1.1 backwards compatibility61# Django-1.1 backwards compatibility
60database_engine = sqlite362database_engine = sqlite3
61database_name = webcatalog.db63database_name = webcatalog.db
6264
63
64[django_databases]65[django_databases]
65default = default_database66default = default_database
6667
@@ -103,6 +104,7 @@
103# Structure here is: LP Team = Django Group104# Structure here is: LP Team = Django Group
104canonical-isd-hackers = admin105canonical-isd-hackers = admin
105canonical-losas = admin106canonical-losas = admin
107canonical-ca-hackers = developers
106108
107[oops]109[oops]
108oops_dir = /srv/%(hostname)s/staging-logs/www-oops110oops_dir = /srv/%(hostname)s/staging-logs/www-oops
@@ -134,3 +136,10 @@
134sso_api_auth_username = insert-your-sso-api-username-here136sso_api_auth_username = insert-your-sso-api-username-here
135sso_api_auth_password = insert-your-sso-api-password-here137sso_api_auth_password = insert-your-sso-api-password-here
136sso_auth_mode_no_ubuntu_sso_plaintext_only = True138sso_auth_mode_no_ubuntu_sso_plaintext_only = True
139
140[celery]
141broker_backend = amqp
142broker_url = amqp://guest:guest@localhost:5672//
143celery_imports = webcatalog.tasks
144celery_result_backend = database
145celerybeat_scheduler = djcelery.schedulers.DatabaseScheduler
137\ No newline at end of file146\ No newline at end of file
138147
=== modified file 'django_project/settings.py'
--- django_project/settings.py 2012-03-27 20:43:10 +0000
+++ django_project/settings.py 2012-07-02 21:27:20 +0000
@@ -13,4 +13,7 @@
13 'local.cfg'])13 'local.cfg'])
14 if os.path.exists(path)]14 if os.path.exists(path)]
1515
16configglue(WebCatalogSchema, config_files, __name__)
17\ No newline at end of file16\ No newline at end of file
17configglue(WebCatalogSchema, config_files, __name__)
18
19import djcelery
20djcelery.setup_loader()
1821
=== modified file 'setup.py'
--- setup.py 2012-06-28 14:28:26 +0000
+++ setup.py 2012-07-02 21:27:20 +0000
@@ -50,6 +50,8 @@
50 'ssoclient==1.0',50 'ssoclient==1.0',
51 'pep8',51 'pep8',
52 'PIL',52 'PIL',
53 'celery',
54 'django-celery',
53 ],55 ],
54 package_data = find_packages_data('src'),56 package_data = find_packages_data('src'),
55 dependency_links = [57 dependency_links = [
5658
=== modified file 'src/webcatalog/admin.py'
--- src/webcatalog/admin.py 2012-06-28 13:29:29 +0000
+++ src/webcatalog/admin.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,8 @@
1717
18"""Admin classes for the Apps Directory."""18"""Admin classes for the Apps Directory."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,21
22 with_statement,
23)
24from django.contrib import admin22from django.contrib import admin
25from webcatalog.models import (23from webcatalog.models import (
26 Application,24 Application,
2725
=== added file 'src/webcatalog/decorators.py'
--- src/webcatalog/decorators.py 1970-01-01 00:00:00 +0000
+++ src/webcatalog/decorators.py 2012-07-02 21:27:20 +0000
@@ -0,0 +1,32 @@
1# -*- coding: utf-8 -*-
2# This file is part of the Apps Directory
3# Copyright (C) 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as
7# published by the Free Software Foundation, either version 3 of the
8# License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18"""Decorators for the webcatalog application."""
19
20from __future__ import absolute_import
21
22from django.contrib.auth.decorators import permission_required
23
24
25__metaclass__ = type
26__all__ = [
27 'scheduler_required',
28]
29
30
31scheduler_required = permission_required('webcatalog.schedule_tasks',
32 login_url='/cat/forbidden/')
033
=== modified file 'src/webcatalog/department_filters.py'
--- src/webcatalog/department_filters.py 2012-06-06 17:42:04 +0000
+++ src/webcatalog/department_filters.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Department filters."""18"""Department filters."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25import re22import re
2623
2724
=== modified file 'src/webcatalog/forms.py'
--- src/webcatalog/forms.py 2012-06-28 14:25:19 +0000
+++ src/webcatalog/forms.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Forms used by the Apps Directory."""18"""Forms used by the Apps Directory."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25import apt22import apt
26import json23import json
2724
=== modified file 'src/webcatalog/management/commands/cleanup.py'
--- src/webcatalog/management/commands/cleanup.py 2012-06-06 16:38:11 +0000
+++ src/webcatalog/management/commands/cleanup.py 2012-07-02 21:27:20 +0000
@@ -27,16 +27,17 @@
2727
28try:28try:
29 import psycopg229 import psycopg2
30 from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
31 psycopg2_available = True30 psycopg2_available = True
32except ImportError:31except ImportError:
33 psycopg2_available = False32 psycopg2_available = False
3433
3534
36class DjangoSessionCleaner(object):35class DjangoSessionCleaner(object):
37 @staticmethod36 def __init__(self, cmd):
38 def declare_expired_keys_cursor(cur):37 self.cmd = cmd
39 print "Opening cursor"38
39 def declare_expired_keys_cursor(self, cur):
40 self.cmd.output("Opening cursor\n", 1)
40 # We order by expire_date to force the index to be used.41 # We order by expire_date to force the index to be used.
41 cur.execute("CLOSE ALL")42 cur.execute("CLOSE ALL")
42 cur.execute("""43 cur.execute("""
@@ -46,8 +47,7 @@
46 ORDER BY expire_date47 ORDER BY expire_date
47 """)48 """)
4849
49 @staticmethod50 def remove_batch(self, cur, batch_size):
50 def remove_batch(cur, batch_size):
51 cur.execute("FETCH %s FROM _django_session_clean", [batch_size])51 cur.execute("FETCH %s FROM _django_session_clean", [batch_size])
52 session_keys = [session_key for session_key, in cur.fetchall()]52 session_keys = [session_key for session_key, in cur.fetchall()]
53 if not session_keys:53 if not session_keys:
@@ -66,9 +66,11 @@
6666
6767
68class OAuthNonceCleaner(object):68class OAuthNonceCleaner(object):
69 @staticmethod69 def __init__(self, cmd):
70 def declare_expired_keys_cursor(cur):70 self.cmd = cmd
71 print "Opening cursor"71
72 def declare_expired_keys_cursor(self, cur):
73 self.cmd.output("Opening cursor\n", 1)
72 # We order by created_at to force the index to be used.74 # We order by created_at to force the index to be used.
73 # Use a 5 hour clock skew as defined in openid.store.nonce75 # Use a 5 hour clock skew as defined in openid.store.nonce
74 cur.execute("CLOSE ALL")76 cur.execute("CLOSE ALL")
@@ -79,8 +81,7 @@
79 ORDER BY created_at81 ORDER BY created_at
80 """)82 """)
8183
82 @staticmethod84 def remove_batch(self, cur, batch_size):
83 def remove_batch(cur, batch_size):
84 cur.execute("FETCH %s FROM _oauth_nonce_clean", [batch_size])85 cur.execute("FETCH %s FROM _oauth_nonce_clean", [batch_size])
85 nonce_ids = [nonce_id for nonce_id, in cur.fetchall()]86 nonce_ids = [nonce_id for nonce_id, in cur.fetchall()]
86 if not nonce_ids:87 if not nonce_ids:
@@ -117,6 +118,7 @@
117 )118 )
118119
119 def handle_label(self, table, **options):120 def handle_label(self, table, **options):
121 self.verbosity = int(options['verbosity'])
120 if not psycopg2_available:122 if not psycopg2_available:
121 raise CommandError('This command requires psycopg2')123 raise CommandError('This command requires psycopg2')
122124
@@ -128,8 +130,7 @@
128 msg = ("Invalid cleaner.\nNo cleaner found for table: %s\n"130 msg = ("Invalid cleaner.\nNo cleaner found for table: %s\n"
129 "Supported tables are: %s" % (table, CLEANERS.keys()))131 "Supported tables are: %s" % (table, CLEANERS.keys()))
130 raise CommandError(msg)132 raise CommandError(msg)
131 cleaner = cleaner_cls()133 cleaner = cleaner_cls(self)
132 connection.isolation_level = ISOLATION_LEVEL_AUTOCOMMIT
133 cur = connection.cursor()134 cur = connection.cursor()
134135
135 removed = -1136 removed = -1
@@ -149,12 +150,12 @@
149 removed = cleaner.remove_batch(cur, batch_size)150 removed = cleaner.remove_batch(cur, batch_size)
150 actual_batch_time = time.time() - batch_start151 actual_batch_time = time.time() - batch_start
151 total_removed += removed152 total_removed += removed
152 print "Removed %d rows (%d total removed). Batch size %d" % (153 message = "Removed %d rows (%d total removed). Batch size %d\n"
153 removed, total_removed, batch_size)154 self.output(message % (removed, total_removed, batch_size), 1)
154155
155 # Done156 # Done
156 if removed == 0:157 if removed == 0:
157 print "All done."158 self.output("All done.\n", 1)
158 break159 break
159160
160 # Increase or decrease the batch size by 10%, minimum 1.161 # Increase or decrease the batch size by 10%, minimum 1.
@@ -163,3 +164,10 @@
163 batch_size -= batch_size_wobble164 batch_size -= batch_size_wobble
164 elif actual_batch_time < target_batch_time * 0.9:165 elif actual_batch_time < target_batch_time * 0.9:
165 batch_size += batch_size_wobble166 batch_size += batch_size_wobble
167
168 def output(self, message, level=None, flush=False):
169 if hasattr(self, 'stdout'):
170 if level is None or self.verbosity >= level:
171 self.stdout.write(message)
172 if flush:
173 self.stdout.flush()
166174
=== modified file 'src/webcatalog/management/commands/import_all_app_install_data.py'
--- src/webcatalog/management/commands/import_all_app_install_data.py 2012-06-28 13:13:15 +0000
+++ src/webcatalog/management/commands/import_all_app_install_data.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Management command to import app install data for all distroseries."""18"""Management command to import app install data for all distroseries."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25__metaclass__ = type22__metaclass__ = type
26__all__ = []23__all__ = []
@@ -37,7 +34,8 @@
37 for distroseries in settings.UBUNTU_SERIES_FOR_VERSIONS.values():34 for distroseries in settings.UBUNTU_SERIES_FOR_VERSIONS.values():
38 self.output("Importing app-install-data for {0}\n".format(35 self.output("Importing app-install-data for {0}\n".format(
39 distroseries), 1)36 distroseries), 1)
40 call_command('import_app_install_data', distroseries)37 call_command('import_app_install_data', distroseries,
38 verbosity=self.verbosity)
41 self.output("Running check_all_latest across all distroseries.\n", 1)39 self.output("Running check_all_latest across all distroseries.\n", 1)
42 call_command('check_all_latest')40 call_command('check_all_latest')
4341
4442
=== modified file 'src/webcatalog/management/commands/import_all_ratings_stats.py'
--- src/webcatalog/management/commands/import_all_ratings_stats.py 2012-06-21 20:18:44 +0000
+++ src/webcatalog/management/commands/import_all_ratings_stats.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Management command to import review statistics for all distroseries."""18"""Management command to import review statistics for all distroseries."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25__metaclass__ = type22__metaclass__ = type
26__all__ = []23__all__ = []
@@ -37,7 +34,8 @@
37 for distroseries in settings.UBUNTU_SERIES_FOR_VERSIONS.values():34 for distroseries in settings.UBUNTU_SERIES_FOR_VERSIONS.values():
38 self.output("Importing ratings stats for {0}\n".format(35 self.output("Importing ratings stats for {0}\n".format(
39 distroseries), 1)36 distroseries), 1)
40 call_command('import_ratings_stats', distroseries)37 call_command('import_ratings_stats', distroseries,
38 verbosity=self.verbosity)
4139
42 def output(self, message, level=None, flush=False):40 def output(self, message, level=None, flush=False):
43 if hasattr(self, 'stdout'):41 if hasattr(self, 'stdout'):
4442
=== modified file 'src/webcatalog/management/commands/import_app_install_data.py'
--- src/webcatalog/management/commands/import_app_install_data.py 2012-06-28 14:46:12 +0000
+++ src/webcatalog/management/commands/import_app_install_data.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Management command to import app install data for a distroseries."""18"""Management command to import app install data for a distroseries."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25import os22import os
26import re23import re
2724
=== modified file 'src/webcatalog/management/commands/import_sca_apps.py'
--- src/webcatalog/management/commands/import_sca_apps.py 2012-06-28 14:25:19 +0000
+++ src/webcatalog/management/commands/import_sca_apps.py 2012-07-02 21:27:20 +0000
@@ -19,10 +19,7 @@
19 Center.19 Center.
20"""20"""
2121
22from __future__ import (22from __future__ import absolute_import
23 absolute_import,
24 with_statement,
25)
2623
27import json24import json
28import os25import os
2926
=== modified file 'src/webcatalog/managers.py'
--- src/webcatalog/managers.py 2012-06-14 09:23:32 +0000
+++ src/webcatalog/managers.py 2012-07-02 21:27:20 +0000
@@ -17,11 +17,7 @@
1717
18"""Django object managers."""18"""Django object managers."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
24
2521
26__metaclass__ = type22__metaclass__ = type
27__all__ = [23__all__ = [
2824
=== modified file 'src/webcatalog/models/applications.py'
--- src/webcatalog/models/applications.py 2012-06-28 12:27:22 +0000
+++ src/webcatalog/models/applications.py 2012-07-02 21:27:20 +0000
@@ -17,19 +17,22 @@
1717
18"""Models for the package application."""18"""Models for the package application."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25import logging22import logging
26from datetime import datetime23from datetime import datetime
2724
28from django.conf import settings25from django.conf import settings
29from django.contrib.auth.models import User26from django.contrib.auth.models import (
27 Group,
28 Permission,
29 User,
30)
31from django.contrib.contenttypes.models import ContentType
30from django.core.urlresolvers import reverse32from django.core.urlresolvers import reverse
31from django.db import models33from django.db import models
32from django.template.defaultfilters import slugify34from django.template.defaultfilters import slugify
35from django.db.models.signals import post_syncdb
3336
34from webcatalog.department_filters import department_filters37from webcatalog.department_filters import department_filters
35from webcatalog.managers import ApplicationManager, DistroSeriesManager38from webcatalog.managers import ApplicationManager, DistroSeriesManager
@@ -290,3 +293,20 @@
290293
291 pkgs = '&'.join('pkg_name=%s' % pkg_name for pkg_name in pkg_names)294 pkgs = '&'.join('pkg_name=%s' % pkg_name for pkg_name in pkg_names)
292 return reverse('wc-package-list') + '?' + pkgs295 return reverse('wc-package-list') + '?' + pkgs
296
297
298# GroupPermissions can't go in fixtures because Permissions are stored as
299# model metadata, so our Permission's primary key can change without warning
300def post_syncdb_handler(sender, **kwargs):
301 ct = ContentType.objects.get_for_model(Application)
302 group, _ = Group.objects.get_or_create(name='developers')
303 perm, _ = Permission.objects.get_or_create(
304 codename='schedule_tasks',
305 name="Can schedule asynchronous tasks via the web",
306 content_type=ct,
307 )
308 if not perm in group.permissions.all():
309 group.permissions.add(perm)
310 group.save()
311
312post_syncdb.connect(post_syncdb_handler)
293313
=== modified file 'src/webcatalog/preflight.py'
--- src/webcatalog/preflight.py 2012-06-06 17:42:04 +0000
+++ src/webcatalog/preflight.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Preflight check for webcatalog."""18"""Preflight check for webcatalog."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25from django import db22from django import db
26from django.conf import settings23from django.conf import settings
2724
=== modified file 'src/webcatalog/schema.py'
--- src/webcatalog/schema.py 2012-06-14 23:40:45 +0000
+++ src/webcatalog/schema.py 2012-07-02 21:27:20 +0000
@@ -98,3 +98,14 @@
98 rec_service_root = schema.StringOption(98 rec_service_root = schema.StringOption(
99 default="http://rec.ubuntu.com/api/1.0")99 default="http://rec.ubuntu.com/api/1.0")
100 num_recommended_apps = schema.IntOption(default=4)100 num_recommended_apps = schema.IntOption(default=4)
101
102 class celery(schema.Section):
103 broker_backend = schema.StringOption(default='memory')
104 celery_result_backend = schema.StringOption(default='memory')
105 celery_always_eager = schema.BoolOption()
106 celery_eager_propagates_exceptions = schema.BoolOption()
107 broker_url = schema.StringOption()
108 celery_imports = schema.ListOption(item=schema.StringOption())
109 celery_ignore_result = schema.BoolOption()
110 celery_disable_rate_limits = schema.BoolOption(default=True)
111 celerybeat_scheduler = schema.StringOption()
101112
=== modified file 'src/webcatalog/static/css/webcatalog.css'
--- src/webcatalog/static/css/webcatalog.css 2012-05-29 13:28:54 +0000
+++ src/webcatalog/static/css/webcatalog.css 2012-07-02 21:27:20 +0000
@@ -361,7 +361,7 @@
361 margin-left: 7px;361 margin-left: 7px;
362}362}
363p.error {363p.error {
364 background-color: #f2cfce;364 background-color: #df382b;
365}365}
366.emaillinkportlet {366.emaillinkportlet {
367 margin-top: 64px;367 margin-top: 64px;
368368
=== added file 'src/webcatalog/tasks.py'
--- src/webcatalog/tasks.py 1970-01-01 00:00:00 +0000
+++ src/webcatalog/tasks.py 2012-07-02 21:27:20 +0000
@@ -0,0 +1,54 @@
1from time import sleep
2
3from celery.task import task
4from django.conf import settings
5from django.core.management import call_command
6
7
8@task(name="webcatalog.tasks.import_exhibits")
9def import_exhibits():
10 """Import all exhibits"""
11 call_command('import_exhibits')
12
13
14@task(name="webcatalog.tasks.import_sca")
15def import_sca():
16 """Import all data from the Software Center Agent (MyApps)"""
17 call_command('import_sca_apps')
18
19
20@task(name="webcatalog.tasks.import_app_install_data")
21def import_app_install_data():
22 """Import all data from app-install-data and apt-cache"""
23 call_command('import_all_app_install_data', verbosity=0)
24
25
26@task(name="webcatalog.tasks.import_ratings_stats")
27def import_ratings_stats():
28 """Import all ratings and reviews data"""
29 call_command('import_all_ratings_stats', verbosity=0)
30
31
32@task(name="webcatalog.tasks.check_all_latest")
33def check_all_latest():
34 """Update the 'is_latest' bit on all Applications"""
35 call_command('check_all_latest')
36
37
38@task(name="webcatalog.tasks.cleanup_sessions")
39def cleanup_sessions():
40 """Remove stale sessions from the DB"""
41 call_command('cleanup', 'django_session', verbosity=0)
42
43
44@task(name="webcatalog.tasks.cleanup_nonces")
45def cleanup_nonces():
46 """Remove stale OAuth nonces from the DB"""
47 call_command('cleanup', 'webcatalog_nonce', verbosity=0)
48
49
50@task(name="webcatalog.tasks.fail")
51def fail():
52 """Sleep for 5 seconds, then raise an error"""
53 sleep(5)
54 raise ZeroDivisionError('Intentionally failed')
055
=== added file 'src/webcatalog/templates/forbidden.html'
--- src/webcatalog/templates/forbidden.html 1970-01-01 00:00:00 +0000
+++ src/webcatalog/templates/forbidden.html 2012-07-02 21:27:20 +0000
@@ -0,0 +1,23 @@
1{% extends "webcatalog/base.html" %}
2{% load i18n %}
3
4{% block title %}
5 {% trans "Access forbidden" %}
6{% endblock %}
7
8{% block header %}
9 {% trans "Access forbidden" %}
10{% endblock %}
11
12{% block content %}
13 <p> You just tried to access a feature which you don't have permission
14 to use.</p>
15 {% if extra_message %}
16 <p>{{ extra_message }}</p>
17 {% endif %}
18 {% if next %}
19 <p> If you signed in with the wrong account you might want to try
20 <a href="{{next}}">logging in again</a>.
21 </p>
22 {% endif %}
23{% endblock %}
024
=== added file 'src/webcatalog/templates/webcatalog/task_list.html'
--- src/webcatalog/templates/webcatalog/task_list.html 1970-01-01 00:00:00 +0000
+++ src/webcatalog/templates/webcatalog/task_list.html 2012-07-02 21:27:20 +0000
@@ -0,0 +1,84 @@
1{% extends "webcatalog/base.html" %}
2{% load i18n %}
3
4{% block title %}{% trans "Ubuntu Apps Directory" %} &mdash; {% trans "Schedule tasks" %}{% endblock %}
5{% block header %}{% trans "Schedule asynchronous tasks" %}{% endblock %}
6{% block head_extra %}
7 {{ block.super }}
8<script src="{% url wc-combo %}?yui/3.4.0/build/yui/yui-min.js"></script>
9<style type="text/css">
10div.task button {
11 width: 200px;
12 height: 30px;
13}
14
15div#messages {
16 min-height: 40px;
17}
18div.traceback {
19 background: #ddd;
20 padding: 8px;
21 margin-bottom: 16px;
22}
23</style>
24<script>
25YUI({combine: true, comboBase: '{% url wc-combo %}?', root: 'yui/3.4.0/build/'}).use('io-base', 'node-base', function (Y) {
26 var check_status = function(task_id, message) {
27 var check_uri = "/cat/task_status/" + task_id + '/';
28 var UNREADY_STATES = ["PENDING", "RECEIVED", "STARTED", "RETRY"];
29
30 Y.io(check_uri, {
31 on: {
32 complete: function(id, response){
33 if (response.status == 200) {
34 var result = JSON.parse(response.responseText);
35 var status = result['status'];
36 if (UNREADY_STATES.indexOf(status) >= 0) {
37 message.append('. . .&nbsp;');
38 setTimeout(function() {check_status(task_id, message)}, 3000);
39 }
40 else {
41 if (status == 'FAILURE') {
42 message.removeClass('success');
43 message.addClass('error');
44 Y.one('#messages').append(
45 '<div class="traceback"><pre>' +
46 result['traceback'] + '</pre></div>');
47 }
48 message.append(status);
49 Y.all('button').removeAttribute('disabled');
50 }
51 }
52 }
53 }
54 });
55 }
56 Y.all('p.message').each(function (message) {
57 if (message.hasClass('success')) {
58 var task_id = message.one('span').getContent();
59 Y.all('button').setAttribute('disabled', 'disabled')
60 check_status(task_id, message);
61 }
62 });
63});
64</script>
65{% endblock %}
66{% block content %}
67
68<div id="messages">
69{% if messages %}
70 {% for message in messages %}
71 <p class="message{% if message.tags %} {{ message.tags }}{% endif %}">{{ message|safe }}</p>
72 {% endfor %}
73{% endif %}
74</div>
75{% for task in task_list %}
76<div class="task">
77<form method="post" action=".">{% csrf_token %}
78<input type="hidden" name="task" value="{{ task.name }}"/>
79<button type="submit">{{ task.name }}</button> {{ task.doc }}
80</form>
81</div>
82{% endfor %}
83
84{% endblock %}
085
=== modified file 'src/webcatalog/templatetags/webcatalog.py'
--- src/webcatalog/templatetags/webcatalog.py 2012-06-06 16:38:11 +0000
+++ src/webcatalog/templatetags/webcatalog.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Custom template tags for the Apps Directory."""18"""Custom template tags for the Apps Directory."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25__metaclass__ = type22__metaclass__ = type
26__all__ = [23__all__ = [
2724
=== modified file 'src/webcatalog/tests/__init__.py'
--- src/webcatalog/tests/__init__.py 2012-06-28 09:49:11 +0000
+++ src/webcatalog/tests/__init__.py 2012-07-02 21:27:20 +0000
@@ -27,6 +27,7 @@
27from .test_migrations import *27from .test_migrations import *
28from .test_pep8 import *28from .test_pep8 import *
29from .test_preflight import *29from .test_preflight import *
30from .test_tasks import *
30from .test_templatetags import *31from .test_templatetags import *
31from .test_utilities import *32from .test_utilities import *
32from .test_views import *33from .test_views import *
3334
=== modified file 'src/webcatalog/tests/factory.py'
--- src/webcatalog/tests/factory.py 2012-06-28 09:49:11 +0000
+++ src/webcatalog/tests/factory.py 2012-07-02 21:27:20 +0000
@@ -17,17 +17,19 @@
1717
18"""A TestCase class with a built in object factory."""18"""A TestCase class with a built in object factory."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,21
22 with_statement,
23)
24import os22import os
25from datetime import (23from datetime import (
26 datetime,24 datetime,
27 timedelta,25 timedelta,
28)26)
29from itertools import count27from itertools import count
30from django.contrib.auth.models import User28from django.contrib.auth.models import (
29 Group,
30 Permission,
31 User,
32)
31from django.contrib.sessions.models import Session33from django.contrib.sessions.models import Session
32from django.test import (34from django.test import (
33 TestCase,35 TestCase,
@@ -70,7 +72,8 @@
7072
71 def make_user(self, username=None, email=None, password='test',73 def make_user(self, username=None, email=None, password='test',
72 first_name=None, last_name=None, open_id=None,74 first_name=None, last_name=None, open_id=None,
73 is_admin=False):75 is_admin=False, permissions=None, groups=None,
76 log_in=False):
74 if username is None:77 if username is None:
75 username = self.get_unique_string(prefix='username')78 username = self.get_unique_string(prefix='username')
76 if email is None:79 if email is None:
@@ -98,6 +101,15 @@
98 useropenid = UserOpenID.objects.create(101 useropenid = UserOpenID.objects.create(
99 user=user, claimed_id=open_id, display_id=open_id)102 user=user, claimed_id=open_id, display_id=open_id)
100103
104 if permissions:
105 for permission in permissions:
106 user.user_permissions.add(
107 Permission.objects.get(codename=permission))
108
109 if groups:
110 for group_name in groups:
111 group, created = Group.objects.get_or_create(name=group_name)
112 user.groups.add(group)
101 return user113 return user
102114
103 def make_application(self, package_name=None, name=None,115 def make_application(self, package_name=None, name=None,
@@ -241,6 +253,15 @@
241 super(TestCaseWithFactory, self).setUp()253 super(TestCaseWithFactory, self).setUp()
242 self.factory = WebCatalogObjectFactory()254 self.factory = WebCatalogObjectFactory()
243255
256 def make_user(self, *args, **kwargs):
257 """Invokes self.factory.make_user(), but can also log the user in"""
258 logged_in = kwargs.pop('logged_in', False)
259 password = kwargs.get('password', 'test')
260 result = self.factory.make_user(*args, **kwargs)
261 if logged_in:
262 self.client.login(username=result.username, password=password)
263 return result
264
244265
245class TxTestCaseWithFactory(TransactionTestCase):266class TxTestCaseWithFactory(TransactionTestCase):
246267
247268
=== modified file 'src/webcatalog/tests/helpers.py'
--- src/webcatalog/tests/helpers.py 2012-06-28 09:34:49 +0000
+++ src/webcatalog/tests/helpers.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Helpers for testing the Apps Directory."""18"""Helpers for testing the Apps Directory."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25__metaclass__ = type22__metaclass__ = type
26__all__ = [23__all__ = [
2724
=== modified file 'src/webcatalog/tests/test_commands.py'
--- src/webcatalog/tests/test_commands.py 2012-06-28 14:14:23 +0000
+++ src/webcatalog/tests/test_commands.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,8 @@
1717
18"""Test cases for the Apps Directory management commands."""18"""Test cases for the Apps Directory management commands."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,21
22 with_statement,
23)
24import apt22import apt
25import json23import json
26import os24import os
@@ -716,7 +714,7 @@
716 def test_updates_last_import_record(self):714 def test_updates_last_import_record(self):
717 onion = self.factory.make_distroseries(code_name='onion')715 onion = self.factory.make_distroseries(code_name='onion')
718 orig_timestamp = datetime(2011, 07, 18, 14, 43)716 orig_timestamp = datetime(2011, 07, 18, 14, 43)
719 import_record = ReviewStatsImport.objects.create(717 ReviewStatsImport.objects.create(
720 distroseries=onion, last_import=orig_timestamp)718 distroseries=onion, last_import=orig_timestamp)
721719
722 call_command('import_ratings_stats', 'onion')720 call_command('import_ratings_stats', 'onion')
@@ -728,7 +726,7 @@
728 def test_download_review_stats_no_previous(self):726 def test_download_review_stats_no_previous(self):
729 # If there have been no previous imports, the complete stats727 # If there have been no previous imports, the complete stats
730 # will be retrieved.728 # will be retrieved.
731 onion = self.factory.make_distroseries(code_name='onion')729 self.factory.make_distroseries(code_name='onion')
732730
733 call_command('import_ratings_stats', 'onion')731 call_command('import_ratings_stats', 'onion')
734732
735733
=== modified file 'src/webcatalog/tests/test_department_filters.py'
--- src/webcatalog/tests/test_department_filters.py 2012-06-06 18:00:26 +0000
+++ src/webcatalog/tests/test_department_filters.py 2012-07-02 21:27:20 +0000
@@ -17,11 +17,7 @@
1717
18"""Test cases for department filters."""18"""Test cases for department filters."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
24
2521
26from webcatalog.tests.factory import TestCaseWithFactory22from webcatalog.tests.factory import TestCaseWithFactory
27from webcatalog.department_filters import (23from webcatalog.department_filters import (
2824
=== modified file 'src/webcatalog/tests/test_forms.py'
--- src/webcatalog/tests/test_forms.py 2012-06-28 14:25:19 +0000
+++ src/webcatalog/tests/test_forms.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Test cases for the Apps Directory forms."""18"""Test cases for the Apps Directory forms."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25from django.forms import ValidationError22from django.forms import ValidationError
26from django.test import TestCase23from django.test import TestCase
2724
=== modified file 'src/webcatalog/tests/test_managers.py'
--- src/webcatalog/tests/test_managers.py 2012-06-26 12:40:57 +0000
+++ src/webcatalog/tests/test_managers.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Test cases for object managers."""18"""Test cases for object managers."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25from webcatalog.tests.factory import TestCaseWithFactory22from webcatalog.tests.factory import TestCaseWithFactory
26from webcatalog.models import Application23from webcatalog.models import Application
2724
=== modified file 'src/webcatalog/tests/test_models.py'
--- src/webcatalog/tests/test_models.py 2012-06-06 19:48:08 +0000
+++ src/webcatalog/tests/test_models.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Test cases for models."""18"""Test cases for models."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25from django.core.urlresolvers import reverse22from django.core.urlresolvers import reverse
26from django.core.files.images import ImageFile23from django.core.files.images import ImageFile
2724
=== modified file 'src/webcatalog/tests/test_preflight.py'
--- src/webcatalog/tests/test_preflight.py 2012-06-21 20:18:44 +0000
+++ src/webcatalog/tests/test_preflight.py 2012-07-02 21:27:20 +0000
@@ -28,25 +28,13 @@
2828
2929
30class TestPreflight(TestCaseWithFactory):30class TestPreflight(TestCaseWithFactory):
31
32 def login(self, add_to_group=True, user=None):
33 if not user:
34 user = User.objects.create_user(username='test',
35 email='test@test.com',
36 password='test')
37 if add_to_group:
38 group = Group.objects.create(name='preflight')
39 user.groups.add(group)
40 user.save()
41 self.client.login(username='test', password='test')
42
43 @patch('webcatalog.utilities.WebServices.identity_provider')31 @patch('webcatalog.utilities.WebServices.identity_provider')
44 @patch('webcatalog.utilities.WebServices.get_screenshots_for_package')32 @patch('webcatalog.utilities.WebServices.get_screenshots_for_package')
45 @patch('webcatalog.utilities.WebServices.recommender_api')33 @patch('webcatalog.utilities.WebServices.recommender_api')
46 @patch('webcatalog.utilities.WebServices.rnr_api')34 @patch('webcatalog.utilities.WebServices.rnr_api')
47 def test_login_needed_for_preflight(self, mock_rnr, mock_recommender,35 def test_login_needed_for_preflight(self, mock_rnr, mock_recommender,
48 mock_screenshots, mock_sso):36 mock_screenshots, mock_sso):
49 self.login()37 self.make_user(groups=['preflight'], logged_in=True)
5038
51 with patch_settings(PREFLIGHT_GROUPS=['preflight']):39 with patch_settings(PREFLIGHT_GROUPS=['preflight']):
52 response = self.client.get('/preflight/')40 response = self.client.get('/preflight/')
@@ -60,8 +48,7 @@
60 self.assertEqual(404, response.status_code)48 self.assertEqual(404, response.status_code)
6149
62 def test_login_but_wrong_group_means_preflight_not_found(self):50 def test_login_but_wrong_group_means_preflight_not_found(self):
63 with patch_settings(PREFLIGHT_GROUPS=['preflight']):51 self.make_user(logged_in=True)
64 self.login(add_to_group=False)
6552
66 response = self.client.get('/preflight/')53 response = self.client.get('/preflight/')
6754
@@ -77,7 +64,7 @@
77 mock_recommender.server_status.return_value = 'ok'64 mock_recommender.server_status.return_value = 'ok'
78 mock_screenshots.return_value = ['some_screenshot.jpg']65 mock_screenshots.return_value = ['some_screenshot.jpg']
79 mock_sso.validate_token.return_value = False66 mock_sso.validate_token.return_value = False
80 self.login()67 self.make_user(groups=['preflight'], logged_in=True)
8168
82 with patch_settings(PREFLIGHT_GROUPS=['preflight']):69 with patch_settings(PREFLIGHT_GROUPS=['preflight']):
83 response = self.client.get('/preflight/')70 response = self.client.get('/preflight/')
8471
=== added file 'src/webcatalog/tests/test_tasks.py'
--- src/webcatalog/tests/test_tasks.py 1970-01-01 00:00:00 +0000
+++ src/webcatalog/tests/test_tasks.py 2012-07-02 21:27:20 +0000
@@ -0,0 +1,42 @@
1from django.conf import settings
2from django.test import TestCase
3from mock import patch
4
5from webcatalog.models import Exhibit
6from webcatalog.tasks import (
7 fail,
8 import_exhibits,
9)
10
11
12class ImportExhbitsTaskTestCase(TestCase):
13 @patch('webcatalog.management.commands.import_exhibits.urllib.urlopen')
14 def test_success(self, mock_urlopen):
15 """Test that the ``import_exhibits`` task runs with no errors"""
16 fd = mock_urlopen.return_value
17 fd.code = 200
18 fd.read.return_value = """[{
19 "package_names": "foobar",
20 "banner_url": "http://example.com/exhibits/foobar.png",
21 "distroseries": [{"version": "12.04", "code_name": "precise"}],
22 "html": "<div><div>Hello world!</div></div>",
23 "published": true,
24 "date_created": "2012-06-11 17:22:45",
25 "id": 2
26 }]"""
27 expected_url = '%sexhibits/en/' % settings.SCA_API_URL
28
29 result = import_exhibits.delay()
30
31 self.assertIsNone(result.get())
32 self.assertTrue(result.successful())
33 mock_urlopen.assertCalledWith(expected_url)
34 self.assertEqual(1, Exhibit.objects.all().count())
35
36
37class FailTaskTestCase(TestCase):
38 @patch('webcatalog.tasks.sleep')
39 def test_delays_and_fails_as_expected(self, mock_sleep):
40 """Test that the ``fail`` task raises an Exception as expected"""
41 self.assertRaises(ZeroDivisionError, fail.delay)
42 mock_sleep.assertCalledWith(5)
043
=== modified file 'src/webcatalog/tests/test_templatetags.py'
--- src/webcatalog/tests/test_templatetags.py 2012-06-14 13:57:03 +0000
+++ src/webcatalog/tests/test_templatetags.py 2012-07-02 21:27:20 +0000
@@ -17,13 +17,9 @@
1717
18"""Tests for the Apps Directory template tags."""18"""Tests for the Apps Directory template tags."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25import unittest22import unittest
26from decimal import Decimal
2723
28from django.core.urlresolvers import reverse24from django.core.urlresolvers import reverse
29from django.template import Context25from django.template import Context
3026
=== modified file 'src/webcatalog/tests/test_views.py'
--- src/webcatalog/tests/test_views.py 2012-06-28 19:06:37 +0000
+++ src/webcatalog/tests/test_views.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,7 @@
1717
18"""Test cases for the Apps Directory views."""18"""Test cases for the Apps Directory views."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25import json22import json
26import re23import re
@@ -53,6 +50,8 @@
53 'ApplicationReviewsTestCase',50 'ApplicationReviewsTestCase',
54 'ApplicationScreenshotsTestCase',51 'ApplicationScreenshotsTestCase',
55 'IndexTestCase',52 'IndexTestCase',
53 'TasksViewTestCase',
54 'TaskStatusViewTestCase',
56 'TermsOfServiceTestCase',55 'TermsOfServiceTestCase',
57 'OverviewTestCase',56 'OverviewTestCase',
58 'SearchTestCase',57 'SearchTestCase',
@@ -1313,3 +1312,116 @@
1313 expected = '/* js/foo.css */\n/* [missing] */\n'1312 expected = '/* js/foo.css */\n/* [missing] */\n'
1314 self.assertEqual('text/css', response['Content-Type'])1313 self.assertEqual('text/css', response['Content-Type'])
1315 self.assertEqual(expected, response.content)1314 self.assertEqual(expected, response.content)
1315
1316
1317class TasksViewTestCase(TestCaseWithFactory):
1318 """Tests for the task list"""
1319 def test_not_allowed_if_not_logged_in(self):
1320 url = reverse('wc-tasks')
1321
1322 response = self.client.get(url)
1323
1324 self.assertEqual(302, response.status_code)
1325 expected = reverse('wc-forbidden') + '?next=' + url
1326 self.assertTrue(response['location'].endswith(expected))
1327
1328 def test_scheduler_perm_is_required(self):
1329 self.make_user(logged_in=True)
1330 url = reverse('wc-tasks')
1331
1332 response = self.client.get(url)
1333
1334 self.assertEqual(302, response.status_code)
1335 expected = reverse('wc-forbidden') + '?next=' + url
1336 self.assertTrue(response['location'].endswith(expected))
1337
1338 def test_includes_all_tasks(self):
1339 self.maxDiff = None
1340 self.make_user(permissions=['schedule_tasks'], logged_in=True)
1341 url = reverse('wc-tasks')
1342
1343 response = self.client.get(url)
1344
1345 tasks = response.context[0]['task_list']
1346 expected = [
1347 {'doc': 'Import all exhibits', 'name': 'import_exhibits'},
1348 {
1349 'doc': 'Remove stale OAuth nonces from the DB',
1350 'name': 'cleanup_nonces',
1351 },
1352 {
1353 'doc': 'Remove stale sessions from the DB',
1354 'name': 'cleanup_sessions',
1355 },
1356 {
1357 'doc': 'Sleep for 5 seconds, then raise an error',
1358 'name': 'fail',
1359 },
1360 {
1361 'doc': 'Import all data from app-install-data and apt-cache',
1362 'name': 'import_app_install_data',
1363 },
1364 {
1365 'doc': ('Import all data from the Software '
1366 'Center Agent (MyApps)'),
1367 'name': 'import_sca',
1368 },
1369 {
1370 'doc': 'Import all ratings and reviews data',
1371 'name': 'import_ratings_stats',
1372 },
1373 {
1374 'doc': "Update the 'is_latest' bit on all Applications",
1375 'name': 'check_all_latest',
1376 },
1377 ]
1378 self.assertEqual(sorted(expected), sorted(tasks))
1379
1380 @patch('webcatalog.management.commands.import_exhibits.urllib.urlopen')
1381 def test_post_schedules_task(self, mock_urlopen):
1382 fd = mock_urlopen.return_value
1383 fd.code = 200
1384 fd.read.return_value = "[]"
1385 self.make_user(permissions=['schedule_tasks'], logged_in=True)
1386 url = reverse('wc-tasks')
1387 data = {'task': 'import_exhibits'}
1388
1389 response = self.client.post(url, data=data, follow=True)
1390
1391 msg_pat = r'Task id <span>[-\da-f]+</span> submitted'
1392 match = re.search(msg_pat, response.content)
1393 self.assertFalse(match is None)
1394 self.assertEqual(1, mock_urlopen.call_count)
1395
1396
1397class TaskStatusViewTestCase(TestCaseWithFactory):
1398 """Tests for the task status getter"""
1399 def test_not_allowed_if_not_logged_in(self):
1400 url = reverse('wc-task-status', args=['cafe'])
1401
1402 response = self.client.get(url)
1403
1404 self.assertEqual(302, response.status_code)
1405 expected = reverse('wc-forbidden') + '?next=' + url
1406 self.assertTrue(response['location'].endswith(expected))
1407
1408 def test_scheduler_perm_is_required(self):
1409 self.make_user(logged_in=True)
1410 url = reverse('wc-task-status', args=['cafe'])
1411
1412 response = self.client.get(url)
1413
1414 self.assertEqual(302, response.status_code)
1415 expected = reverse('wc-forbidden') + '?next=' + url
1416 self.assertTrue(response['location'].endswith(expected))
1417
1418 def test_unknown_task_is_reported_pending(self):
1419 self.make_user(permissions=['schedule_tasks'], logged_in=True)
1420 url = reverse('wc-task-status', args=['cafe'])
1421
1422 response = self.client.get(url)
1423
1424 data = json.loads(response.content)
1425 expected = {"status": "PENDING", "traceback": None, "id": "cafe"}
1426
1427 self.assertEqual(expected, data)
13161428
=== modified file 'src/webcatalog/urls.py'
--- src/webcatalog/urls.py 2012-06-06 17:42:04 +0000
+++ src/webcatalog/urls.py 2012-07-02 21:27:20 +0000
@@ -17,10 +17,8 @@
1717
18"""Url configuration for the Apps Directory."""18"""Url configuration for the Apps Directory."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,21
22 with_statement,
23)
24from django.conf.urls.defaults import patterns, include, url22from django.conf.urls.defaults import patterns, include, url
25from django.views.generic import TemplateView23from django.views.generic import TemplateView
2624
@@ -59,6 +57,10 @@
59 url(r'^tos/plain/$', name="wc-tos-plain",57 url(r'^tos/plain/$', name="wc-tos-plain",
60 view=TemplateView.as_view(template_name="webcatalog/tos_plain.html")),58 view=TemplateView.as_view(template_name="webcatalog/tos_plain.html")),
61 url(r'^combo/$', 'combo_view', name='wc-combo'),59 url(r'^combo/$', 'combo_view', name='wc-combo'),
60 url(r'^forbidden/$', 'forbidden', name='wc-forbidden'),
61 url(r'^tasks/$', 'tasks', name='wc-tasks'),
62 url(r'^task_status/(?P<task_id>[-\da-f]+)/$', 'task_status',
63 name='wc-task-status'),
6264
63 (r'^api/', include('webcatalog.api.urls')),65 (r'^api/', include('webcatalog.api.urls')),
64)66)
6567
=== modified file 'src/webcatalog/views.py'
--- src/webcatalog/views.py 2012-06-28 14:46:12 +0000
+++ src/webcatalog/views.py 2012-07-02 21:27:20 +0000
@@ -17,15 +17,14 @@
1717
18"""Views for the Apps Directory app."""18"""Views for the Apps Directory app."""
1919
20from __future__ import (20from __future__ import absolute_import
21 absolute_import,
22 with_statement,
23)
2421
25import json22import json
26import operator23import operator
27import os24import os
25from inspect import getmembers
2826
27from celery.task import Task
29from convoy.combo import combine_files, parse_qs28from convoy.combo import combine_files, parse_qs
30from django.conf import settings29from django.conf import settings
31from django.contrib import messages30from django.contrib import messages
@@ -33,17 +32,23 @@
33from django.core.urlresolvers import reverse32from django.core.urlresolvers import reverse
34from django.db.models import Q33from django.db.models import Q
35from django.http import (34from django.http import (
35 HttpResponse,
36 HttpResponseForbidden,
37 HttpResponseNotFound,
36 HttpResponseRedirect,38 HttpResponseRedirect,
37 HttpResponse,
38)39)
39from django.shortcuts import (40from django.shortcuts import (
40 get_list_or_404,
41 get_object_or_404,41 get_object_or_404,
42 render_to_response,42 render_to_response,
43)43)
44from django.template import RequestContext44from django.template import RequestContext
45from django.template.loader import render_to_string
45from django.utils.translation import ugettext as _46from django.utils.translation import ugettext as _
47from django.views.decorators.cache import never_cache
48from django.views.decorators.http import require_GET
49from djcelery.models import TaskState
4650
51import webcatalog.tasks
47from webcatalog.forms import EmailDownloadLinkForm52from webcatalog.forms import EmailDownloadLinkForm
48from webcatalog.models import (53from webcatalog.models import (
49 Application,54 Application,
@@ -51,6 +56,7 @@
51 DistroSeries,56 DistroSeries,
52 Exhibit,57 Exhibit,
53)58)
59from webcatalog.decorators import scheduler_required
54from webcatalog.utilities import WebServices60from webcatalog.utilities import WebServices
5561
5662
@@ -59,8 +65,10 @@
59 'application_detail',65 'application_detail',
60 'application_recommends',66 'application_recommends',
61 'application_reviews',67 'application_reviews',
68 'department_overview',
69 'forbidden',
62 'index',70 'index',
63 'department_overview',71 'schedule_task',
64 'search',72 'search',
65]73]
6674
@@ -295,3 +303,49 @@
295 content_type=content_type, status=200,303 content_type=content_type, status=200,
296 content="".join(content))304 content="".join(content))
297 return HttpResponse(content_type=content_type, status=404)305 return HttpResponse(content_type=content_type, status=404)
306
307
308@scheduler_required
309@never_cache
310def tasks(request):
311 if request.method == 'POST':
312 task_name = request.POST.get('task', '')
313 task = getattr(webcatalog.tasks, task_name, None)
314 if not task:
315 messages.error(request, 'Invalid task name "%s"' % task_name)
316 return HttpResponseRedirect(reverse('wc-tasks'))
317 result = task.delay()
318 messages.success(request,
319 'Task id <span>%s</span> submitted' % result.task_id)
320 return HttpResponseRedirect(reverse('wc-tasks'))
321 is_task = lambda x: isinstance(x, Task)
322 task_list = [
323 {'name': key, 'doc': val.__doc__}
324 for key, val in getmembers(webcatalog.tasks, is_task)
325 ]
326 context = {
327 'task_list': sorted(task_list),
328 }
329 return render_to_response('webcatalog/task_list.html',
330 RequestContext(request, context))
331
332
333@scheduler_required
334@require_GET
335@never_cache
336def task_status(request, task_id):
337 try:
338 task = TaskState.objects.get(task_id=task_id)
339 data = {
340 'id': task.task_id,
341 'status': task.state,
342 'traceback': task.traceback,
343 }
344 except TaskState.DoesNotExist:
345 data = {'id': task_id, 'status': 'PENDING', 'traceback': None}
346 return HttpResponse(json.dumps(data), mimetype='application/json')
347
348
349def forbidden(request):
350 return HttpResponseForbidden(
351 render_to_string('forbidden.html', RequestContext(request)))

Subscribers

People subscribed via source and target branches