13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/example-project/mycmsproject/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django.db import models
4 |
5 | from djangoplugins.fields import PluginField
6 |
7 | from mycmsproject.plugins import ContentType
8 |
9 |
10 | class Content(models.Model):
11 | title = models.CharField(max_length=255)
12 | content = models.TextField()
13 | plugin = PluginField(ContentType, editable=False)
14 |
15 | def get_absolute_url(self):
16 | return self.plugin.get_plugin().get_read_url(self)
17 |
--------------------------------------------------------------------------------
/example-project/mycmsproject/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for mycmsproject project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/
8 | """
9 | from __future__ import absolute_import
10 |
11 | import os
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mycmsproject.settings")
15 | application = get_wsgi_application()
16 |
--------------------------------------------------------------------------------
/djangoplugins/management/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | # Import cascade to work with a number of Django versions.
4 | try:
5 | from django.db.models.signals import post_migrate
6 | except ImportError:
7 | try:
8 | from django.db.models.signals import post_syncdb as post_migrate
9 | except ImportError:
10 | from south.signals import post_migrate
11 |
12 |
13 | from djangoplugins import models as plugins_app
14 | from .commands.syncplugins import SyncPlugins
15 |
16 |
17 | def sync_plugins(sender, verbosity, **kwargs):
18 | # Different django version have different senders.
19 | if (hasattr(sender, "name") and sender.name == "djangoplugins") or \
20 | (sender == plugins_app):
21 | SyncPlugins(False, verbosity).all()
22 |
23 |
24 | # Plugins must be synced to the database.
25 | post_migrate.connect(sync_plugins)
26 |
--------------------------------------------------------------------------------
/example-project/mycmsproject/plugins.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django.conf.urls import url
4 | from django.core.urlresolvers import reverse
5 |
6 | from djangoplugins.point import PluginPoint
7 |
8 | import mycmsproject.views
9 |
10 |
11 | class ContentType(PluginPoint):
12 | urls = [
13 | url(r'^$', mycmsproject.views.content_list, name='content-list'),
14 | url(r'^create/$', mycmsproject.views.content_create,
15 | name='content-create')
16 | ]
17 |
18 | instance_urls = [
19 | url(r'^$', mycmsproject.views.content_read, name='content-read')
20 | ]
21 |
22 | def get_list_url(self):
23 | return reverse('content-list')
24 |
25 | def get_create_url(self):
26 | return reverse('content-create')
27 |
28 | def get_read_url(self, content):
29 | return reverse('content-read', args=[content.pk])
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## django-plugins
2 |
3 | [](https://travis-ci.org/krischer/django-plugins) || Latest stable version: [](https://badge.fury.io/py/django-plugins)
4 |
5 | `django-plugins` provides functionality for Django apps to make them more
6 | reusable.
7 |
8 | **Home page:** http://pypi.python.org/pypi/django-plugins
9 |
10 | **Documentation:** http://packages.python.org/django-plugins/
11 |
12 | **Source code:** https://github.com/krischer/django-plugins
13 |
14 | ### Installation
15 |
16 | `django-plugins` is currently tested with Python **2.7**, **3.3**, **3.4**, and
17 | **3.5** along with Django versions **1.6**-**1.9**. See the [Travis build
18 | matrix](https://travis-ci.org/krischer/django-plugins) for detailed information
19 | regarding the latest master.
20 |
21 | Installation works via `pip`:
22 |
23 | ```bash
24 | $ pip install django-plugins
25 | ```
26 |
--------------------------------------------------------------------------------
/example-project/mycmsproject/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9.7 on 2016-06-09 19:34
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 | import djangoplugins.fields
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | initial = True
13 |
14 | dependencies = [
15 | ('djangoplugins', '0001_initial'),
16 | ]
17 |
18 | operations = [
19 | migrations.CreateModel(
20 | name='Content',
21 | fields=[
22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23 | ('title', models.CharField(max_length=255)),
24 | ('content', models.TextField()),
25 | ('plugin', djangoplugins.fields.PluginField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='djangoplugins.Plugin')),
26 | ],
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/djangoplugins/templatetags/plugins.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django.template import Library, Node, TemplateSyntaxError
4 |
5 | from ..utils import get_plugin_from_string
6 |
7 | register = Library()
8 |
9 |
10 | class PluginsNode(Node):
11 | def __init__(self, point_name, var_name):
12 | self.plugins = get_plugin_from_string(point_name).get_plugins()
13 | self.var_name = var_name
14 |
15 | def render(self, context):
16 | context[self.var_name] = self.plugins
17 | return ''
18 |
19 |
20 | @register.tag
21 | def get_plugins(parser, token):
22 | contents = token.split_contents()
23 | if len(contents) != 4:
24 | raise TemplateSyntaxError("%r tag requires exactly 3 arguments" %
25 | (contents[0]))
26 | if 'as' != contents[2]:
27 | raise TemplateSyntaxError("%r tag 2nd argument must be 'as'" %
28 | (contents[0]))
29 | return PluginsNode(contents[1], contents[3])
30 |
--------------------------------------------------------------------------------
/example-project/mycmsproject/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | The `urlpatterns` list routes URLs to views. For more information please see:
3 | https://docs.djangoproject.com/en/1.8/topics/http/urls/
4 | Examples:
5 | Function views
6 | 1. Add an import: from my_app import views
7 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
8 | Class-based views
9 | 1. Add an import: from other_app.views import Home
10 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
11 | Including another URLconf
12 | 1. Add an import: from blog import urls as blog_urls
13 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
14 | """
15 | from __future__ import absolute_import
16 |
17 | from django.conf.urls import url
18 |
19 | from djangoplugins.utils import include_plugins
20 | from .plugins import ContentType
21 | from .views import index
22 |
23 | urlpatterns = [
24 | url(r'^$', index, name='index'),
25 | url(r'^content/', include_plugins(ContentType)),
26 | url(r'^content/', include_plugins(
27 | ContentType, '{plugin}/(?P\d+)/', 'instance_urls'
28 | )),
29 | ]
30 |
--------------------------------------------------------------------------------
/docs/templates/layout.html:
--------------------------------------------------------------------------------
1 | {# Import the theme's layout. #}
2 | {% extends "!layout.html" %}
3 |
4 | {# Add fork me on github sign #}
5 | {% block header %}
6 |
7 |
20 |
21 |
22 | Fork me on GitHub
23 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.7"
4 | - "3.3"
5 | - "3.4"
6 | - "3.5"
7 | env:
8 | - DJANGO_VERSION=">=1.6,<1.7"
9 | - DJANGO_VERSION=">=1.7,<1.8"
10 | - DJANGO_VERSION=">=1.8,<1.9"
11 | - DJANGO_VERSION=">=1.9,<1.10"
12 | - DJANGO_VERSION=">=1.10,<1.11"
13 | - DJANGO_VERSION=">=1.11,<2.0"
14 | matrix:
15 | exclude:
16 | # Python 3.5 does not work with some django versions.
17 | - python: "3.5"
18 | env: DJANGO_VERSION=">=1.6,<1.7"
19 | - python: "3.5"
20 | env: DJANGO_VERSION=">=1.7,<1.8"
21 | # Django 1.9+ and Python 3.3 don't work together.
22 | - python: "3.3"
23 | env: DJANGO_VERSION=">=1.9,<1.10"
24 | - python: "3.3"
25 | env: DJANGO_VERSION=">=1.10,<1.11"
26 | - python: "3.3"
27 | env: DJANGO_VERSION=">=1.11,<2.0"
28 |
29 | install:
30 | - pip install "Django${DJANGO_VERSION}"
31 | - pip install .
32 | script:
33 | # First run the tests.
34 | - cd $TRAVIS_BUILD_DIR/example-project; python manage.py test djangoplugins
35 | # Now make sure that migrations don't fail.
36 | - cd $TRAVIS_BUILD_DIR/example-project
37 | # manage.py migrate was not available on older django versions.
38 | - if [ ${DJANGO_VERSION} == ">=1.6,<1.7" ];
39 | then python manage.py syncdb --noinput; else
40 | python manage.py migrate;
41 | fi
42 |
--------------------------------------------------------------------------------
/example-project/mycmsproject/views.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from django.http import HttpResponseRedirect
3 |
4 | from django.shortcuts import render
5 | from django.shortcuts import get_object_or_404
6 |
7 | import mycmsproject
8 |
9 |
10 | def index(request):
11 | return render(request, 'index.html')
12 |
13 |
14 | def content_list(request, plugin):
15 | return render(request, 'content/list.html', {
16 | 'plugin': mycmsproject.plugins.ContentType.get_plugin(plugin),
17 | 'posts': mycmsproject.models.Content.objects.all(),
18 | })
19 |
20 |
21 | def content_create(request, plugin):
22 | # Break circular import
23 | import mycmsproject.forms
24 |
25 | plugin = mycmsproject.plugins.ContentType.get_plugin(plugin)
26 | if request.method == 'POST':
27 | form = mycmsproject.forms.ContentForm(request.POST)
28 | if form.is_valid():
29 | content = form.save(commit=False)
30 | content.plugin = plugin.get_model()
31 | content.save()
32 | return HttpResponseRedirect(content.get_absolute_url())
33 | else:
34 | return "[ERROR] from views: {0}".format(form.errors)
35 | else:
36 | form = mycmsproject.forms.ContentForm()
37 | return render(request, 'content/form.html', {
38 | 'form': form,
39 | })
40 |
41 |
42 | def content_read(request, pk, plugin):
43 | plugin = mycmsproject.plugins.ContentType.get_plugin(plugin)
44 | content = get_object_or_404(mycmsproject.models.Content,
45 | pk=pk, plugin=plugin.get_model())
46 | return render(request, 'content/read.html', {
47 | 'plugin': plugin,
48 | 'content': content,
49 | })
50 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changes
2 | =======
3 |
4 | 0.3.0 (2016-07-06)
5 | ------------------
6 |
7 | - Added support for Django 1.8 and 1.9.
8 | - Now works for Django 1.6-1.9 and Python 2.7, 3.3, 3.4, and 3.5
9 | - Using the `syncdb` Django command is no longer recommended. Please use
10 | `migrate` instead.
11 | - Supports django migrations as well as a backwards compatible path for
12 | south migrations.
13 |
14 | 0.2.5 (2014-10-25)
15 | ------------------
16 |
17 | - Officially supported Django versions are now 1.6.8 and 1.7.1.
18 |
19 |
20 | 0.2.4 (2014-07-04)
21 | ------------------
22 |
23 | - Support for Python 3. Currently Python 2.7, 3.2, 3.3, and 3.4 are officially supported.
24 |
25 |
26 | 0.2.3 (2013-12-22)
27 | ------------------
28 |
29 | - Django 1.6 support, thanks Felipe Ćlvarez for this.
30 |
31 | - Added example-project to show how to use ``django-plugins``.
32 |
33 | - Added possibility for ``include_plugins`` to specify more than one list of
34 | url patterns with possibility to customise inclusion url pattern.
35 |
36 | - ``include_plugins`` now automatically provides ``plugin`` argument to view
37 | functions.
38 |
39 | - Now it is possible to get plugin instance from plugon point like this:
40 | ``MyPluginPoint.get_plugin('plugin-name')``.
41 |
42 |
43 | 0.2.2 (2012-02-08)
44 | ------------------
45 |
46 | - Improved ``PluginPoint.get_model()`` method, now this method also checks if
47 | plugin is enabled.
48 |
49 |
50 | 0.2.1 (2011-08-25)
51 | ------------------
52 |
53 | - Fixed django-plugins setup.py, that was not installable.
54 |
55 | - Fixed plugin fields introspection for south.
56 |
57 |
58 | 0.2 (2011-05-30)
59 | ----------------
60 |
61 | - Plugin points and plugins moved from ``__init__.py`` to ``plugin_points.py``
62 | and ``plugins.py``
63 |
64 | - Improved documentation.
65 |
66 |
67 | 0.1 (2011-01-11)
68 | ----------------
69 |
70 | - First public release.
71 |
--------------------------------------------------------------------------------
/djangoplugins/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import migrations, models
5 | import dirtyfields.dirtyfields
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Plugin',
16 | fields=[
17 | ('id', models.AutoField(verbose_name='ID', serialize=False, primary_key=True, auto_created=True)),
18 | ('pythonpath', models.CharField(unique=True, max_length=255)),
19 | ('name', models.CharField(max_length=255, blank=True, null=True)),
20 | ('title', models.CharField(max_length=255, blank=True, default='')),
21 | ('index', models.IntegerField(default=0)),
22 | ('status', models.SmallIntegerField(choices=[(0, 'Enabled'), (1, 'Disabled'), (2, 'Removed')], default=0)),
23 | ],
24 | options={
25 | 'ordering': ('index', 'id'),
26 | },
27 | bases=(dirtyfields.dirtyfields.DirtyFieldsMixin, models.Model),
28 | ),
29 | migrations.CreateModel(
30 | name='PluginPoint',
31 | fields=[
32 | ('id', models.AutoField(verbose_name='ID', serialize=False, primary_key=True, auto_created=True)),
33 | ('pythonpath', models.CharField(max_length=255)),
34 | ('title', models.CharField(max_length=255)),
35 | ('status', models.SmallIntegerField(choices=[(0, 'Enabled'), (1, 'Disabled'), (2, 'Removed')], default=0)),
36 | ],
37 | ),
38 | migrations.AddField(
39 | model_name='plugin',
40 | name='point',
41 | field=models.ForeignKey(to='djangoplugins.PluginPoint'),
42 | ),
43 | migrations.AlterUniqueTogether(
44 | name='plugin',
45 | unique_together=set([('point', 'name')]),
46 | ),
47 | ]
48 |
--------------------------------------------------------------------------------
/djangoplugins/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django.db import connection
4 | from django.conf import settings
5 | from django.conf.urls import include, url
6 |
7 | from importlib import import_module
8 |
9 |
10 | def get_plugin_name(cls):
11 | return "%s.%s" % (cls.__module__, cls.__name__)
12 |
13 |
14 | def get_plugin_from_string(plugin_name):
15 | """
16 | Returns plugin or plugin point class from given ``plugin_name`` string.
17 |
18 | Example of ``plugin_name``::
19 |
20 | 'my_app.MyPlugin'
21 |
22 | """
23 | modulename, classname = plugin_name.rsplit('.', 1)
24 | module = import_module(modulename)
25 | return getattr(module, classname)
26 |
27 |
28 | def include_plugins(point, pattern=r'{plugin}/', urls='urls'):
29 | pluginurls = []
30 | for plugin in point.get_plugins():
31 | if hasattr(plugin, urls) and hasattr(plugin, 'name'):
32 | _urls = getattr(plugin, urls)
33 | for _url in _urls:
34 | _url.default_args['plugin'] = plugin.name
35 | pluginurls.append(url(
36 | pattern.format(plugin=plugin.name),
37 | include(_urls)
38 | ))
39 | return include(pluginurls)
40 |
41 |
42 | def import_app(app_name):
43 | try:
44 | mod = import_module(app_name)
45 | except ImportError: # Maybe it's AppConfig
46 | parts = app_name.split('.')
47 | tmp_app, app_cfg_name = '.'.join(parts[:-1]), parts[-1]
48 | try:
49 | tmp_app = import_module(tmp_app)
50 | except ImportError:
51 | raise
52 | mod = getattr(tmp_app, app_cfg_name).name
53 | mod = import_module(mod)
54 |
55 | return mod
56 |
57 |
58 | def load_plugins():
59 | for app in settings.INSTALLED_APPS:
60 | try:
61 | import_module('%s.plugins' % app)
62 | except ImportError:
63 | import_app(app)
64 |
65 |
66 | def db_table_exists(table_name):
67 | return table_name in connection.introspection.table_names()
68 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | from setuptools import setup, find_packages
3 |
4 |
5 | def read_docs(filename):
6 | path = os.path.join(os.path.dirname(__file__), filename)
7 | return open(path).read()
8 |
9 | long_description = """
10 | ``django-plugins`` offers functionality to make Django apps them more reusable.
11 |
12 | Originally developed by Mantas Zimnickas (sirexas@gmail.com).
13 |
14 |
15 | Home page
16 | http://pypi.python.org/pypi/django-plugins
17 |
18 | Documentation
19 | http://packages.python.org/django-plugins/
20 |
21 | Source code:
22 | https://github.com/krischer/django-plugins\n\n""".lstrip()
23 |
24 | long_description += read_docs('CHANGES.rst')
25 |
26 | setup(name='django-plugins',
27 | version='0.3.0',
28 | author='Lion Krischer',
29 | author_email='lion.krischer@googlemail.com',
30 | packages=find_packages(exclude=['sample-project']),
31 | install_requires=[
32 | 'django>=1.6',
33 | 'django-dirtyfields<1.3',
34 | ],
35 | url='https://github.com/krischer/django-plugins',
36 | download_url='http://pypi.python.org/pypi/django-plugins',
37 | license='LGPL',
38 | description='django-plugins.',
39 | long_description=long_description,
40 | include_package_data=True,
41 | exclude_package_data={'': ['sample-project']},
42 | zip_safe=False,
43 | classifiers=[
44 | 'Development Status :: 5 - Production/Stable',
45 | 'Environment :: Web Environment',
46 | 'Framework :: Django',
47 | 'Intended Audience :: Developers',
48 | 'License :: OSI Approved :: '
49 | 'GNU Library or Lesser General Public License (LGPL)',
50 | 'Operating System :: OS Independent',
51 | 'Programming Language :: Python',
52 | 'Programming Language :: Python :: 2',
53 | 'Programming Language :: Python :: 2.7',
54 | 'Programming Language :: Python :: 3',
55 | 'Programming Language :: Python :: 3.3',
56 | 'Programming Language :: Python :: 3.4',
57 | 'Programming Language :: Python :: 3.5',
58 | 'Topic :: Software Development :: Libraries :: Python Modules',
59 | ])
60 |
--------------------------------------------------------------------------------
/djangoplugins/fields.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django import forms
4 | from django.db import models
5 |
6 | from .models import Plugin
7 | from .utils import get_plugin_name
8 |
9 |
10 | class PluginField(models.ForeignKey):
11 | def __init__(self, point=None, *args, **kwargs):
12 |
13 | # If not migrating, add a new fields.
14 | if point is not None:
15 | kwargs['limit_choices_to'] = {
16 | 'point__pythonpath': get_plugin_name(point)
17 | }
18 |
19 | super(PluginField, self).__init__(
20 | to=kwargs.pop("to", Plugin), *args, **kwargs)
21 |
22 |
23 | class ManyPluginField(models.ManyToManyField):
24 | def __init__(self, point=None, *args, **kwargs):
25 |
26 | # If not migrating, add a new fields.
27 | if point is not None:
28 | kwargs['limit_choices_to'] = {
29 | 'point__pythonpath': get_plugin_name(point)
30 | }
31 |
32 | super(ManyPluginField, self).__init__(
33 | to=kwargs.pop("to", Plugin), *args, **kwargs)
34 |
35 |
36 | def get_plugins_qs(point):
37 | return point.get_plugins_qs().exclude(name__isnull=True)
38 |
39 |
40 | class PluginChoiceField(forms.ModelChoiceField):
41 | def __init__(self, point, *args, **kwargs):
42 | kwargs['to_field_name'] = 'name'
43 | super(PluginChoiceField, self).\
44 | __init__(queryset=get_plugins_qs(point), **kwargs)
45 |
46 | def to_python(self, value):
47 | value = super(PluginChoiceField, self).to_python(value)
48 | if value:
49 | return value.get_plugin()
50 | else:
51 | return value
52 |
53 |
54 | class PluginMultipleChoiceField(forms.ModelMultipleChoiceField):
55 | def __init__(self, point, *args, **kwargs):
56 | kwargs['to_field_name'] = 'name'
57 | super(PluginMultipleChoiceField, self).\
58 | __init__(queryset=get_plugins_qs(point), **kwargs)
59 |
60 |
61 | class PluginModelChoiceField(forms.ModelChoiceField):
62 | def __init__(self, point, *args, **kwargs):
63 | super(PluginModelChoiceField, self).\
64 | __init__(queryset=get_plugins_qs(point), **kwargs)
65 |
66 |
67 | class PluginModelMultipleChoiceField(forms.ModelMultipleChoiceField):
68 | def __init__(self, point, *args, **kwargs):
69 | super(PluginModelMultipleChoiceField, self).\
70 | __init__(queryset=get_plugins_qs(point), **kwargs)
71 |
--------------------------------------------------------------------------------
/example-project/mycmsproject/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for mycmsproject project.
3 |
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/1.8/topics/settings/
6 |
7 | For the full list of settings and their values, see
8 | https://docs.djangoproject.com/en/1.8/ref/settings/
9 | """
10 |
11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
12 | import os
13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
14 |
15 |
16 | # Quick-start development settings - unsuitable for production
17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
18 |
19 | # SECURITY WARNING: keep the secret key used in production secret!
20 | SECRET_KEY = 'n@-%1v6%&qf_x8u+o8=+o%0152$i&6925)wiwhkcrj1c8t#z=^'
21 |
22 | # SECURITY WARNING: don't run with debug turned on in production!
23 | DEBUG = True
24 |
25 |
26 | ALLOWED_HOSTS = []
27 |
28 |
29 | # Application definition
30 |
31 | INSTALLED_APPS = (
32 | 'django.contrib.admin',
33 | 'django.contrib.auth',
34 | 'django.contrib.contenttypes',
35 | 'django.contrib.sessions',
36 | 'django.contrib.messages',
37 | 'django.contrib.staticfiles',
38 | 'djangoplugins',
39 | 'mycmsproject',
40 | 'mycmsplugin',
41 | )
42 |
43 | MIDDLEWARE_CLASSES = (
44 | 'django.contrib.sessions.middleware.SessionMiddleware',
45 | 'django.middleware.common.CommonMiddleware',
46 | 'django.middleware.csrf.CsrfViewMiddleware',
47 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
48 | 'django.contrib.messages.middleware.MessageMiddleware',
49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
50 | )
51 |
52 | ROOT_URLCONF = 'mycmsproject.urls'
53 |
54 | WSGI_APPLICATION = 'mycmsproject.wsgi.application'
55 |
56 |
57 | # Database
58 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases
59 |
60 | DATABASES = {
61 | 'default': {
62 | 'ENGINE': 'django.db.backends.sqlite3',
63 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
64 | }
65 | }
66 |
67 | # Internationalization
68 | # https://docs.djangoproject.com/en/1.8/topics/i18n/
69 |
70 | LANGUAGE_CODE = 'en-us'
71 |
72 | TIME_ZONE = 'UTC'
73 |
74 | USE_I18N = True
75 |
76 | USE_L10N = True
77 |
78 | USE_TZ = True
79 |
80 |
81 | # Static files (CSS, JavaScript, Images)
82 | # https://docs.djangoproject.com/en/1.8/howto/static-files/
83 |
84 | STATIC_URL = '/static/'
85 |
86 |
87 | TEMPLATES = [
88 | {
89 | "BACKEND": "django.template.backends.django.DjangoTemplates",
90 | "DIRS": ["templates"],
91 | "APP_DIRS": True,
92 | "OPTIONS": {
93 | "context_processors": [
94 | "django.template.context_processors.debug",
95 | "django.template.context_processors.request",
96 | "django.contrib.auth.context_processors.auth",
97 | "django.contrib.messages.context_processors.messages",
98 | ]
99 | },
100 | }
101 | ]
102 |
--------------------------------------------------------------------------------
/djangoplugins/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from dirtyfields import DirtyFieldsMixin
4 | from django.db import models
5 | from django.utils.translation import ugettext_lazy as _
6 | from django.utils.encoding import python_2_unicode_compatible
7 | from djangoplugins.signals import django_plugin_enabled, django_plugin_disabled
8 | from .utils import get_plugin_name, get_plugin_from_string
9 |
10 | ENABLED = 0
11 | DISABLED = 1
12 | REMOVED = 2
13 |
14 | STATUS_CHOICES = (
15 | (ENABLED, _('Enabled')),
16 | (DISABLED, _('Disabled')),
17 | (REMOVED, _('Removed')),
18 | )
19 |
20 | STATUS_CHOICES_ENABLED = (ENABLED,)
21 | STATUS_CHOICES_DISABLED = (DISABLED, REMOVED,)
22 |
23 |
24 | class PluginPointManager(models.Manager):
25 | def get_point(self, point):
26 | return self.get(pythonpath=get_plugin_name(point))
27 |
28 |
29 | @python_2_unicode_compatible
30 | class PluginPoint(models.Model):
31 | pythonpath = models.CharField(max_length=255)
32 | title = models.CharField(max_length=255)
33 | status = models.SmallIntegerField(choices=STATUS_CHOICES, default=ENABLED)
34 |
35 | objects = PluginPointManager()
36 |
37 | def __str__(self):
38 | return self.title
39 |
40 |
41 | class PluginManager(models.Manager):
42 | def get_plugin(self, plugin):
43 | return self.get(pythonpath=get_plugin_name(plugin))
44 |
45 | def get_plugins_of(self, point):
46 | return self.filter(point__pythonpath=get_plugin_name(point),
47 | status=ENABLED)
48 |
49 | def get_by_natural_key(self, name):
50 | return self.get(pythonpath=name)
51 |
52 |
53 | @python_2_unicode_compatible
54 | class Plugin(DirtyFieldsMixin, models.Model):
55 | """
56 | Database representation of a plugin.
57 |
58 | Fields ``name`` and ``title`` are synchronized from plugin classes.
59 |
60 | point
61 | Plugin point.
62 |
63 | pythonpath
64 | Full python path to plugin class, including class too.
65 |
66 | name
67 | Plugin slug name, must be unique within one plugin point.
68 |
69 | title
70 | Eny verbose title of this plugin.
71 |
72 | index
73 | Using values from this field plugins are orderd.
74 |
75 | status
76 | Plugin status.
77 | """
78 | point = models.ForeignKey(PluginPoint)
79 | pythonpath = models.CharField(max_length=255, unique=True)
80 | name = models.CharField(max_length=255, null=True, blank=True)
81 | title = models.CharField(max_length=255, default='', blank=True)
82 | index = models.IntegerField(default=0)
83 | status = models.SmallIntegerField(choices=STATUS_CHOICES, default=ENABLED)
84 |
85 | objects = PluginManager()
86 |
87 | class Meta:
88 | unique_together = (("point", "name"),)
89 | ordering = ('index', 'id')
90 |
91 | def __str__(self):
92 | if self.title:
93 | return self.title
94 | if self.name:
95 | return self.name
96 | return self.pythonpath
97 |
98 | def natural_key(self):
99 | return (self.pythonpath,)
100 |
101 | def is_active(self):
102 | return self.status == ENABLED
103 |
104 | def get_plugin(self):
105 | plugin_class = get_plugin_from_string(self.pythonpath)
106 | return plugin_class()
107 |
108 | def save(self, *args, **kwargs):
109 | if "status" in self.get_dirty_fields().keys() and self.pk:
110 | if self.status in STATUS_CHOICES_ENABLED:
111 | django_plugin_enabled.send(sender=self.__class__,
112 | plugin=self.get_plugin())
113 | else:
114 | django_plugin_disabled.send(sender=self.__class__,
115 | plugin=self.get_plugin())
116 |
117 | return super(Plugin, self).save(*args, **kwargs)
118 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 |
15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
16 |
17 | help:
18 | @echo "Please use \`make ' where is one of"
19 | @echo " html to make standalone HTML files"
20 | @echo " dirhtml to make HTML files named index.html in directories"
21 | @echo " singlehtml to make a single large HTML file"
22 | @echo " pickle to make pickle files"
23 | @echo " json to make JSON files"
24 | @echo " htmlhelp to make HTML files and a HTML help project"
25 | @echo " qthelp to make HTML files and a qthelp project"
26 | @echo " devhelp to make HTML files and a Devhelp project"
27 | @echo " epub to make an epub"
28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
29 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
30 | @echo " text to make text files"
31 | @echo " man to make manual pages"
32 | @echo " changes to make an overview of all changed/added/deprecated items"
33 | @echo " linkcheck to check all external links for integrity"
34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
35 |
36 | clean:
37 | -rm -rf $(BUILDDIR)/*
38 |
39 | html:
40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
41 | @echo
42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
43 |
44 | dirhtml:
45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
46 | @echo
47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
48 |
49 | singlehtml:
50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
51 | @echo
52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
53 |
54 | pickle:
55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
56 | @echo
57 | @echo "Build finished; now you can process the pickle files."
58 |
59 | json:
60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
61 | @echo
62 | @echo "Build finished; now you can process the JSON files."
63 |
64 | htmlhelp:
65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
66 | @echo
67 | @echo "Build finished; now you can run HTML Help Workshop with the" \
68 | ".hhp project file in $(BUILDDIR)/htmlhelp."
69 |
70 | qthelp:
71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
72 | @echo
73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/name.qhcp"
76 | @echo "To view the help file:"
77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/name.qhc"
78 |
79 | devhelp:
80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
81 | @echo
82 | @echo "Build finished."
83 | @echo "To view the help file:"
84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/name"
85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/name"
86 | @echo "# devhelp"
87 |
88 | epub:
89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
90 | @echo
91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
92 |
93 | latex:
94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
95 | @echo
96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
98 | "(use \`make latexpdf' here to do that automatically)."
99 |
100 | latexpdf:
101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
102 | @echo "Running LaTeX files through pdflatex..."
103 | make -C $(BUILDDIR)/latex all-pdf
104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
105 |
106 | text:
107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
108 | @echo
109 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
110 |
111 | man:
112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
113 | @echo
114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
115 |
116 | changes:
117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
118 | @echo
119 | @echo "The overview file is in $(BUILDDIR)/changes."
120 |
121 | linkcheck:
122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
123 | @echo
124 | @echo "Link check complete; look for any errors in the above output " \
125 | "or in $(BUILDDIR)/linkcheck/output.txt."
126 |
127 | doctest:
128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
129 | @echo "Testing of doctests in the sources finished, look at the " \
130 | "results in $(BUILDDIR)/doctest/output.txt."
131 |
--------------------------------------------------------------------------------
/djangoplugins/management/commands/syncplugins.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from optparse import make_option
4 |
5 | from django import VERSION as django_version
6 |
7 | from django.core.management.base import BaseCommand
8 | from django.utils import six
9 |
10 | from djangoplugins.point import PluginMount
11 | from djangoplugins.utils import get_plugin_name, load_plugins, db_table_exists
12 | from djangoplugins.models import Plugin, PluginPoint, REMOVED, ENABLED
13 |
14 |
15 | class Command(BaseCommand):
16 | help = ("Syncs the registered plugins and plugin points with the model "
17 | "versions.")
18 | if django_version <= (1, 8):
19 | option_list = BaseCommand.option_list + (
20 | make_option('--delete',
21 | action='store_true',
22 | dest='delete',
23 | default=False,
24 | help='delete the REMOVED Plugin and PluginPoint '
25 | 'instances.'),
26 | )
27 |
28 | requires_model_validation = True
29 |
30 | def add_arguments(self, parser):
31 | parser.add_argument('--delete',
32 | action='store_true',
33 | dest='delete',
34 | help='delete the REMOVED Plugin and PluginPoint '
35 | 'instances. ')
36 |
37 | def handle(self, *args, **options):
38 | sync = SyncPlugins(options.get('delete'), options.get('verbosity'))
39 | sync.all()
40 |
41 |
42 | class SyncPlugins():
43 | """
44 | In most methods ``src`` and ``dst`` variables are used, they meaning is:
45 |
46 | ``src``
47 | source, registered plugin point objects
48 |
49 | ``dst``
50 | destination, database
51 | """
52 |
53 | def __init__(self, delete_removed=False, verbosity=1):
54 | load_plugins()
55 | self.delete_removed = delete_removed
56 | self.verbosity = int(verbosity)
57 |
58 | def print_(self, verbosity, message):
59 | if self.verbosity >= verbosity:
60 | print(message)
61 |
62 | def get_classes_dict(self, classes):
63 | return dict([(get_plugin_name(i), i) for i in classes])
64 |
65 | def get_instances_dict(self, qs):
66 | return dict((i.pythonpath, i) for i in qs)
67 |
68 | def available(self, src, dst, model):
69 | """
70 | Iterate over all registered plugins or plugin points and prepare to add
71 | them to database.
72 | """
73 | for name, point in six.iteritems(src):
74 | inst = dst.pop(name, None)
75 | if inst is None:
76 | self.print_(1, "Registering %s for %s" % (model.__name__,
77 | name))
78 | inst = model(pythonpath=name)
79 | if inst.status == REMOVED:
80 | self.print_(1, "Updating %s for %s" % (model.__name__, name))
81 | # re-enable a previously removed plugin point and its plugins
82 | inst.status = ENABLED
83 | yield point, inst
84 |
85 | def missing(self, dst):
86 | """
87 | Mark all missing plugins, that exists in database, but are not
88 | registered.
89 | """
90 | for inst in six.itervalues(dst):
91 | if inst.status != REMOVED:
92 | inst.status = REMOVED
93 | inst.save()
94 |
95 | def delete(self, dst):
96 | count = dst.objects.filter(status=REMOVED).count()
97 | if count:
98 | self.print_(1, "Deleting %d Removed %ss" % (count, dst.__name__))
99 | dst.objects.filter(status=REMOVED).delete()
100 |
101 | def points(self):
102 | src = self.get_classes_dict(PluginMount.points)
103 | dst = self.get_instances_dict(PluginPoint.objects.all())
104 |
105 | for point, inst in self.available(src, dst, PluginPoint):
106 | if hasattr(point, '_title'):
107 | inst.title = point._title
108 | else:
109 | inst.title = inst.pythonpath.split('.')[-1]
110 | inst.save()
111 | self.plugins(point, inst)
112 |
113 | self.missing(dst)
114 |
115 | if self.delete_removed:
116 | self.delete(PluginPoint)
117 |
118 | def plugins(self, point, point_inst):
119 | src = self.get_classes_dict(point.plugins)
120 | dst = self.get_instances_dict(point_inst.plugin_set.all())
121 |
122 | for plugin, inst in self.available(src, dst, Plugin):
123 | inst.point = point_inst
124 | inst.name = getattr(plugin, 'name', None)
125 | if hasattr(plugin, 'title'):
126 | inst.title = six.text_type(getattr(plugin, 'title'))
127 | inst.save()
128 |
129 | self.missing(dst)
130 |
131 | def all(self):
132 | """
133 | Synchronize all registered plugins and plugin points to database.
134 | """
135 | # Django >= 1.9 changed something with the migration logic causing
136 | # plugins to be executed before the corresponding database tables
137 | # exist. This method will only return something if the database
138 | # tables have already been created.
139 | # XXX: I don't fully understand the issue and there should be
140 | # another way but this appears to work fine.
141 | if django_version >= (1, 9) and (
142 | not db_table_exists(Plugin._meta.db_table) or
143 | not db_table_exists(PluginPoint._meta.db_table)):
144 | return
145 | self.points()
146 |
--------------------------------------------------------------------------------
/djangoplugins/point.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django import VERSION as django_version
4 | from django.utils.translation import ugettext_lazy as _
5 | from django.core.exceptions import ObjectDoesNotExist
6 | from django.utils import six
7 |
8 | from .models import Plugin, PluginPoint as PluginPointModel, ENABLED
9 | from .utils import get_plugin_name, db_table_exists
10 |
11 |
12 | _PLUGIN_POINT = ""
13 |
14 |
15 | def is_plugin_point(cls):
16 | return repr(cls.__base__) == _PLUGIN_POINT
17 |
18 |
19 | class PluginMount(type):
20 | """
21 | See: http://martyalchin.com/2008/jan/10/simple-plugin-framework/
22 |
23 | """
24 |
25 | points = []
26 |
27 | def __new__(meta, class_name, bases, class_dict):
28 | cls = type.__new__(meta, class_name, bases, class_dict)
29 | if is_plugin_point(cls):
30 | PluginMount.points.append(cls)
31 | return cls
32 |
33 | def __init__(cls, name, bases, attrs):
34 | if is_plugin_point(cls):
35 | # This branch only executes when processing the mount point itself.
36 | # So, since this is a new plugin type, not an implementation, this
37 | # class shouldn't be registered as a plugin. Instead, it sets up a
38 | # list where plugins can be registered later.
39 | cls.plugins = []
40 | elif hasattr(cls, 'plugins'):
41 | # This must be a plugin implementation, which should be registered.
42 | # Simply appending it to the list is all that's needed to keep
43 | # track of it later.
44 | cls.plugins.append(cls)
45 |
46 | DoesNotExist = ObjectDoesNotExist
47 |
48 |
49 | class PluginPoint(six.with_metaclass(PluginMount, object)):
50 | @classmethod
51 | def get_pythonpath(cls):
52 | return get_plugin_name(cls)
53 |
54 | @classmethod
55 | def is_active(cls):
56 | if is_plugin_point(cls):
57 | raise Exception(_('This method is only available to plugin '
58 | 'classes.'))
59 | else:
60 | return cls.get_model().is_active()
61 |
62 | @classmethod
63 | def get_model(cls, name=None, status=ENABLED):
64 | """
65 | Returns model instance of plugin point or plugin, depending from which
66 | class this methos is called.
67 |
68 | Example::
69 |
70 | plugin_model_instance = MyPlugin.get_model()
71 | plugin_model_instance = MyPluginPoint.get_model('plugin-name')
72 | plugin_point_model_instance = MyPluginPoint.get_model()
73 |
74 | """
75 | ppath = cls.get_pythonpath()
76 | if is_plugin_point(cls):
77 | if name is not None:
78 | kwargs = {}
79 | if status is not None:
80 | kwargs['status'] = status
81 | return Plugin.objects.get(point__pythonpath=ppath,
82 | name=name, **kwargs)
83 | else:
84 | return PluginPointModel.objects.get(pythonpath=ppath)
85 | else:
86 | return Plugin.objects.get(pythonpath=ppath)
87 |
88 | @classmethod
89 | def get_plugin(cls, name=None, status=ENABLED):
90 | return cls.get_model(name, status).get_plugin()
91 |
92 | @classmethod
93 | def get_point(cls):
94 | """
95 | Returns plugin point model instance. Only used from plugin classes.
96 | """
97 | if is_plugin_point(cls):
98 | raise Exception(_('This method is only available to plugin '
99 | 'classes.'))
100 | else:
101 | return cls.__base__
102 |
103 | @classmethod
104 | def get_point_model(cls):
105 | """
106 | Returns plugin point model instance. Only used from plugin classes.
107 | """
108 | if is_plugin_point(cls):
109 | raise Exception(_('This method is only available to plugin '
110 | 'classes.'))
111 | else:
112 | return PluginPointModel.objects.\
113 | get(plugin__pythonpath=cls.get_pythonpath())
114 |
115 | @classmethod
116 | def get_plugins(cls):
117 | """
118 | Returns all plugin instances of plugin point, passing all args and
119 | kwargs to plugin constructor.
120 | """
121 | # Django >= 1.9 changed something with the migration logic causing
122 | # plugins to be executed before the corresponding database tables
123 | # exist. This method will only return something if the database
124 | # tables have already been created.
125 | # XXX: I don't fully understand the issue and there should be
126 | # another way but this appears to work fine.
127 | if django_version >= (1, 9) and \
128 | not db_table_exists(Plugin._meta.db_table):
129 | raise StopIteration
130 |
131 | if is_plugin_point(cls):
132 | for plugin_model in cls.get_plugins_qs():
133 | yield plugin_model.get_plugin()
134 | else:
135 | raise Exception(_('This method is only available to plugin point '
136 | 'classes.'))
137 |
138 | @classmethod
139 | def get_plugins_qs(cls):
140 | """
141 | Returns query set of all plugins belonging to plugin point.
142 |
143 | Example::
144 |
145 | for plugin_instance in MyPluginPoint.get_plugins_qs():
146 | print(plugin_instance.get_plugin().name)
147 |
148 | """
149 | if is_plugin_point(cls):
150 | point_pythonpath = cls.get_pythonpath()
151 | return Plugin.objects.filter(point__pythonpath=point_pythonpath,
152 | status=ENABLED).\
153 | order_by('index')
154 | else:
155 | raise Exception(_('This method is only available to plugin point '
156 | 'classes.'))
157 |
158 | @classmethod
159 | def get_name(cls):
160 | if is_plugin_point(cls):
161 | raise Exception(_('This method is only available to plugin '
162 | 'classes.'))
163 | else:
164 | return cls.get_model().name
165 |
166 | @classmethod
167 | def get_title(cls):
168 | if is_plugin_point(cls):
169 | raise Exception(_('This method is only available to plugin '
170 | 'classes.'))
171 | else:
172 | return cls.get_model().title
173 |
--------------------------------------------------------------------------------
/djangoplugins/tests.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from django import forms
4 | from django.test import TestCase
5 | from django.utils.translation import ugettext_lazy as _
6 | from django.utils import six
7 |
8 | from .fields import PluginChoiceField, PluginModelChoiceField, \
9 | PluginModelMultipleChoiceField
10 | from .point import PluginMount, PluginPoint
11 | from .models import Plugin, PluginPoint as PluginPointModel
12 | from .models import ENABLED, DISABLED, REMOVED
13 | from .management.commands.syncplugins import SyncPlugins
14 |
15 |
16 | class MyPluginPoint(PluginPoint):
17 | pass
18 |
19 |
20 | class MyPlugin(MyPluginPoint):
21 | pass
22 |
23 |
24 | class MyPluginFull(MyPluginPoint):
25 | name = 'my-plugin-full'
26 | title = _('My Plugin Full')
27 |
28 |
29 | class MyPlugin2(MyPluginPoint):
30 | name = 'my-plugin-2'
31 | title = _('My Plugin 2')
32 |
33 |
34 | class PluginSyncTestCaseBase(TestCase):
35 | def delete_plugins_from_db(self):
36 | Plugin.objects.all().delete()
37 | PluginPointModel.objects.all().delete()
38 |
39 | def prepate_query_sets(self):
40 | self.points = PluginPointModel.objects.filter(
41 | pythonpath='djangoplugins.tests.MyPluginPoint')
42 | self.plugins = Plugin.objects.filter(
43 | pythonpath='djangoplugins.tests.MyPlugin')
44 |
45 |
46 | class PluginSyncTestCase(PluginSyncTestCaseBase):
47 | def setUp(self):
48 | self.delete_plugins_from_db()
49 | self.prepate_query_sets()
50 |
51 | def test_plugins_not_synced(self):
52 | """
53 | At first there should not be any plugins in database
54 | """
55 | self.assertEqual(self.points.count(), 0)
56 | self.assertEqual(self.plugins.count(), 0)
57 |
58 | def test_plugins_are_synced(self):
59 | """
60 | Now sync plugins and check if they appear in database
61 | """
62 | SyncPlugins(False, 0).all()
63 | self.assertEqual(self.points.filter(status=ENABLED).count(), 1)
64 | self.assertEqual(self.plugins.filter(status=ENABLED).count(), 1)
65 |
66 | def test_plugins_meta(self):
67 | SyncPlugins(False, 0).all()
68 | plugin_model = MyPluginPoint.get_model('my-plugin-full')
69 | self.assertEqual('djangoplugins.tests.MyPluginFull',
70 | plugin_model.pythonpath)
71 |
72 |
73 | class PluginSyncRemovedTestCase(PluginSyncTestCaseBase):
74 | def setUp(self):
75 | self.prepate_query_sets()
76 | self.copy_of_points = PluginMount.points
77 |
78 | def tearDown(self):
79 | PluginMount.points = self.copy_of_points
80 |
81 | def test_removed_plugins(self):
82 | """
83 | Remove all plugin points and sync again, without deleting.
84 | All plugin points (but not plugins) should be marked as REMOVED
85 | """
86 | PluginMount.points = []
87 | SyncPlugins(False, 0).all()
88 | self.assertEqual(self.points.filter(status=REMOVED).count(), 1)
89 | self.assertEqual(self.plugins.filter(status=REMOVED).count(), 0)
90 |
91 | def test_sync_and_delete(self):
92 | """
93 | Sync plugins again, with deleting all removed from database.
94 | Using cascaded deletes, all plugins, that belongs to being deleted
95 | plugin points will be deleted also.
96 | """
97 | PluginMount.points = []
98 | SyncPlugins(True, 0).all()
99 | self.assertEqual(self.points.count(), 0)
100 | self.assertEqual(self.plugins.count(), 0)
101 |
102 |
103 | class PluginModelsTest(TestCase):
104 | def test_plugins_of_point(self):
105 | qs = MyPluginPoint.get_plugins_qs()
106 | self.assertEqual(3, qs.count())
107 |
108 | def test_plugin_model(self):
109 | plugin_name = 'djangoplugins.tests.MyPlugin'
110 | plugin = Plugin.objects.get(pythonpath=plugin_name)
111 | self.assertEqual(plugin_name, six.text_type(plugin))
112 | self.assertEqual((plugin_name,), plugin.natural_key())
113 |
114 | def test_plugin_model_full(self):
115 | plugin_name = 'djangoplugins.tests.MyPluginFull'
116 | plugin = Plugin.objects.get(pythonpath=plugin_name)
117 | self.assertEqual(_('My Plugin Full'), six.text_type(plugin))
118 |
119 | def test_plugin_point_model(self):
120 | point_name = 'djangoplugins.tests.MyPluginPoint'
121 | point = PluginPointModel.objects.get(pythonpath=point_name)
122 | self.assertEqual('MyPluginPoint', six.text_type(point))
123 |
124 | def test_plugins_of_plugin(self):
125 | self.assertRaises(Exception, MyPlugin.get_plugins_qs)
126 |
127 |
128 | class PluginsTest(TestCase):
129 | def test_get_model(self):
130 | point = 'djangoplugins.tests.MyPluginPoint'
131 | plugin = 'djangoplugins.tests.MyPluginFull'
132 |
133 | model = MyPluginFull.get_model()
134 | self.assertEqual(plugin, model.pythonpath)
135 |
136 | model = MyPluginFull().get_model()
137 | self.assertEqual(plugin, model.pythonpath)
138 |
139 | model = MyPluginPoint.get_model('my-plugin-full')
140 | self.assertEqual(plugin, model.pythonpath)
141 |
142 | model = MyPluginPoint.get_model()
143 | self.assertEqual(point, model.pythonpath)
144 |
145 | model = MyPluginPoint().get_model()
146 | self.assertEqual(point, model.pythonpath)
147 |
148 | model = MyPluginFull.get_point_model()
149 | self.assertEqual(point, model.pythonpath)
150 |
151 | self.assertRaises(Exception, MyPluginPoint.get_point_model)
152 |
153 | def test_get_point(self):
154 | point = MyPluginFull.get_point()
155 | self.assertTrue(point is MyPluginPoint)
156 |
157 | self.assertRaises(Exception, MyPluginPoint.get_point)
158 |
159 | def test_get_plugin(self):
160 | model = MyPluginFull.get_model()
161 | plugin = model.get_plugin()
162 | self.assertTrue(isinstance(plugin, MyPluginFull))
163 |
164 | def test_disabled_plugins(self):
165 | self.assertTrue(MyPluginFull.is_active())
166 | self.assertEqual(3, MyPluginPoint.get_plugins_qs().count())
167 | self.assertEqual(3, len(list(MyPluginPoint.get_plugins())))
168 |
169 | model = MyPluginFull.get_model()
170 | model.status = DISABLED
171 | model.save()
172 |
173 | self.assertFalse(MyPluginFull.is_active())
174 | self.assertEqual(2, MyPluginPoint.get_plugins_qs().count())
175 | self.assertEqual(2, len(list(MyPluginPoint.get_plugins())))
176 | self.assertRaises(Plugin.DoesNotExist,
177 | lambda: MyPluginPoint.get_model('my-plugin-full'))
178 |
179 | plugin_model = MyPluginPoint.get_model('my-plugin-full', status=None)
180 | self.assertEqual('my-plugin-full', plugin_model.name)
181 |
182 | def test_get_meta(self):
183 | self.assertEqual('my-plugin-full', MyPluginFull.get_name())
184 | self.assertEqual(_('My Plugin Full'), MyPluginFull.get_title())
185 |
186 | model = MyPluginFull.get_model()
187 | model.name = 'test'
188 | model.save()
189 |
190 | self.assertEqual('test', MyPluginFull.get_name())
191 |
192 |
193 | class MyTestForm(forms.Form):
194 | plugin_choice = PluginChoiceField(MyPluginPoint)
195 | model_choice = PluginModelChoiceField(MyPluginPoint)
196 |
197 | # plugin_multi_choice = PluginMultipleChoiceField(MyPluginPoint)
198 | model_multi_choice = PluginModelMultipleChoiceField(MyPluginPoint)
199 |
200 |
201 | class PluginsFieldsTest(TestCase):
202 | def test_validation(self):
203 | form = MyTestForm({
204 | 'plugin_choice': 'my-plugin-2',
205 | 'model_choice': '%d' % MyPlugin2.get_model().id,
206 | # 'plugin_multi_choice': ['my-plugin-2'],
207 | 'model_multi_choice': ['%d' % MyPlugin2.get_model().id],
208 | })
209 | self.assertTrue(form.is_valid())
210 |
211 | cld = form.cleaned_data
212 | self.assertTrue(isinstance(cld['plugin_choice'], MyPlugin2))
213 | self.assertTrue(isinstance(cld['model_choice'], Plugin))
214 | self.assertTrue(isinstance(cld['model_multi_choice'][0], Plugin))
215 |
--------------------------------------------------------------------------------
/LICENCE.txt:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
167 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # django-plugins documentation build configuration file, created by
4 | # sphinx-quickstart on Tue Nov 9 14:17:09 2010.
5 | #
6 | # This file is execfile()d with the current directory set to its containing
7 | # dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 | import sphinx_bootstrap_theme
15 |
16 |
17 | import os
18 | import sys
19 |
20 | sys.path.insert(0, os.path.abspath('..'))
21 | from django.conf import settings
22 | settings.configure()
23 |
24 |
25 | # If extensions (or modules to document with autodoc) are in another directory,
26 | # add these directories to sys.path here. If the directory is relative to the
27 | # documentation root, use os.path.abspath to make it absolute, like shown here.
28 | # sys.path.insert(0, os.path.abspath('.'))
29 |
30 | # -- General configuration ----------------------------------------------------
31 |
32 | # If your documentation needs a minimal Sphinx version, state it here.
33 | # needs_sphinx = '1.0'
34 |
35 | # Add any Sphinx extension module names here, as strings. They can be
36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
37 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest',
38 | 'sphinx.ext.intersphinx']
39 |
40 | # Add any paths that contain templates here, relative to this directory.
41 | templates_path = ["templates"]
42 |
43 | # The suffix of source filenames.
44 | source_suffix = '.rst'
45 |
46 | # The encoding of source files.
47 | # source_encoding = 'utf-8-sig'
48 |
49 | # The master toctree document.
50 | master_doc = 'index'
51 |
52 | # General information about the project.
53 | project = u'django-plugins'
54 | copyright = u'2010-2016, sirex and krischer'
55 |
56 | # The version info for the project you're documenting, acts as replacement for
57 | # |version| and |release|, also used in various other places throughout the
58 | # built documents.
59 | #
60 | # The short X.Y version.
61 | version = '0.3'
62 | # The full verson, including alpha/beta/rc tags.
63 | release = '0.3.0'
64 |
65 | # The language for content autogenerated by Sphinx. Refer to documentation
66 | # for a list of supported languages.
67 | # language = None
68 |
69 | # There are two options for replacing |today|: either, you set today to some
70 | # non-false value, then it is used:
71 | # today = ''
72 | # Else, today_fmt is used as the format for a strftime call.
73 | # today_fmt = '%B %d, %Y'
74 |
75 | # List of patterns, relative to source directory, that match files and
76 | # directories to ignore when looking for source files.
77 | exclude_patterns = ['build']
78 |
79 | # The reST default role (used for this markup: `text`) to use for all
80 | # documents. default_role = None
81 |
82 | # If true, '()' will be appended to :func: etc. cross-reference text.
83 | # add_function_parentheses = True
84 |
85 | # If true, the current module name will be prepended to all description
86 | # unit titles (such as .. function::).
87 | # add_module_names = True
88 |
89 | # If true, sectionauthor and moduleauthor directives will be shown in the
90 | # output. They are ignored by default.
91 | # show_authors = False
92 |
93 | # The name of the Pygments (syntax highlighting) style to use.
94 | pygments_style = 'sphinx'
95 |
96 | # A list of ignored prefixes for module index sorting.
97 | # modindex_common_prefix = []
98 |
99 |
100 | # -- Options for HTML output --------------------------------------------------
101 |
102 | # The theme to use for HTML and HTML Help pages. See the documentation for
103 | # a list of builtin themes.
104 | html_theme = 'bootstrap'
105 | html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
106 |
107 | # Theme options are theme-specific and customize the look and feel of a
108 | # theme further.
109 | html_theme_options = {
110 | # Navigation bar title. (Default: ``project`` value)
111 | # 'navbar_title': "Demo",
112 |
113 | # Tab name for entire site. (Default: "Site")
114 | # 'navbar_site_name': "Site",
115 |
116 | # Render the next and previous page links in navbar. (Default: true)
117 | 'navbar_sidebarrel': True,
118 |
119 | # Render the current pages TOC in the navbar. (Default: true)
120 | 'navbar_pagenav': True,
121 |
122 | # Global TOC depth for "site" navbar tab. (Default: 1)
123 | # Switching to -1 shows all levels.
124 | 'globaltoc_depth': 2,
125 |
126 | # Include hidden TOCs in Site navbar?
127 | #
128 | # Note: If this is "false", you cannot have mixed ``:hidden:`` and
129 | # non-hidden ``toctree`` directives in the same page, or else the build
130 | # will break.
131 | #
132 | # Values: "true" (default) or "false"
133 | 'globaltoc_includehidden': "true",
134 |
135 | # HTML navbar class (Default: "navbar") to attach to
element.
136 | # For black navbar, do "navbar navbar-inverse"
137 | 'navbar_class': "navbar",
138 |
139 | # Fix navigation bar to top of page?
140 | # Values: "true" (default) or "false"
141 | 'navbar_fixed_top': "true",
142 |
143 | # Location of link to source.
144 | # Options are "nav" (default), "footer" or anything else to exclude.
145 | 'source_link_position': "footer",
146 |
147 | # Bootswatch (http://bootswatch.com/) theme.
148 | #
149 | # Options are nothing with "" (default) or the name of a valid theme
150 | # such as "amelia" or "cosmo".
151 | 'bootswatch_theme': "simplex",
152 |
153 | # Choose Bootstrap version.
154 | # Values: "3" (default) or "2" (in quotes)
155 | 'bootstrap_version': "3",
156 | }
157 |
158 |
159 | # Theme options are theme-specific and customize the look and feel of a theme
160 | # further. For a list of options available for each theme, see the
161 | # documentation.
162 | # html_theme_options = {}
163 |
164 | # Add any paths that contain custom themes here, relative to this directory.
165 | # html_theme_path = []
166 |
167 | # The name for this set of Sphinx documents. If None, it defaults to
168 | # " v documentation".
169 | # html_title = None
170 |
171 | # A shorter title for the navigation bar. Default is the same as html_title.
172 | # html_short_title = None
173 |
174 | # The name of an image file (relative to this directory) to place at the top
175 | # of the sidebar.
176 | # html_logo = None
177 |
178 | # The name of an image file (within the static path) to use as favicon of the
179 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
180 | # pixels large.
181 | # html_favicon = None
182 |
183 | # Add any paths that contain custom static files (such as style sheets) here,
184 | # relative to this directory. They are copied after the builtin static files,
185 | # so a file named "default.css" will overwrite the builtin "default.css".
186 | html_static_path = []
187 |
188 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
189 | # using the given strftime format.
190 | # html_last_updated_fmt = '%b %d, %Y'
191 |
192 | # If true, SmartyPants will be used to convert quotes and dashes to
193 | # typographically correct entities.
194 | # html_use_smartypants = True
195 |
196 | # Custom sidebar templates, maps document names to template names.
197 | # html_sidebars = {}
198 |
199 | # Additional templates that should be rendered to pages, maps page names to
200 | # template names.
201 | # html_additional_pages = {}
202 |
203 | # If false, no module index is generated.
204 | # html_domain_indices = True
205 |
206 | # If false, no index is generated.
207 | # html_use_index = True
208 |
209 | # If true, the index is split into individual pages for each letter.
210 | # html_split_index = False
211 |
212 | # If true, links to the reST sources are added to the pages.
213 | # html_show_sourcelink = True
214 |
215 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
216 | # html_show_sphinx = True
217 |
218 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
219 | # html_show_copyright = True
220 |
221 | # If true, an OpenSearch description file will be output, and all pages will
222 | # contain a tag referring to it. The value of this option must be the
223 | # base URL from which the finished HTML is served.
224 | # html_use_opensearch = ''
225 |
226 | # This is the file name suffix for HTML files (e.g. ".xhtml").
227 | # html_file_suffix = None
228 |
229 | # Output file base name for HTML help builder.
230 | htmlhelp_basename = 'namedoc'
231 |
232 |
233 | # -- Options for LaTeX output -------------------------------------------------
234 |
235 | # The paper size ('letter' or 'a4').
236 | # latex_paper_size = 'letter'
237 |
238 | # The font size ('10pt', '11pt' or '12pt').
239 | # latex_font_size = '10pt'
240 |
241 | # Grouping the document tree into LaTeX files. List of tuples
242 | # (source start file, target name, title, author, documentclass
243 | # [howto/manual]).
244 | latex_documents = [
245 | ('index', 'name.tex', u'django-plugins Documentation',
246 | u'sirex and krischer', 'manual'),
247 | ]
248 |
249 | # The name of an image file (relative to this directory) to place at the top of
250 | # the title page.
251 | # latex_logo = None
252 |
253 | # For "manual" documents, if this is true, then toplevel headings are parts,
254 | # not chapters.
255 | # latex_use_parts = False
256 |
257 | # If true, show page references after internal links.
258 | # latex_show_pagerefs = False
259 |
260 | # If true, show URL addresses after external links.
261 | # latex_show_urls = False
262 |
263 | # Additional stuff for the LaTeX preamble.
264 | # latex_preamble = ''
265 |
266 | # Documents to append as an appendix to all manuals.
267 | # latex_appendices = []
268 |
269 | # If false, no module index is generated.
270 | # latex_domain_indices = True
271 |
272 |
273 | # -- Options for manual page output -------------------------------------------
274 |
275 | # One entry per manual page. List of tuples
276 | # (source start file, name, description, authors, manual section).
277 | man_pages = [
278 | ('index', 'name', u'django-plugins Documentation',
279 | [u'sirex and krischer'], 1),
280 | ]
281 |
282 |
283 | # Example configuration for intersphinx: refer to the Python standard library.
284 | intersphinx_mapping = {'http://docs.python.org/': None}
285 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Documentation for django-plugins
2 | ================================
3 |
4 |
5 | .. contents:: Page Contents
6 | :local:
7 | :depth: 2
8 |
9 |
10 | Introduction
11 | ------------
12 |
13 | ``django-plugins`` will help you make your Django app more reusable. You will
14 | be able to define plugin points, plugins and various ways, how plugins can be
15 | integrated with your base app and extended from other apps providing plugins.
16 |
17 | The idea for ``django-plugins`` was taken from `Marty Alchin blog`_; for a deep
18 | understanding about how this plugin system work, read `Marty Alchin blog`_.
19 |
20 | .. _Marty Alchin blog: http://martyalchin.com/2008/jan/10/simple-plugin-framework/
21 |
22 | **Features**
23 |
24 | - Synchronization with database.
25 | - Plugin management from Django admin.
26 | - Model fields:
27 | - :class:`djangoplugins.fields.PluginField`
28 | - :class:`djangoplugins.fields.ManyPluginField`
29 | - Form fields:
30 | - :class:`djangoplugins.fields.PluginChoiceField`
31 | - :class:`djangoplugins.fields.PluginModelChoiceField`
32 | - :class:`djangoplugins.fields.PluginMultipleChoiceField`
33 | - :class:`djangoplugins.fields.PluginModelMultipleChoiceField`
34 | - Possibility to include plugins to urls.
35 | - Possibility to access plugins from templates.
36 | - Many ways to access plugins and associated models.
37 |
38 | Use case
39 | --------
40 |
41 | ``django-plugins`` can be used in those situations where instances of your
42 | particular model can behave differently.
43 |
44 | For example, you have one ``Node`` model::
45 |
46 | class Node(models.Model):
47 | title = models.CharField(max_length=255)
48 | body = models.TextField()
49 |
50 | This model stores basic information for news, articles and other possible
51 | content types. Each different content type has different forms, different
52 | templates for displaying and listing content.
53 |
54 | To implement all this, you simply can use ``django-plugins``::
55 |
56 | class Node(models.Model):
57 | title = models.CharField(max_length=255)
58 | body = models.TextField()
59 | content_type = PluginField(ContentType)
60 |
61 | Then in your ``views.py`` you do::
62 |
63 | @render_to('create.html')
64 | def node_create(request, plugin):
65 | return {'form': plugin.get_form()}
66 |
67 | @render_to('update.html')
68 | def node_update(request, plugin, node_id):
69 | node = get_object_or_404(Node, pk=node_id)
70 | return {'form': plugin.get_form(instance=node)}
71 |
72 | @render_to()
73 | def node_read(request, node_id):
74 | node = get_object_or_404(Node, pk=node_id)
75 | plugin = node.content_type.get_plugin()
76 | return {
77 | 'TEMPLATE': plugin.get_template(),
78 | 'plugin': plugin,
79 | 'node': node,
80 | }
81 |
82 | How to use it in your app?
83 | --------------------------
84 |
85 | ``django-plugins`` is currently tested with Python **2.7**, **3.2**, **3.3**,
86 | and **3.4** along with Django versions **1.7** and **1.8**. It might well
87 | work with other versions. Always make sure to use the latest minor dot release
88 | of any Django version to enhance security and stability.
89 |
90 | Installation works via ``pip``:
91 |
92 | .. code-block:: bash
93 |
94 | $ pip install django-plugins
95 |
96 |
97 | All plugin points and plugins live in the ``plugins.py`` file in your django
98 | app folder.
99 |
100 | Example how to register a plugin point::
101 |
102 | from djangoplugins.point import PluginPoint
103 |
104 | class MyPluginPoint(PluginPoint):
105 | """
106 | Documentation, that describes how plugins can implement this plugin
107 | point.
108 |
109 | """
110 | pass
111 |
112 |
113 | Example how to register the plugin that implements ``MyPluginPoint``, defined
114 | above::
115 |
116 | class MyPlugin1(MyPluginPoint):
117 | name = 'plugin-1'
118 | title = 'Plugin 1'
119 |
120 | class MyPlugin2(MyPluginPoint):
121 | name = 'plugin-1'
122 | title = 'Plugin 2'
123 |
124 | All plugins must define at least ``name`` and ``title`` attributes. These
125 | properties are used everywhere in plugin system.
126 |
127 | ``name``
128 | This is a slug like name, used in urls and similar places.
129 |
130 | ``title``
131 | Any human readable title for plugin. Value of this attribute will be shown
132 | to users everywhere.
133 |
134 |
135 | Database
136 | --------
137 |
138 | All defined plugins and plugin points are synchronized to database using Django
139 | management command ``syncplugins`` or ``migrate``. ``migrate`` should be always
140 | enough, but some times, if you added or changed plugins code and need to update
141 | those changes to database, but don't want anything more, then you should use
142 | ``syncplugins`` management command.
143 |
144 | When added to database, plugins can be ordered, disabled, accessed from Django
145 | admin, etc.
146 |
147 | ``syncplugins`` command detects if plugins or plugin points where removed from
148 | code and marks them as ``REMOVED``, but leaves them in place. If you want to
149 | clean up your database and really delete all removed plugins us ``--delete``
150 | flag.
151 |
152 | Utilizing available plugins
153 | ---------------------------
154 |
155 | There are many ways how you can use plugins and plugin points. Out of the box
156 | plugins are stored as python objects and synchronized to database called plugin
157 | models.
158 |
159 | Each plugin is linked to one record of :class:`djangoplugins.models.Plugin`
160 | model. Plugins provides all login, plugin models provides all database
161 | possibilities, like sorting, searching, filtering. Combining both we get
162 | powerful plugin system.
163 |
164 | Plugin classes are hardcoded and cannot be modified by users directly. But
165 | users can modify database instances linked to those hardcoded plugins. Thats
166 | why you should always trust database instances, but no hardcoded plugins,
167 | because users can change some thing in database and expects to see those
168 | changes in his web site.
169 |
170 | Plugin and plugin models, both has ``name`` and ``title`` attributes, but you
171 | should always use these attributes from model instances, but not from plugins.
172 |
173 | Here is example to illustrate this::
174 |
175 | BAD:
176 |
177 | plugin = MyPlugin()
178 | print(plugin.title)
179 |
180 | GOOD:
181 |
182 | plugin = MyPlugin()
183 | if plugin.is_active():
184 | print(plugin.get_model().title)
185 |
186 | As you see, in GOOD example, we also check if a plugin is active. Users can
187 | enable or disable plugins using admin. Thats why you should always check if a
188 | plugin is active, before using it. Using methods like ``get_plugins`` and
189 | ``get_plugins_qs`` you will always get only active plugins. So checking if
190 | plugin is active is needed only if you working with particular plugin, bet not
191 | with all plugins of a point.
192 |
193 | ``get_plugins`` method of each plugin point class and plugin point model
194 | instance, returns list of all active plugin instances.
195 |
196 | Example, how to use it::
197 |
198 | from my_app.plugins import MyPluginPoint
199 |
200 | @register.inclusion_tag('templatetags/actions.html', takes_context=True)
201 | def my_plugins(context):
202 | plugins = MyPluginPoint.get_plugins()
203 | return {'plugins': plugins}
204 |
205 | ``templatetags/actions.html``::
206 |
207 |
208 | {% for plugin in plugins %}
209 |
plugin.title
210 | {% endfor %}
211 |
212 |
213 | If you need to sort or filter plugins, you should always access them via Django
214 | ORM::
215 |
216 | from my_app.plugins import MyPluginPoint
217 |
218 | @render_to('my_app/my_template.html')
219 | def my_view(request):
220 | return {
221 | 'plugins': MyPluginPoint.get_plugins_qs().order_by('name')
222 | }
223 |
224 |
225 |
226 | Signals
227 | -------
228 |
229 | There are two registered signals with Django that you might use for hooking
230 | event handlers in case a Django plugin gets disabled or enabled at a certain
231 | point. See the following code snippet for an example usage:
232 |
233 | .. code-block:: python
234 |
235 | from django.dispatch.dispatcher import receiver
236 | from djangoplugins.signals import django_plugin_disabled, django_plugin_enabled
237 |
238 | @receiver(django_plugin_enabled)
239 | def _django_plugin_enabled(sender, plugin, **kwargs):
240 | enable_plugin(plugin)
241 |
242 | @receiver(django_plugin_disabled)
243 | def _django_plugin_disabled(sender, plugin, **kwargs):
244 | disable_plugin(plugin)
245 |
246 |
247 |
248 | Model fields
249 | ------------
250 |
251 | You can tie your models with plugins. Using example below, plugins can be
252 | assigned to model instances::
253 |
254 | from django.db import models
255 | from djangoplugins.fields import PluginField
256 | from my_app.plugins import MyPluginPoint
257 |
258 | class MyModel(models.Model):
259 | plugin = PluginField(MyPluginPoint)
260 |
261 |
262 | Also there is ``ManyPluginField``, for many-to-many relation.
263 |
264 | PluginField
265 | ~~~~~~~~~~~
266 |
267 | .. autoclass:: djangoplugins.fields.PluginField
268 | :members:
269 |
270 |
271 | This field is simply foreign key to ``Plugin`` model.
272 |
273 | Takes one extra required argument:
274 |
275 | .. attribute:: ForeignKey.point
276 |
277 | Plugin point class.
278 |
279 |
280 | ManyPluginField
281 | ~~~~~~~~~~~~~~~
282 |
283 | .. autoclass:: djangoplugins.fields.ManyPluginField
284 | :members:
285 |
286 | Takes one extra required argument, ``point``, as for ``PluginField``.
287 |
288 | Form fields
289 | -----------
290 |
291 | It's easy to put your plugin point to forms using set of plugin fields for
292 | forms::
293 |
294 | from django import forms
295 | from djangoplugins.fields import (
296 | PluginChoiceField, PluginMultipleChoiceField,
297 | PluginModelChoiceField, PluginModelMultipleChoiceField,
298 | )
299 | from my_app.plugins import MyPluginPoint
300 |
301 | class MyForm(forms.Form):
302 | # Two fields below provides simple ChoiceField with choices of plugins.
303 | choice = PluginChoiceField(MyPluginPoint)
304 | # This field currently disabled:
305 | # http://code.djangoproject.com/ticket/9161
306 | #multiple_choice = PluginMultipleChoiceField(MyPluginPoint)
307 |
308 | # These two fields below provides ModelChoiceField with queryset of
309 | # plugis.
310 | model_choice = PluginModelChoiceField(MyPluginPoint)
311 | model_multiple_choice = PluginModelMultipleChoiceField(MyPluginPoint)
312 |
313 | PluginChoiceField
314 | ~~~~~~~~~~~~~~~~~
315 | .. autoclass:: djangoplugins.fields.PluginChoiceField
316 | :members:
317 |
318 | * Default widget: ``Select``
319 | * Empty value: ``''`` (an empty string)
320 | * Normalizes to: Plugin object.
321 | * Validates that the given value is valid plugin name of specified plugin
322 | point.
323 | * Error message keys: ``required``, ``invalid_choice``
324 |
325 | This field can be used, when you want to validate if a string is valid plugin
326 | name and that plugin belongs to specified plugin point.
327 |
328 | Also this field normalizes to plugin object instance, but not to plugin model
329 | instance.
330 |
331 | Takes one extra required argument:
332 |
333 | .. attribute:: PluginChoiceField.point
334 |
335 | Plugin point class.
336 |
337 |
338 | PluginMultipleChoiceField
339 | ~~~~~~~~~~~~~~~~~~~~~~~~~
340 |
341 | .. autoclass:: djangoplugins.fields.PluginMultipleChoiceField
342 | :members:
343 |
344 | * Default widget: ``SelectMultiple``
345 | * Empty value: ``[]`` (an empty list)
346 | * Normalizes to: A list of Plugin objects.
347 | * Validates that every value in the given list of values is valid plugin
348 | name of specified plugin point.
349 | * Error message keys: ``required``, ``invalid_choice``, ``invalid_list``
350 |
351 | Takes one extra required argument, ``point``, as for ``PluginChoiceField``.
352 |
353 | PluginModelChoiceField
354 | ~~~~~~~~~~~~~~~~~~~~~~
355 |
356 | .. autoclass:: djangoplugins.fields.PluginModelChoiceField
357 | :members:
358 |
359 | * Default widget: ``Select``
360 | * Empty value: ``None``
361 | * Normalizes to: A Plugin model instance.
362 | * Validates that the given id is plugin id of specified plugin point.
363 | * Error message keys: ``required``, ``invalid_choice``
364 |
365 | Takes one extra required argument, ``point``, as for ``PluginChoiceField``.
366 |
367 | PluginModelMultipleChoiceField
368 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
369 |
370 | .. autoclass:: djangoplugins.fields.PluginModelMultipleChoiceField
371 | :members:
372 |
373 | * Default widget: ``SelectMultiple``
374 | * Empty value: ``[]`` (an empty list)
375 | * Normalizes to: A list of Plugin model instances.
376 | * Validates that every id in the given list of values is plugin id of
377 | specified plugin point.
378 | * Error message keys: ``required``, ``list``, ``invalid_choice``,
379 | ``invalid_pk_value``
380 |
381 | Takes one extra required argument, ``point``, as for ``PluginChoiceField``.
382 |
383 |
384 | Database Models
385 | ---------------
386 |
387 | `djangoplugins` uses the following database modules:
388 |
389 | .. autoclass:: djangoplugins.models.PluginPoint
390 | :members:
391 |
392 | .. autoclass:: djangoplugins.models.Plugin
393 | :members:
394 |
395 |
396 |
397 | Urls
398 | ----
399 |
400 | ``django-plugins`` has build-in possibility to include urls from plugins. Here
401 | is example how this can be done::
402 |
403 | from django.conf.urls.defaults import patterns
404 | from djangoplugins.utils import include_plugins
405 | from my_app.plugin_points import MyPluginPoint
406 |
407 | urlpatterns = patterns('wora.views',
408 | (r'^plugin/', include_plugins(MyPluginPoint)),
409 | )
410 |
411 | ``include_plugins`` function will search ``get_urls`` and ``name`` attributes
412 | in all plugins, and if both are available, then provided urls will be included.
413 |
414 | Example plugin::
415 |
416 | class MyPluginWithUrls(MyPluginPoint):
417 | name = 'my-plugin'
418 | title = 'My plugin'
419 |
420 | def get_urls(self):
421 | return patterns('my_app.views',
422 | url(r'create/$', 'create', name='my-app-create'),
423 | url(r'read/$', 'read', name='my-app-read'),
424 | url(r'update/$', 'update', name='my-app-update'),
425 | url(r'delete/$', 'delete', name='my-app-delete'),
426 | )
427 |
428 | With this plugin, plugin point inclusion will provide these urls::
429 |
430 | /plugin/my-plugin/create/
431 | /plugin/my-plugin/read/
432 | /plugin/my-plugin/update/
433 | /plugin/my-plugin/delete/
434 |
435 | Plugin points are better place to define urls. Here is example, how all this
436 | can be done::
437 |
438 | class MyPluginPoint(PluginPoint):
439 | def get_urls(self):
440 | return patterns('my_app.views',
441 | url(r'create/$', 'create',
442 | name='my-app-%s-create' % self.name),
443 | )
444 |
445 | class MyPlugin1(MyPluginPoint):
446 | name = 'my-plugin-1'
447 | title = 'My Plugin 1'
448 |
449 | class MyPlugin2(MyPluginPoint):
450 | name = 'my-plugin-2'
451 | title = 'My Plugin 2'
452 |
453 | class MyPlugin3(MyPluginPoint):
454 | name = 'my-plugin-3'
455 | title = 'My Plugin 3'
456 |
457 | From all these plugins, these urls will be available::
458 |
459 | /plugin/my-plugin-1/create/
460 | /plugin/my-plugin-2/create/
461 | /plugin/my-plugin-3/create/
462 |
463 | In templates all these urls can be added using these url names::
464 |
465 | {% url my-app-my-plugin-1-create %}
466 | {% url my-app-my-plugin-2-create %}
467 | {% url my-app-my-plugin-3-create %}
468 |
469 |
470 | Templates
471 | ---------
472 |
473 | You can access your plugins in templates using ``get_plugins`` template tag.::
474 |
475 | {% load plugins %}
476 | {% get_plugins my_app.plugins.MyPluginPoint as plugins %}
477 |
482 |
483 | In example above, ``get_plugins`` returns ordered queryset of plugin models,
484 | but not plugins directly.
485 |
486 | Using plugins with Django ORM
487 | -----------------------------
488 |
489 | It is possible to use plugins with Django ORM.
490 |
491 | If your model has plugin field, you can::
492 |
493 | from my_app.models import MyModel
494 | from my_app.plugins import MyPlugin
495 |
496 | plugin_model = MyPlugin.get_model()
497 |
498 | qs = MyModel.objects.\
499 | filter(name='name', plugin=plugin_model).\
500 | order_by('plugin__order')
501 |
502 | qs = MyModel.objects.filter(plugin__name='email')
503 |
504 | As mentioned above, you can get queryset of all plugins from a plugin point
505 | easily::
506 |
507 | count = MyPluginPoint.get_plugins_qs().count()
508 |
509 | How to get all plugins?
510 | -----------------------
511 |
512 | There are two ways, how you can get all plugins of a plugin point::
513 |
514 | MyPluginPoint.get_plugins()
515 |
516 | and::
517 |
518 | MyPluginPoint.get_plugins_qs()
519 |
520 | First example returns plugins directly in random order. Second example returns
521 | Django queryset with plugin models ordered by ``order`` field.
522 |
523 | How to get model instance of a plugin?
524 | --------------------------------------
525 |
526 | In example below are listed all possible ways, how you can get model instance
527 | of a plugin.
528 |
529 | ::
530 |
531 | plugin = MyPlugin()
532 |
533 | # Get model instance from plugin instance.
534 | plugin_model = plugin.get_model()
535 |
536 | # Get model instance from plugin class.
537 | plugin_model = MyPlugin.get_model()
538 |
539 | # Get model instance by plugin name.
540 | plugin_model = MyPluginPoint.get_model('my-plugin')
541 |
542 | # Get model instance of a plugin point:
543 | plugin_point_model = MyPluginPoint.get_model()
544 |
545 | ``get_model`` method can raise ``ObjectDoesNotExist`` exception, so you should
546 | check it::
547 |
548 | try:
549 | plugin_model = MyPlugin.get_model()
550 | except MyPlugin.DoesNotExist:
551 | plugin_model = None
552 |
553 | How to get plugin from a model instance?
554 | -----------------------------------------
555 |
556 | Easy::
557 |
558 | plugin = plugin_model.get_plugin()
559 |
560 |
561 | Why another plugin system?
562 | --------------------------
563 |
564 | Currently these similar projects exists:
565 |
566 | - django-app-plugins_ - template oriented, pretty complete, but totally
567 | undocumented. Project is not active and bugs are fixed only in forked
568 | repository django-caching-app-plugins_.
569 | - django-plugins_ - template oriented, small project. Plugins are uploaded
570 | through Django admin.
571 |
572 | .. _django-app-plugins: http://code.google.com/p/django-app-plugins/
573 | .. _django-plugins: https://github.com/alex/django-plugins
574 | .. _django-caching-app-plugins: https://bitbucket.org/bkroeze/django-caching-app-plugins/
575 |
576 | Also there is a lot of articles and code snippets, that describes how plugin
577 | system can be implemented. Here is article, that most influenced this project:
578 |
579 | - http://martyalchin.com/2008/jan/10/simple-plugin-framework/
580 |
581 | Also see list of other articles and python plugin system implementations:
582 |
583 | - http://wehart.blogspot.com/2009/01/python-plugin-frameworks.html
584 |
585 | None of these projects fully provides what I need:
586 |
587 | - Good documentation.
588 | - Plugins and plugin points should be provided by Django apps, not only by
589 | single uploaded files.
590 | - Plugins should not be restricted by file names, then can be registered
591 | anywhere, like Django signals.
592 | - Plugins should be synchronized with database, and plugin point can be used as
593 | fields.
594 |
595 |
596 |
--------------------------------------------------------------------------------