Commit fd7476715230d54e084c3090e36bb6fd83e7286d
Exists in
master
and in
29 other branches
Merge branch 'master' of github.com:colab/colab
Showing
22 changed files
with
349 additions
and
45 deletions
Show diff stats
colab/__init__.py
colab/celery.py
@@ -14,13 +14,6 @@ app = Celery('colab') | @@ -14,13 +14,6 @@ app = Celery('colab') | ||
14 | app.config_from_object('django.conf:settings') | 14 | app.config_from_object('django.conf:settings') |
15 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) | 15 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) |
16 | 16 | ||
17 | -app.conf.update( | ||
18 | - CELERY_RESULT_BACKEND='djcelery.backends.database:DatabaseBackend', | ||
19 | -) | ||
20 | -app.conf.update( | ||
21 | - CELERY_RESULT_BACKEND='djcelery.backends.cache:CacheBackend', | ||
22 | -) | ||
23 | - | ||
24 | 17 | ||
25 | @app.task(bind=True) | 18 | @app.task(bind=True) |
26 | def debug_task(self): | 19 | def debug_task(self): |
colab/home/apps.py
colab/plugins/__init__.py
colab/plugins/gitlab/apps.py
1 | 1 | ||
2 | from ..utils.apps import ColabProxiedAppConfig | 2 | from ..utils.apps import ColabProxiedAppConfig |
3 | +from colab.plugins.gitlab.tasks import handling_method | ||
4 | +from colab.signals.signals import register_signal, connect_signal | ||
3 | 5 | ||
4 | 6 | ||
5 | class ProxyGitlabAppConfig(ColabProxiedAppConfig): | 7 | class ProxyGitlabAppConfig(ColabProxiedAppConfig): |
6 | name = 'colab.plugins.gitlab' | 8 | name = 'colab.plugins.gitlab' |
7 | verbose_name = 'Gitlab Plugin' | 9 | verbose_name = 'Gitlab Plugin' |
10 | + short_name = 'gitlab' | ||
11 | + | ||
12 | + signals_list = ['gitlab_create_project'] | ||
13 | + | ||
14 | + def register_signal(self): | ||
15 | + register_signal(self.short_name, self.signals_list) | ||
16 | + | ||
17 | + def connect_signal(self): | ||
18 | + connect_signal(self.signals_list[0], self.short_name, handling_method) |
colab/plugins/utils/apps.py
@@ -4,3 +4,9 @@ from django.apps import AppConfig | @@ -4,3 +4,9 @@ from django.apps import AppConfig | ||
4 | 4 | ||
5 | class ColabProxiedAppConfig(AppConfig): | 5 | class ColabProxiedAppConfig(AppConfig): |
6 | colab_proxied_app = True | 6 | colab_proxied_app = True |
7 | + | ||
8 | + def register_signals(self): | ||
9 | + pass | ||
10 | + | ||
11 | + def connect_signals(self): | ||
12 | + pass |
@@ -0,0 +1,22 @@ | @@ -0,0 +1,22 @@ | ||
1 | + | ||
2 | +from django.apps import apps | ||
3 | + | ||
4 | + | ||
5 | +def _init_signals(method_name): | ||
6 | + for app in apps.get_app_configs(): | ||
7 | + # Try to get the method with `method_name`. | ||
8 | + # If it exists call it using `app` as the first parameter. | ||
9 | + # This is required because methods take `self` as first | ||
10 | + # parameter and as we are calling it as a function python | ||
11 | + # won't send it explicitly. | ||
12 | + # If the method doesn't exist we return a dummy function that | ||
13 | + # won't do anything. | ||
14 | + getattr(app, method_name, lambda: None)() | ||
15 | + | ||
16 | + | ||
17 | +def register_signal(): | ||
18 | + _init_signals('register_signal') | ||
19 | + | ||
20 | + | ||
21 | +def connect_signal(): | ||
22 | + _init_signals('connect_signal') |
@@ -0,0 +1,20 @@ | @@ -0,0 +1,20 @@ | ||
1 | +from django.test import TestCase | ||
2 | +from mock import patch, MagicMock | ||
3 | +from colab.plugins.utils import signals | ||
4 | + | ||
5 | + | ||
6 | +class SignalsTest(TestCase): | ||
7 | + @patch("colab.plugins.utils.signals.apps.get_app_configs") | ||
8 | + def test_init_signals(self, mock_app): | ||
9 | + method_name = 'test' | ||
10 | + | ||
11 | + app_mock = MagicMock() | ||
12 | + | ||
13 | + apps_list = ['a', 'b', app_mock] | ||
14 | + | ||
15 | + mock_app.return_value = apps_list | ||
16 | + signals._init_signals(method_name) | ||
17 | + | ||
18 | + app_mock.test.assert_called_with() | ||
19 | + self.assertEqual(1, app_mock.test.call_count) | ||
20 | + self.assertTrue(app_mock.test.called) |
colab/settings.py
@@ -47,7 +47,6 @@ INSTALLED_APPS = ( | @@ -47,7 +47,6 @@ INSTALLED_APPS = ( | ||
47 | 'haystack', | 47 | 'haystack', |
48 | 'hitcounter', | 48 | 'hitcounter', |
49 | 'taggit', | 49 | 'taggit', |
50 | - 'djcelery', | ||
51 | 50 | ||
52 | # Own apps | 51 | # Own apps |
53 | 'colab.home', | 52 | 'colab.home', |
@@ -57,6 +56,7 @@ INSTALLED_APPS = ( | @@ -57,6 +56,7 @@ INSTALLED_APPS = ( | ||
57 | 'colab.search', | 56 | 'colab.search', |
58 | 'colab.tz', | 57 | 'colab.tz', |
59 | 'colab.utils', | 58 | 'colab.utils', |
59 | + 'colab.signals', | ||
60 | ) | 60 | ) |
61 | 61 | ||
62 | ROOT_URLCONF = 'colab.urls' | 62 | ROOT_URLCONF = 'colab.urls' |
@@ -253,8 +253,8 @@ from .utils import conf | @@ -253,8 +253,8 @@ from .utils import conf | ||
253 | 253 | ||
254 | SOCIAL_NETWORK_ENABLED = locals().get('SOCIAL_NETWORK_ENABLED') or False | 254 | SOCIAL_NETWORK_ENABLED = locals().get('SOCIAL_NETWORK_ENABLED') or False |
255 | 255 | ||
256 | -locals().update(conf.load_colab_apps()) | ||
257 | locals().update(conf.load_py_settings()) | 256 | locals().update(conf.load_py_settings()) |
257 | +locals().update(conf.load_colab_apps()) | ||
258 | 258 | ||
259 | COLAB_APPS = locals().get('COLAB_APPS') or {} | 259 | COLAB_APPS = locals().get('COLAB_APPS') or {} |
260 | PROXIED_APPS = {} | 260 | PROXIED_APPS = {} |
@@ -0,0 +1,47 @@ | @@ -0,0 +1,47 @@ | ||
1 | + | ||
2 | +from django.dispatch import Signal | ||
3 | + | ||
4 | +from .exceptions import SignalDoesNotExist | ||
5 | + | ||
6 | +registered_signals = {} | ||
7 | +signal_instances = {} | ||
8 | + | ||
9 | + | ||
10 | +class ColabSignal(Signal): | ||
11 | + def __reduce__(self): | ||
12 | + """ | ||
13 | + | ||
14 | + In order to send a signal to a celery task, it is necessary to pickle | ||
15 | + the objects that will be used as parameters. However, | ||
16 | + django.dispatch.Signal has an instance of threading.Lock, which is an | ||
17 | + object that cannot be pickled. Therefore, this function changes the | ||
18 | + pickle behaviour of Signal, making that only the providind_args of | ||
19 | + Signal to be pickled.""" | ||
20 | + | ||
21 | + return (ColabSignal, (self.providing_args,)) | ||
22 | + | ||
23 | + | ||
24 | +def register_signal(plugin_name, list_signals): | ||
25 | + for signal in list_signals: | ||
26 | + if signal in registered_signals: | ||
27 | + if plugin_name not in registered_signals[signal]: | ||
28 | + registered_signals[signal].append(plugin_name) | ||
29 | + else: | ||
30 | + registered_signals[signal] = [] | ||
31 | + registered_signals[signal].append(plugin_name) | ||
32 | + signal_instances[signal] = ColabSignal() | ||
33 | + | ||
34 | + | ||
35 | +def connect_signal(signal_name, sender, handling_method): | ||
36 | + if signal_name in signal_instances: | ||
37 | + signal_instances[signal_name].connect(handling_method.delay, | ||
38 | + sender=sender) | ||
39 | + else: | ||
40 | + raise SignalDoesNotExist | ||
41 | + | ||
42 | + | ||
43 | +def send(signal_name, sender, **kwargs): | ||
44 | + if signal_name in signal_instances: | ||
45 | + signal_instances[signal_name].send(sender=sender, **kwargs) | ||
46 | + else: | ||
47 | + raise SignalDoesNotExist |
@@ -0,0 +1,76 @@ | @@ -0,0 +1,76 @@ | ||
1 | +""" | ||
2 | +Test Signals class. | ||
3 | +Objective: Test parameters, and behavior. | ||
4 | +""" | ||
5 | + | ||
6 | +from django.test import TestCase | ||
7 | + | ||
8 | +from mock import patch, MagicMock, PropertyMock | ||
9 | + | ||
10 | +from ..signals import registered_signals, register_signal, connect_signal, send | ||
11 | +from ..exceptions import SignalDoesNotExist | ||
12 | + | ||
13 | + | ||
14 | +class SignalsTest(TestCase): | ||
15 | + | ||
16 | + def setUp(self): | ||
17 | + self.list_signal = ['a', 'b', 'c'] | ||
18 | + self.plugin_name = 'test_signal' | ||
19 | + | ||
20 | + def test_register_signal_(self): | ||
21 | + register_signal(self.plugin_name, self.list_signal) | ||
22 | + signal_name = 'a' | ||
23 | + signal_list = ['test_signal'] | ||
24 | + self.assertEqual(len(registered_signals[signal_name]), 1) | ||
25 | + self.assertEqual(registered_signals[signal_name], signal_list) | ||
26 | + | ||
27 | + def test_register_signal_already_registered(self): | ||
28 | + signal_name = 'a' | ||
29 | + signal_list = ['test_signal'] | ||
30 | + | ||
31 | + register_signal(self.plugin_name, self.list_signal) | ||
32 | + self.assertEqual(len(registered_signals[signal_name]), 1) | ||
33 | + | ||
34 | + register_signal(self.plugin_name, self.list_signal) | ||
35 | + self.assertEqual(len(registered_signals[signal_name]), 1) | ||
36 | + self.assertEqual(registered_signals[signal_name], signal_list) | ||
37 | + | ||
38 | + def test_connect_non_registered_signal(self): | ||
39 | + sender = 'Test' | ||
40 | + handling_method = 'Test' | ||
41 | + signal_name = 'Test' | ||
42 | + | ||
43 | + self.assertRaises(SignalDoesNotExist, connect_signal, signal_name, | ||
44 | + sender, handling_method) | ||
45 | + | ||
46 | + @patch('colab.signals.signals.Signal.connect') | ||
47 | + def test_connect_already_registered_signal(self, mock): | ||
48 | + sender = 'Test' | ||
49 | + handling_method = MagicMock() | ||
50 | + type(handling_method).delay = PropertyMock(return_value='Test') | ||
51 | + signal_name = 'a' | ||
52 | + | ||
53 | + register_signal(self.plugin_name, self.list_signal) | ||
54 | + | ||
55 | + connect_signal(signal_name, sender, handling_method) | ||
56 | + args, kwargs = mock.call_args | ||
57 | + | ||
58 | + self.assertEqual(args[0], handling_method.delay) | ||
59 | + self.assertEqual(kwargs['sender'], sender) | ||
60 | + self.assertTrue(mock.is_called) | ||
61 | + | ||
62 | + @patch('colab.signals.signals.Signal.send') | ||
63 | + def test_send_signal(self, mock): | ||
64 | + sender = 'Test' | ||
65 | + signal_name = 'a' | ||
66 | + | ||
67 | + register_signal(self.plugin_name, self.list_signal) | ||
68 | + send(signal_name, sender) | ||
69 | + | ||
70 | + args, kwargs = mock.call_args | ||
71 | + | ||
72 | + self.assertEqual(kwargs['sender'], sender) | ||
73 | + self.assertTrue(mock.is_called) | ||
74 | + | ||
75 | + def test_send_signal_not_registered(self): | ||
76 | + self.assertRaises(SignalDoesNotExist, send, 'test_signal', 'test') |
colab/utils/conf.py
1 | 1 | ||
2 | import os | 2 | import os |
3 | import sys | 3 | import sys |
4 | +import logging | ||
4 | import importlib | 5 | import importlib |
5 | import warnings | 6 | import warnings |
6 | 7 | ||
7 | from django.core.exceptions import ImproperlyConfigured | 8 | from django.core.exceptions import ImproperlyConfigured |
8 | 9 | ||
10 | +logger = logging.getLogger('colab.init') | ||
11 | +if os.environ.get('COLAB_DEBUG'): | ||
12 | + logger.addHandler(logging.StreamHandler()) | ||
13 | + logger.setLevel(logging.INFO) | ||
14 | + | ||
9 | 15 | ||
10 | class InaccessibleSettings(ImproperlyConfigured): | 16 | class InaccessibleSettings(ImproperlyConfigured): |
11 | """Settings.py is Inaccessible. | 17 | """Settings.py is Inaccessible. |
@@ -48,57 +54,71 @@ def _load_py_file(py_path, path): | @@ -48,57 +54,71 @@ def _load_py_file(py_path, path): | ||
48 | 54 | ||
49 | 55 | ||
50 | def load_py_settings(): | 56 | def load_py_settings(): |
51 | - settings_dir = '/etc/colab/settings.d' | ||
52 | settings_file = os.getenv('COLAB_SETTINGS', '/etc/colab/settings.py') | 57 | settings_file = os.getenv('COLAB_SETTINGS', '/etc/colab/settings.py') |
53 | settings_module = settings_file.split('.')[-2].split('/')[-1] | 58 | settings_module = settings_file.split('.')[-2].split('/')[-1] |
54 | py_path = "/".join(settings_file.split('/')[:-1]) | 59 | py_path = "/".join(settings_file.split('/')[:-1]) |
55 | 60 | ||
61 | + logger.info('Settings file: %s', settings_file) | ||
62 | + | ||
56 | if not os.path.exists(py_path): | 63 | if not os.path.exists(py_path): |
57 | msg = "The py file {} does not exist".format(py_path) | 64 | msg = "The py file {} does not exist".format(py_path) |
58 | raise InaccessibleSettings(msg) | 65 | raise InaccessibleSettings(msg) |
59 | 66 | ||
60 | py_settings = _load_py_file(settings_module, py_path) | 67 | py_settings = _load_py_file(settings_module, py_path) |
61 | 68 | ||
62 | - # Try to read settings from settings.d | 69 | + # Read settings from settings.d |
70 | + settings_dir = '/etc/colab/settings.d' | ||
71 | + logger.info('Settings directory: %s', settings_dir) | ||
72 | + | ||
73 | + if not os.path.exists(settings_dir): | ||
74 | + return py_settings | ||
75 | + | ||
76 | + for file_name in os.listdir(settings_dir): | ||
77 | + if not file_name.endswith('.py'): | ||
78 | + continue | ||
63 | 79 | ||
64 | - if os.path.exists(settings_dir): | ||
65 | - for file_name in os.listdir(settings_dir): | ||
66 | - if file_name.endswith('.py'): | ||
67 | - file_module = file_name.split('.')[0] | ||
68 | - py_settings_d = _load_py_file(file_module, settings_dir) | ||
69 | - py_settings.update(py_settings_d) | 80 | + file_module = file_name.split('.')[0] |
81 | + py_settings_d = _load_py_file(file_module, settings_dir) | ||
82 | + py_settings.update(py_settings_d) | ||
83 | + logger.info('Loaded %s/%s', settings_dir, file_name) | ||
70 | 84 | ||
71 | return py_settings | 85 | return py_settings |
72 | 86 | ||
73 | 87 | ||
74 | def load_colab_apps(): | 88 | def load_colab_apps(): |
75 | plugins_dir = os.getenv('COLAB_PLUGINS', '/etc/colab/plugins.d/') | 89 | plugins_dir = os.getenv('COLAB_PLUGINS', '/etc/colab/plugins.d/') |
90 | + logger.info('Plugin settings directory: %s', plugins_dir) | ||
76 | 91 | ||
77 | COLAB_APPS = {} | 92 | COLAB_APPS = {} |
78 | 93 | ||
79 | # Try to read settings from plugins.d | 94 | # Try to read settings from plugins.d |
80 | - if os.path.exists(plugins_dir): | ||
81 | - for file_name in os.listdir(plugins_dir): | ||
82 | - if file_name.endswith('.py'): | ||
83 | - file_module = file_name.split('.')[0] | ||
84 | - py_settings_d = _load_py_file(file_module, plugins_dir) | ||
85 | - fields = ['verbose_name', 'upstream', 'urls', | ||
86 | - 'menu_urls', 'middlewares', 'dependencies', | ||
87 | - 'context_processors', 'private_token'] | ||
88 | - | ||
89 | - app_name = py_settings_d.get('name') | ||
90 | - if not app_name: | ||
91 | - warnings.warn("Plugin missing name variable") | ||
92 | - continue | ||
93 | - | ||
94 | - COLAB_APPS[app_name] = {} | ||
95 | - COLAB_APPS[app_name]['menu_title'] = \ | ||
96 | - py_settings_d.get('menu_title') | ||
97 | - | ||
98 | - for key in fields: | ||
99 | - value = py_settings_d.get(key) | ||
100 | - if value: | ||
101 | - COLAB_APPS[app_name][key] = value | 95 | + if not os.path.exists(plugins_dir): |
96 | + return {'COLAB_APPS': COLAB_APPS} | ||
97 | + | ||
98 | + for file_name in os.listdir(plugins_dir): | ||
99 | + if not file_name.endswith('.py'): | ||
100 | + continue | ||
101 | + | ||
102 | + file_module = file_name.split('.')[0] | ||
103 | + py_settings_d = _load_py_file(file_module, plugins_dir) | ||
104 | + logger.info('Loaded plugin settings: %s/%s', plugins_dir, file_name) | ||
105 | + | ||
106 | + app_name = py_settings_d.get('name') | ||
107 | + if not app_name: | ||
108 | + warnings.warn("Plugin missing name variable") | ||
109 | + continue | ||
110 | + | ||
111 | + COLAB_APPS[app_name] = {} | ||
112 | + COLAB_APPS[app_name]['menu_title'] = py_settings_d.get('menu_title') | ||
113 | + | ||
114 | + fields = ['verbose_name', 'upstream', 'urls', | ||
115 | + 'menu_urls', 'middlewares', 'dependencies', | ||
116 | + 'context_processors', 'private_token'] | ||
117 | + | ||
118 | + for key in fields: | ||
119 | + value = py_settings_d.get(key) | ||
120 | + if value: | ||
121 | + COLAB_APPS[app_name][key] = value | ||
102 | 122 | ||
103 | return {'COLAB_APPS': COLAB_APPS} | 123 | return {'COLAB_APPS': COLAB_APPS} |
104 | 124 |
docs/source/plugindev.rst
1 | 1 | ||
2 | -.. _plugin-dev: | 2 | +.. _plugin-dev: |
3 | 3 | ||
4 | Plugin Developer Documentation | 4 | Plugin Developer Documentation |
5 | ==================================== | 5 | ==================================== |
6 | 6 | ||
7 | Getting Started | 7 | Getting Started |
8 | --------------- | 8 | --------------- |
9 | -.. TODO | 9 | + |
10 | +Signals | ||
11 | +---------- | ||
12 | +Implement signals in plugins is optional! You may follow this steps only if you | ||
13 | +want to communicate with another plugins. | ||
14 | + | ||
15 | +In order to configure a plugin to able to listen and send signals using Colab | ||
16 | +signals structure, some steps are required: | ||
17 | + | ||
18 | +* In the apps.py file it is necessary to declare a list variable containing all | ||
19 | + the signals that the plugin will dispatch. It is suggested to name the | ||
20 | + variable "registered_signals", but that nomenclature not strictly necessary. | ||
21 | +* It is also necessary to declare a variable containing the name of the plugin | ||
22 | + that will send the signal. It must be said that the name of the plugin cannot | ||
23 | + contain any special character, such as dot or comma. It is suggested to name | ||
24 | + the variable "short_name", but that nomenclature is not strictly | ||
25 | + necessary. | ||
26 | +* In order to actually register the signals, it is necessary to implement the | ||
27 | + method register_signal, which require the name of the plugin that is | ||
28 | + registering the signals and a list of signals to be registered as parameters. | ||
29 | + You must not call this method nowhere. | ||
30 | +* In order to listen for a given signal, it is required to create a handling | ||
31 | + method. This method should be located at a file named tasks.py in the same | ||
32 | + directory as the plugins files. It also must be said that this method need to | ||
33 | + receive at least a \*\*kwargs parameter. An example of a handling method can | ||
34 | + be seen below: | ||
35 | + | ||
36 | +.. code-block:: python | ||
37 | + from colab.celery import app | ||
38 | + | ||
39 | + @app.task(bind=True) | ||
40 | + def handling_method(self, **kwargs): | ||
41 | + # DO SOMETHING | ||
42 | + | ||
43 | +* With signals registered and handling method defined you must connect them. | ||
44 | + To do it you must call connect_signal passing signal name, sender and handling | ||
45 | + method as arguments. These should be implemented on plugin's apps.py. It must | ||
46 | + be said that the plugin app class must extend ColabProxiedAppConfig. An | ||
47 | + example of this configuration can be seen below: | ||
48 | + | ||
49 | + | ||
50 | +.. code-block:: python | ||
51 | + from colab.plugins.utils.apps import ColabProxiedAppConfig | ||
52 | + from colab.signals.signals import register_signal, connect_signal | ||
53 | + from colab.plugins.PLUGIN.tasks import HANDLING_METHOD | ||
54 | + | ||
55 | + class PluginApps(ColabProxiedAppConfig): | ||
56 | + short_name = PLUGIN_NAME | ||
57 | + signals_list = [SIGNAL1, SIGNAL2] | ||
58 | + | ||
59 | + def registered_signal(self): | ||
60 | + register_signal(self.short_name, self.signals_list) | ||
61 | + | ||
62 | + def connect_signal(self): | ||
63 | + connect_signal(self.signals_list[0], self.short_name, | ||
64 | + HANDLING_METHOD) | ||
65 | + connect_signal(self.signals_list[1], self.short_name, | ||
66 | + HANDLING_METHOD) | ||
67 | + | ||
68 | + | ||
69 | +* To send a broadcast signal you must call send method anywhere passing signal | ||
70 | + name and sender as arguments. If necessary you can pass another parameters in | ||
71 | + \*\*kwargs. As you can see below: | ||
72 | + | ||
73 | +.. code-block:: python | ||
74 | + from colab.signals.signals import send | ||
75 | + | ||
76 | + send(signal_name, sender) | ||
77 | + | ||
78 | +* If you want to run celery manually to make some tests, you should execute: | ||
79 | + | ||
80 | +.. code-block:: shell | ||
81 | + celery -A colab worker --loglevel=debug |
setup.py
vagrant/provision.sh
@@ -46,3 +46,5 @@ colab-admin loaddata /vagrant/tests/test_data.json | @@ -46,3 +46,5 @@ colab-admin loaddata /vagrant/tests/test_data.json | ||
46 | sudo cp $basedir/vagrant/misc/etc/init.d/celeryd /etc/init.d/ | 46 | sudo cp $basedir/vagrant/misc/etc/init.d/celeryd /etc/init.d/ |
47 | sudo cp $basedir/vagrant/misc/etc/default/celeryd /etc/default/ | 47 | sudo cp $basedir/vagrant/misc/etc/default/celeryd /etc/default/ |
48 | sudo service celeryd start | 48 | sudo service celeryd start |
49 | + | ||
50 | +colab-admin rebuild_index --noinput |