├── djangoplugins ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── plugins.py ├── management │ ├── commands │ │ ├── __init__.py │ │ └── syncplugins.py │ └── __init__.py ├── __init__.py ├── apps.py ├── signals.py ├── admin.py ├── utils.py ├── fields.py ├── models.py ├── point.py └── tests.py ├── example-project ├── mycmsproject │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── __init__.py │ ├── templates │ │ ├── base.html │ │ ├── content │ │ │ ├── form.html │ │ │ ├── list.html │ │ │ └── read.html │ │ └── index.html │ ├── forms.py │ ├── models.py │ ├── wsgi.py │ ├── plugins.py │ ├── urls.py │ ├── views.py │ └── settings.py ├── mycmsplugin │ ├── __init__.py │ └── plugins.py ├── manage.py └── Makefile ├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── README.md ├── docs ├── templates │ └── layout.html ├── Makefile ├── conf.py └── index.rst ├── .travis.yml ├── CHANGES.rst ├── setup.py └── LICENCE.txt /djangoplugins/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example-project/mycmsproject/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.rst 3 | include *.txt 4 | -------------------------------------------------------------------------------- /djangoplugins/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /example-project/mycmsplugin/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /example-project/mycmsproject/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /djangoplugins/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /djangoplugins/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | default_app_config = 'djangoplugins.apps.DjangoPluginsConfig' 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/build/html 8 | -------------------------------------------------------------------------------- /djangoplugins/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangoPluginsConfig(AppConfig): 5 | name = 'djangoplugins' 6 | verbose_name = "Django Plugins" 7 | -------------------------------------------------------------------------------- /djangoplugins/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | django_plugin_disabled = Signal(providing_args=["plugin"]) 4 | django_plugin_enabled = Signal(providing_args=["plugin"]) 5 | -------------------------------------------------------------------------------- /example-project/mycmsplugin/plugins.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from mycmsproject.plugins import ContentType 4 | 5 | 6 | class BlogPost(ContentType): 7 | title = 'Blog post' 8 | name = 'blog-post' 9 | -------------------------------------------------------------------------------- /example-project/mycmsproject/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Django plugins demo 5 | 6 | 7 | {% block content %}{% endblock %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | build 4 | dist/ 5 | djangoenv/ 6 | docs/build/ 7 | example-project/bin/ 8 | example-project/db.sqlite3 9 | example-project/include/ 10 | example-project/lib/ 11 | example-project/local/ 12 | example-project/share/ 13 | -------------------------------------------------------------------------------- /example-project/mycmsproject/forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django import forms 4 | 5 | from .models import Content 6 | 7 | 8 | class ContentForm(forms.ModelForm): 9 | class Meta: 10 | model = Content 11 | fields = '__all__' 12 | -------------------------------------------------------------------------------- /example-project/mycmsproject/templates/content/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | Home | 6 |
7 | 8 |
9 | {% csrf_token %} 10 | {{ form.as_p }} 11 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /djangoplugins/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django.contrib import admin 4 | 5 | from .models import Plugin 6 | 7 | 8 | class PluginAdmin(admin.ModelAdmin): 9 | list_display = ('title', 'index', 'status') 10 | list_filter = ('point', 'status') 11 | admin.site.register(Plugin, PluginAdmin) 12 | -------------------------------------------------------------------------------- /example-project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import 3 | 4 | import os 5 | import sys 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mycmsproject.settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /example-project/mycmsproject/templates/content/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | Home | 6 | Create {{ plugin.title }} 7 |
8 | 9 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /example-project/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: bin/python db.sqlite3 3 | 4 | bin/python: 5 | virtualenv --no-site-packages . 6 | cd .. && $(CURDIR)/bin/python setup.py develop 7 | 8 | db.sqlite3: 9 | bin/python manage.py migrate 10 | 11 | .PHONY: run 12 | run: 13 | bin/python manage.py runserver 14 | 15 | 16 | .PHONY: clean 17 | clean: 18 | rm -rf bin build var 19 | find -type f -iname '*.pyc' -exec rm {} + 20 | -------------------------------------------------------------------------------- /example-project/mycmsproject/templates/content/read.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 | Home | 7 | List {{ plugin.title }} | 8 | Create {{ plugin.title }} 9 |
10 | 11 |

{{ content.title }}

12 | 13 |
14 | {{ content.content }} 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /example-project/mycmsproject/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load plugins %} 3 | 4 | 5 | {% block content %} 6 |

Available plugins for Content Type

7 | {% get_plugins mycmsproject.plugins.ContentType as contenttypes %} 8 | 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 | [![Build Status](https://travis-ci.org/krischer/django-plugins.svg?branch=master)](https://travis-ci.org/krischer/django-plugins) || Latest stable version: [![PyPI version](https://badge.fury.io/py/django-plugins.svg)](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 |
    478 | {% for plugin in plugins %} 479 |
  • {{ plugin.title }} {{ plugin.get_plugin.plugin_class_attr }}
  • 480 | {% endfor %} 481 |
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 | --------------------------------------------------------------------------------