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 | 14 | app.config_from_object('django.conf:settings') |
15 | 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 | 18 | @app.task(bind=True) |
26 | 19 | def debug_task(self): | ... | ... |
colab/home/apps.py
colab/plugins/__init__.py
colab/plugins/gitlab/apps.py
1 | 1 | |
2 | 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 | 7 | class ProxyGitlabAppConfig(ColabProxiedAppConfig): |
6 | 8 | name = 'colab.plugins.gitlab' |
7 | 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
... | ... | @@ -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 @@ |
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 | 47 | 'haystack', |
48 | 48 | 'hitcounter', |
49 | 49 | 'taggit', |
50 | - 'djcelery', | |
51 | 50 | |
52 | 51 | # Own apps |
53 | 52 | 'colab.home', |
... | ... | @@ -57,6 +56,7 @@ INSTALLED_APPS = ( |
57 | 56 | 'colab.search', |
58 | 57 | 'colab.tz', |
59 | 58 | 'colab.utils', |
59 | + 'colab.signals', | |
60 | 60 | ) |
61 | 61 | |
62 | 62 | ROOT_URLCONF = 'colab.urls' |
... | ... | @@ -253,8 +253,8 @@ from .utils import conf |
253 | 253 | |
254 | 254 | SOCIAL_NETWORK_ENABLED = locals().get('SOCIAL_NETWORK_ENABLED') or False |
255 | 255 | |
256 | -locals().update(conf.load_colab_apps()) | |
257 | 256 | locals().update(conf.load_py_settings()) |
257 | +locals().update(conf.load_colab_apps()) | |
258 | 258 | |
259 | 259 | COLAB_APPS = locals().get('COLAB_APPS') or {} |
260 | 260 | PROXIED_APPS = {} | ... | ... |
... | ... | @@ -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 @@ |
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 | 2 | import os |
3 | 3 | import sys |
4 | +import logging | |
4 | 5 | import importlib |
5 | 6 | import warnings |
6 | 7 | |
7 | 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 | 16 | class InaccessibleSettings(ImproperlyConfigured): |
11 | 17 | """Settings.py is Inaccessible. |
... | ... | @@ -48,57 +54,71 @@ def _load_py_file(py_path, path): |
48 | 54 | |
49 | 55 | |
50 | 56 | def load_py_settings(): |
51 | - settings_dir = '/etc/colab/settings.d' | |
52 | 57 | settings_file = os.getenv('COLAB_SETTINGS', '/etc/colab/settings.py') |
53 | 58 | settings_module = settings_file.split('.')[-2].split('/')[-1] |
54 | 59 | py_path = "/".join(settings_file.split('/')[:-1]) |
55 | 60 | |
61 | + logger.info('Settings file: %s', settings_file) | |
62 | + | |
56 | 63 | if not os.path.exists(py_path): |
57 | 64 | msg = "The py file {} does not exist".format(py_path) |
58 | 65 | raise InaccessibleSettings(msg) |
59 | 66 | |
60 | 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 | 85 | return py_settings |
72 | 86 | |
73 | 87 | |
74 | 88 | def load_colab_apps(): |
75 | 89 | plugins_dir = os.getenv('COLAB_PLUGINS', '/etc/colab/plugins.d/') |
90 | + logger.info('Plugin settings directory: %s', plugins_dir) | |
76 | 91 | |
77 | 92 | COLAB_APPS = {} |
78 | 93 | |
79 | 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 | 123 | return {'COLAB_APPS': COLAB_APPS} |
104 | 124 | ... | ... |
docs/source/plugindev.rst
1 | 1 | |
2 | -.. _plugin-dev: | |
2 | +.. _plugin-dev: | |
3 | 3 | |
4 | 4 | Plugin Developer Documentation |
5 | 5 | ==================================== |
6 | 6 | |
7 | 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 | 46 | sudo cp $basedir/vagrant/misc/etc/init.d/celeryd /etc/init.d/ |
47 | 47 | sudo cp $basedir/vagrant/misc/etc/default/celeryd /etc/default/ |
48 | 48 | sudo service celeryd start |
49 | + | |
50 | +colab-admin rebuild_index --noinput | ... | ... |