Browse Source

Initial files

Marcos Dumay de Medeiros 8 years ago
commit
da5e6578f7
43 changed files with 2504 additions and 0 deletions
  1. 45 0
      .gitignore
  2. 3 0
      MANIFEST.in
  3. 6 0
      README.rst
  4. 6 0
      setup.cfg
  5. 98 0
      setup.py
  6. 0 0
      src/rapid/__init__.py
  7. 306 0
      src/rapid/filters.py
  8. 20 0
      src/rapid/forms.py
  9. 0 0
      src/rapid/migrations/__init__.py
  10. 40 0
      src/rapid/models.py
  11. 65 0
      src/rapid/permissions.py
  12. 176 0
      src/rapid/registry.py
  13. 7 0
      src/rapid/templates/rapid/bare/create.html
  14. 4 0
      src/rapid/templates/rapid/bare/delete.html
  15. 31 0
      src/rapid/templates/rapid/bare/detail.html
  16. 54 0
      src/rapid/templates/rapid/bare/list.html
  17. 95 0
      src/rapid/templates/rapid/bare/select.html
  18. 26 0
      src/rapid/templates/rapid/bare/update.html
  19. 13 0
      src/rapid/templates/rapid/filters/column_selector.html
  20. 1 0
      src/rapid/templates/rapid/filters/icon.html
  21. 2 0
      src/rapid/templates/rapid/filters/key_value.html
  22. 22 0
      src/rapid/templates/rapid/filters/model_filters.html
  23. 51 0
      src/rapid/templates/rapid/filters/register.html
  24. 8 0
      src/rapid/templates/rapid/list/field_header.html
  25. 5 0
      src/rapid/templates/rapid/list/instance_actions.html
  26. 22 0
      src/rapid/templates/rapid/list/pagination.html
  27. 7 0
      src/rapid/templates/rapid/list/show_value.html
  28. 21 0
      src/rapid/templates/rapid/main_window.html
  29. 1 0
      src/rapid/templates/rapid/overlay/call.html
  30. 112 0
      src/rapid/templates/rapid/overlay/register.html
  31. 44 0
      src/rapid/templates/rapid/overlay/text.html
  32. 54 0
      src/rapid/templates/rapid/widgets/multiple-selector.html
  33. 22 0
      src/rapid/templates/rapid/widgets/single-selector.html
  34. 4 0
      src/rapid/templatetags/__init__.py
  35. 61 0
      src/rapid/templatetags/rapid_crud.py
  36. 31 0
      src/rapid/templatetags/rapid_filters.py
  37. 28 0
      src/rapid/templatetags/rapid_list.py
  38. 63 0
      src/rapid/templatetags/rapid_menu.py
  39. 3 0
      src/rapid/tests.py
  40. 27 0
      src/rapid/urls.py
  41. 492 0
      src/rapid/views.py
  42. 106 0
      src/rapid/widgets.py
  43. 322 0
      src/rapid/wrappers.py

+ 45 - 0
.gitignore

@@ -0,0 +1,45 @@
+# Backup files
+*.~
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+bin/
+build/
+develop-eggs/
+dist/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+.tox/
+.coverage
+.cache
+nosetests.xml
+coverage.xml
+
+# Translations
+*.mo
+
+# Sphinx documentation
+docs/_build/
+
+#Virtualenv dir
+ENV/

+ 3 - 0
MANIFEST.in

@@ -0,0 +1,3 @@
+include DESCRIPTION.rst
+graft rapid
+prune rapid/migrations/

+ 6 - 0
README.rst

@@ -0,0 +1,6 @@
+A sample Python project
+=======================
+
+A sample project that exists as an aid to the `Python Packaging User Guide
+<https://packaging.python.org>`_'s `Tutorial on Packaging and Distributing
+Projects <https://packaging.python.org/en/latest/distributing.html>`_.

+ 6 - 0
setup.cfg

@@ -0,0 +1,6 @@
+[bdist_wheel]
+# This flag says that the code is written to work on both Python 2 and Python
+# 3. If at all possible, it is good practice to do this. If you cannot, you
+# will need to generate wheels for each Python version that you support.
+#universal=1
+

+ 98 - 0
setup.py

@@ -0,0 +1,98 @@
+__author__ = 'marcos.medeiros'
+
+"""Rapid-Django installation script
+"""
+
+# Always prefer setuptools over distutils
+from setuptools import setup, find_packages
+# To use a consistent encoding
+from codecs import open
+from os import path
+
+here = path.abspath(path.dirname(__file__))
+
+# Get the long description from the relevant file
+with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
+    long_description = f.read()
+
+setup(
+    name='rapid',
+
+    version='0.0.1.dev1',
+
+    description='Opionated tools for rapid development of enterprise CRUD portals',
+    long_description=long_description,
+
+    # The project's main homepage.
+    url='https://marcosdumay.com/rapid-django',
+
+    # Author details
+    author='Marcos Dumay de Medeiros',
+    author_email='marcos@marcosdumay.com',
+
+    # Choose your license
+    license='MIT',
+
+    # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
+    classifiers=[
+        'Development Status :: 2 - Pre-Alpha',
+        'Environment :: Web Environment',
+        'Framework :: Django :: 1.8',
+        'Intended Audience :: Developers',
+        'Topic :: Software Development :: Build Tools',
+        'License :: OSI Approved :: MIT License',
+        'Natural Language :: Portuguese (Brazilian)',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        #'Programming Language :: Python :: 3',
+        #'Programming Language :: Python :: 3.2',
+        #'Programming Language :: Python :: 3.3',
+        #'Programming Language :: Python :: 3.4',
+    ],
+
+    # What does your project relate to?
+    keywords='CRUD',
+
+    # You can just specify the packages manually here if your project is
+    # simple. Or you can use find_packages().
+    packages=find_packages('src', exclude=['contrib', 'docs', 'tests*']),
+    package_dir = {'': 'src'},
+
+    # List run-time dependencies here.  These will be installed by pip when
+    # your project is installed. For an analysis of "install_requires" vs pip's
+    # requirements files see:
+    # https://packaging.python.org/en/latest/requirements.html
+    install_requires=['django>=1.8'],
+
+    # List additional groups of dependencies here (e.g. development
+    # dependencies). You can install these using the following syntax,
+    # for example:
+    # $ pip install -e .[dev,test]
+    extras_require={},
+
+    # If there are data files included in your packages that need to be
+    # installed, specify them here.  If using Python 2.6 or less, then these
+    # have to be included in MANIFEST.in as well.
+
+    include_package_data=True,
+    # package_data={
+    #     'sample': ['package_data.dat'],
+    # },
+
+    # Although 'package_data' is the preferred approach, in some case you may
+    # need to place data files outside of your packages. See:
+    # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
+    # In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
+    data_files=[],
+
+    # To provide executable scripts, use entry points in preference to the
+    # "scripts" keyword. Entry points provide cross-platform support and allow
+    # pip to create the appropriate form of executable for the target platform.
+    entry_points={
+        # 'console_scripts': [
+        #     'sample=sample:main',
+        # ],
+    },
+)

+ 0 - 0
src/rapid/__init__.py


+ 306 - 0
src/rapid/filters.py

@@ -0,0 +1,306 @@
+# -*- coding: utf-8 -*-
+
+__author__ = 'marcos.medeiros'
+
+from django.db import models
+from django.db.models.fields.related import ForeignObjectRel
+import datetime
+import decimal
+import inspect
+import locale
+
+from django.template import loader, Context
+
+_filter_templates = 'rapid/filters/'
+_filter_url_parameter = 'filter'
+
+class FilterOperator:
+    def __init__(self, display, query, multiple=False):
+        self.display = display
+        self.query = query
+        self.multiple = multiple
+
+
+class FilterValue:
+    available_operations = []
+    selection_template = _filter_templates + 'key_value.html'
+
+    def __init__(self, field, url_value=''):
+        self.value = None
+        self.field = field
+        raise NotImplementedError
+
+    def query_value(self):
+        return self.value
+
+    def url_value(self):
+        return unicode(self.value)
+
+    def operator_from_url(self, url_value):
+        for o in self.available_operations:
+            if unicode(o.query) == unicode(url_value):
+                return o
+        return None
+
+
+class IntegralFilter(FilterValue):
+    available_operations = [
+        FilterOperator('igual a', 'exact'),
+        FilterOperator('maior que', 'gt'),
+        FilterOperator('maior ou igual a', 'gte'),
+        FilterOperator('menor que', 'lt'),
+        FilterOperator('menor ou igual a', 'lte'),
+    ]
+
+    def __init__(self, field, url_value=0):
+        self.value = int(url_value)
+
+
+class BooleanFilter(FilterValue):
+    available_operations = [
+        FilterOperator('valor', 'exact'),
+    ]
+
+    def __init__(self, field, url_value='true'):
+        self.value = bool(url_value)
+
+    def __unicode__(self):
+        return 'true' if self.value else ''
+
+
+class TextFilter(FilterValue):
+    available_operations = [
+        FilterOperator('igual a', 'iexact'),
+        FilterOperator(u'começa com', 'istartswith'),
+        FilterOperator('acaba com', 'iendswith'),
+        FilterOperator('contém', 'icontains'),
+    ]
+
+    def __init__(self, field, url_value=''):
+        self.value = url_value
+
+    def url_value(self):
+        return self.value if self.value else ''
+
+
+class DateTimeFilter(FilterValue):
+    available_operations = [
+        FilterOperator('igual a', 'exact'),
+        FilterOperator('maior que', 'gt'),
+        FilterOperator('maior ou igual a', 'gte'),
+        FilterOperator('menor que', 'lt'),
+        FilterOperator('menor ou igual a', 'lte'),
+    ]
+
+    dateformat = '%Y:%m:%d:%H:%M:%S'
+
+    def __init__(self, field, url_value=None):
+        if url_value:
+            self.value = datetime.datetime.strptime(url_value, self.dateformat)
+        else:
+            self.value = datetime.datetime.now()
+
+    def url_value(self):
+        return self.value.strftime(self.dateformat)
+
+
+class RealFilter(FilterValue):
+    available_operations = [
+        FilterOperator('igual a', 'exact'),
+        FilterOperator('maior que', 'gt'),
+        FilterOperator('maior ou igual a', 'gte'),
+        FilterOperator('menor que', 'lt'),
+        FilterOperator('menor ou igual a', 'lte'),
+    ]
+
+    def __init__(self, field, url_value=0.0):
+        self.value = decimal.Decimal(url_value)
+
+
+class TimeFilter(DateTimeFilter):
+    available_operations = [
+        FilterOperator('igual a', 'exact'),
+        FilterOperator('maior que', 'gt'),
+        FilterOperator('maior ou igual a', 'gte'),
+        FilterOperator('menor que', 'lt'),
+        FilterOperator('menor ou igual a', 'lte'),
+    ]
+
+    dateformat = 'HH:MM:SS'
+
+
+class RelationFilter(FilterValue):
+    available_operations = [
+        FilterOperator('igual a', 'exact'),
+        FilterOperator('na lista', 'in'),
+    ]
+
+    def __init__(self, field, url_value=''):
+        model = field.related_model()
+        self.model = model
+        self.value = [model.default_manager().filter(pk__in=int(v)) for v in url_value.split(':') if v]
+
+    def url_value(self):
+        return ':'.join([str(v.pk) for v in self.value])
+
+
+field_type_dispatcher = {
+    models.AutoField: IntegralFilter,
+    models.BigIntegerField: IntegralFilter,
+    #models.BinaryField:
+    #models.BooleanField: BooleanFilter,
+    models.CharField: TextFilter,
+    #models.CommaSeparatedIntegerField:
+    #models.DateField: DateTimeFilter,
+    #models.DateTimeField: DateTimeFilter,
+    #models.DecimalField: RealFilter,
+    #models.DurationField:TimeFilter,
+    models.EmailField: TextFilter,
+    #models.FileField:
+    #models.FilePathField:
+    models.FloatField: RealFilter,
+    #models.ImageField:
+    models.IntegerField: IntegralFilter,
+    #models.IPAddressField:
+    #models.GenericIPAddressField:
+    #models.NullBooleanField: BooleanFilter,
+    models.PositiveIntegerField: IntegralFilter,
+    models.PositiveSmallIntegerField: IntegralFilter,
+    #models.SlugField:
+    models.SmallIntegerField: IntegralFilter,
+    models.TextField: TextFilter,
+    #models.TimeField: TimeFilter,
+    #models.URLField:
+    #models.UUIDField:
+    #models.ForeignKey: RelationFilter,
+    #models.ManyToManyField: RelationFilter,
+    #models.OneToOneField: RelationFilter,
+    #models.Manager: RelationFilter,
+    #ForeignObjectRel: RelationFilter,
+}
+
+def _get_field_type(field):
+    tt = inspect.getmro(type(field.field))
+    for t in tt:
+        v = field_type_dispatcher.get(t)
+        if v:
+            return v
+    return None
+
+def _get_default_field_value(field):
+    t = _get_field_type(field)
+    if t:
+        return t(field)
+    return None
+
+def _get_field_value(field, url_value):
+    value_type = _get_field_type(field)
+    if value_type:
+        return value_type(field, url_value)
+    return None
+
+class Filter:
+    def __init__(self, field, operator=None, value=None):
+        self.field = field
+        if value:
+            self.value = value
+        else:
+            self.value = _get_default_field_value(field)
+        if operator:
+            self.operator = operator
+        else:
+            self.operator = self.value.available_operations[0]
+
+    def query_dict(self):
+        return self.field.bare_name() + '__' + self.operator.query, self.value.query_value()
+
+    def url_para(self):
+        return self.field.bare_name() + '-' + self.operator.query + '=' + self.value.url_vaue()
+
+    @classmethod
+    def from_request(cls, field, request):
+        for o in _get_field_type(field).available_operations:
+            if request.GET.has_key(field.bare_name() + "-" + o.query):
+                valstr = request.GET[field.bare_name() + "-" + o.query]
+                val = _get_field_value(field, valstr)
+                yield Filter(field, o, val)
+
+    def selection_value_html(self, request):
+        c = Context({
+            'id': self.field.bare_name() + '_' + self.operator.query,
+            'field': self.field,
+            'operator': self.operator,
+            'default_value': self.value.value,
+        })
+        t = loader.get_template(self.value.selection_template)
+        return t.render(c, request)
+
+    @classmethod
+    def selection_type_html(cls, field, request):
+        v = _get_field_type(field)
+        ff = [Filter(field, o) for o in v.available_operations]
+        ss = [(f.operator, f.selection_value_html(request)) for f in ff]
+        act = request.get_full_path()
+        c = Context({
+            'field': field,
+            'operators': v.available_operations,
+            'selectors': ss,
+            'action': act,
+        })
+        t = loader.get_template(_filter_templates + 'column_selector.html')
+        return t.render(c)
+
+
+class FilterSet:
+    def __init__(self, filters=None):
+        self.filters = filters if filters else {}
+
+    def query_dict(self):
+        q = []
+        for ff in self.filters.values():
+            q += [f.query_dict() for f in ff]
+        return dict(q)
+
+    def url_para(self):
+        return ';'.join([f.url_para for f in self.filters])
+
+    @classmethod
+    def from_request(cls, model, request):
+        ff = dict([(f, list(Filter.from_request(f, request))) for f in model.fields() if _get_field_type(f)])
+        return FilterSet(ff)
+
+    @classmethod
+    def can_filter(cls, field):
+        return bool(_get_field_type(field))
+
+    def render_filters(self, request):
+        remove_filter_element = '<a class="rapid-remove-filter"><span class="fa fa-times"></span></a>'
+        ret = ''
+        kk = self.filters.keys()
+        kk.sort(key=lambda f: f.name(), cmp=locale.strcoll)
+        for field in kk:
+            ret += '<div class="rapid-field-filters %s %s">%s\n' % ('visible' if self.filters[field] else 'hidden',
+                                                                    field.bare_name(), field.name().capitalize())
+            for f in self.filters[field]:
+                ret += '<div>' + f.selection_value_html(request) + remove_filter_element + '</div>\n'
+            ret += '</div>'
+        return ret
+
+    def render_selectors(self, request):
+        ret = ''
+        kk = self.filters.keys()
+        kk.sort(key=lambda f: f.name(), cmp=locale.strcoll)
+        for field in kk:
+            ret += '<div class="rapid-filter-selection %s hidden">\n' % field.bare_name()
+            ret += Filter.selection_type_html(field, request)
+            ret += '</div>\n'
+        return ret
+
+    def filters_url(self):
+        pass
+
+    def has_filters(self):
+        for k in self.filters.keys():
+            if self.filters[k]:
+                return True
+        return False

+ 20 - 0
src/rapid/forms.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+
+__author__ = 'marcos.medeiros'
+
+from django import forms
+
+from rapid.models import Profile, Application
+from rapid.wrappers import FieldData
+from rapid.widgets import RapidReadOnly, RapidRelationReadOnly, RapidSelector
+
+class ManageUsers(forms.ModelForm):
+    class Meta:
+        model = Profile
+        fields = '__all__'
+        widgets = {
+            'application': RapidRelationReadOnly(Application),
+            'name': RapidReadOnly(),
+            'description': RapidReadOnly,
+            'users': RapidSelector(FieldData.from_model(Profile, 'users'))
+        }

+ 0 - 0
src/rapid/migrations/__init__.py


+ 40 - 0
src/rapid/models.py

@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+from django.db import models
+from django.contrib.auth.models import User
+
+class CrudModel(models.Model):
+    class Meta:
+        abstract = True
+
+
+class Application(models.Model):
+    name = models.CharField(max_length=60, unique=True, verbose_name=u"nome")
+    python_name = models.CharField(max_length=255, unique=True, verbose_name=u"Nome no Python")
+    managers = models.ManyToManyField(User, verbose_name=u"gestores", related_name='managed_applications')
+    url = models.CharField(max_length=255, unique=True)
+    enabled = models.BooleanField(default=True, verbose_name=u"ativa")
+
+    def __unicode__(self):
+        return self.name
+
+    url_name = 'aplicacao'
+
+    class Meta:
+        verbose_name = u'aplicação'
+        verbose_name_plural = u'aplicações'
+
+
+class Profile(models.Model):
+    id = models.AutoField(primary_key=True)
+    application = models.ForeignKey(Application, verbose_name=u'aplicação')
+    name = models.CharField(max_length=60, verbose_name=u'nome')
+    description = models.TextField(verbose_name=u'descrição')
+    users = models.ManyToManyField(User, verbose_name=u'usuários', blank=True)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = u'perfil'
+        verbose_name_plural = u'perfis'

+ 65 - 0
src/rapid/permissions.py

@@ -0,0 +1,65 @@
+__author__ = 'marcos.medeiros'
+
+from rapid import wrappers
+
+def default_read(model):
+    m = wrappers.ModelData(model)
+
+    def perm(request):
+        if request.user.is_authenticated() and request.user.is_staff():
+            return m.default_manager()
+        return None
+    return perm
+
+def default_write(model):
+    def perm(request):
+        return None
+    return perm
+
+def to_profile(model, profile):
+    m = wrappers.ModelData(model)
+    if hasattr(profile, "__iter__"):
+
+        def perm(request):
+            if not request.user.is_authenticated():
+                return None
+            up = [p.pk for p in request.user.profile_set]
+            for p in up:
+                if p in profile:
+                    return m.default_manager()
+            return None
+        return perm
+
+    def perm(request):
+        if not request.user.is_authenticated():
+            return None
+        up = [p.pk for p in request.user.profile_set]
+        if profile in up:
+            return m.default_manager()
+        return None
+    return perm
+
+def to_staff(model):
+    m = wrappers.ModelData(model)
+
+    def perm(request):
+        if request.user.is_authenticated() and request.user.is_staff():
+            return m.default_manager()
+        return None
+    return perm
+
+def to_all(model):
+    m = wrappers.ModelData(model)
+
+    def perm(request):
+        return m.default_manager()
+    return perm
+
+def to_admins(model):
+    m = wrappers.ModelData(model)
+
+    def perm(request):
+        if request.user.is_authenticated() and request.user.is_admin():
+            return m.default_manager()
+        return None
+    return perm

+ 176 - 0
src/rapid/registry.py

@@ -0,0 +1,176 @@
+from django.core.urlresolvers import reverse
+from rapid.models import Application
+from django.conf.urls import url
+import inspect
+import logging
+from os import path
+
+def _split_all_path(file_name):
+    file_name = path.splitdrive(file_name)[1]
+    p = 'a'
+    while p:
+        file_name, p = path.split(file_name)
+        yield p
+
+def _caller_urls_module():
+    st = inspect.stack()
+    for rec in st:
+        file_name = rec[1]
+        segments = list(_split_all_path(file_name))
+        i = 0
+        for i in xrange(0, len(segments) - 2):
+            p = path.normcase(segments[i])
+            p = path.splitext(p)[0]
+            if p == "urls":
+                return segments[i+1]
+    return None
+
+def _model_name(model):
+    if hasattr(model, "url_name"):
+        return model.url_name
+    return model._meta.verbose_name
+
+class MenuEntry:
+    """
+    The data that goes on a menu item.
+    Model, permissions and url
+    """
+    def __init__(self, model, permission_set, name=None):
+        self.name = name
+        self.model = model
+        self.permission_set = permission_set
+
+    def get_url(self, instance=None):
+        ats = [(x, getattr(instance, x)) for x in self.action.query_parameters]
+        return reverse(self.url, kwargs=dict(ats))
+
+    def __unicode__(self):
+        return u"Menu entry: " + self.name + " -> " + unicode(self.action)
+
+    def __str__(self):
+        return str(unicode(self))
+
+
+class ModuleEntry:
+    """
+    Module data used at menu construction
+    """
+    def __init__(self, python_name, menu_name, base_url):
+        self.python_name = python_name
+        self.menu_name = menu_name
+        self.base_url = url
+        self.models = set()
+
+
+class Action:
+    """
+    An action to be done over a model.
+    Default actions are "list", "view", "edit", "add", "delete", and "select",
+    those are defined at the views module.
+    """
+    def __init__(self, name, url_parameters, query_parameters, view_factory,
+                 verbose_name=None, icon=None, visibility=None):
+        self.name = name
+        self.url_parameters = url_parameters
+        self.query_parameters = query_parameters
+        self.view_factory = view_factory
+        self.verbose_name = verbose_name if verbose_name else name
+        self.icon = icon
+        self.visibility = self.Visibility.details if visibility is None else visibility
+
+    def __unicode__(self):
+        return u"Action: " + self.name
+
+    def __str__(self):
+        return str(unicode(self))
+
+    class Visibility:
+        hidden = 1
+        details = 2
+        list = 3
+
+
+class _Registry:
+    """
+    Registry of URLs, models and views present on the menu
+
+    The registry must:
+        -- for the menu creation
+        list registered modules
+        list registered models
+        list menu entries by module
+        list actions by model
+        reverse url of action and model
+        -- for crud generation
+        query if model is registered
+        list actions by model
+        reverse url of action and model
+    """
+
+    def __init__(self):
+        """
+        Populates the menu registry
+        """
+        self._modules = {}  # ModuleEntry by python_name
+        self._models = {}  # {'action name': MenuEntry} by model class
+        for a in Application.objects.filter(enabled=True):
+            m = ModuleEntry(a.python_name, a.name, a.url)
+            self._modules[a.python_name] = m
+
+    def register_action(self, action, entry, **kwargs):
+        """
+        Registers an action at this registry, so it will appear on the menu
+        and can be reversed at the cruds.
+        :param action: Action type
+        :param entry: The menu entry where it will appear
+        :param kwargs: Arguments (besides model) that'll be passed to the view_factory of the action
+        :return: A Django URL pattern that should be added to the patterns of a urls.py module
+        """
+        from django.contrib.auth.models import User
+        module_name = _caller_urls_module()
+        model = entry.model
+        if not module_name:
+            raise Exception("Unidentified python module registering " + str(model))
+        if not registry._modules.has_key(module_name):
+            logging.error("Module " + module_name + " is not set-up for registering cruds")
+            return None
+
+        module_entry = registry._modules[module_name]
+        module_entry.models.add(model)
+
+        if not entry.name:
+            entry.name = _model_name(model)
+        entry.action = action
+        entry_url = module_entry.menu_name + '_' + entry.name + '_' + action.name
+        entry.url = entry_url
+        model_actions = self._models.get(model, {})
+        if model_actions.has_key(action.name):
+            raise Exception("Action " + action.name + " already registered for model " + str(model))
+        model_actions[action.name] = entry
+        self._models[model] = model_actions
+        return url(r'^/%s/%s/%s$' % (entry.name, action.name, action.url_parameters),
+                   action.view_factory(model=entry.model, **kwargs), name=entry_url)
+
+
+    def get_url_of_action(self, model, action_name, **kwargs):
+        acts = self._models.get(model)
+        if acts and acts.has_key(action_name):
+            return reverse(acts[action_name].url, kwargs=kwargs)
+
+    def modules(self):
+        return self._modules.values()
+
+    def entry_names(self):
+        return [x.menu_name for x in self._modules.values()]
+
+    def is_controlled(self, model):
+        return self._models.has_key(model)
+
+    def model_entry(self, model):
+        return self._models.get(model)
+
+    def module_models(self, module):
+        return self._modules[module].models
+
+
+registry = _Registry()

+ 7 - 0
src/rapid/templates/rapid/bare/create.html

@@ -0,0 +1,7 @@
+<p>{{object_data.model_name.title}}</p>
+
+<form method="POST" action="">
+    {% csrf_token %}
+    {{ form.as_p }}
+    <p><a class="btn btn-default rapid-submit-form">Adicionar</a></p>
+</form>

+ 4 - 0
src/rapid/templates/rapid/bare/delete.html

@@ -0,0 +1,4 @@
+<form method="post" action="">{% csrf_token %}
+    <p>Você tem certeza que deseja apagar "{{ object }}"?</p>
+    <p><a class="btn btn-default rapid-submit-form">Apagar</a></p>
+</form>

+ 31 - 0
src/rapid/templates/rapid/bare/detail.html

@@ -0,0 +1,31 @@
+<span class="must_reload"></span>
+<p>{{object_data.model_name.title}}</p>
+
+<div>
+    {% for f, v, iter in object_data.fields_and_values %}
+        <p><span class="field_label">{{ f.name.capitalize }}:</span>
+        {% if iter %}
+            {% for val, link in v %}
+                {% if link and link.can_read %}
+                    <span class="field_value"><a href="{{ link.view_url }}">{{ val }}</a></span>
+                {% else %}
+                    <span class="field_value">{{ val }}</span>
+                {% endif %}
+            {% endfor %}
+        {% else %}
+            {% if v.1 and v.1.can_read %}
+                <span class="field_value"><a href="{{ v.1.view_url }}">{{ v.0 }}</a></span>
+            {% else %}
+                <span class="field_value">{{ v.0 }}</span>
+            {% endif %}
+        {% endif %}
+        </p>
+    {% endfor %}
+</div>
+{% if object_data.can_write %}
+<div>
+    {% for a, u in object_data.instance_actions %}{% if a.action.name != 'view' %}
+        <a href="{{ u }}" class="btn btn-default"><span class="fa {{ a.action.icon }}">{{ a.action.verbose_name.capitalize }}</span></a>
+    {% endif %}{% endfor %}
+</div>
+{% endif %}

+ 54 - 0
src/rapid/templates/rapid/bare/list.html

@@ -0,0 +1,54 @@
+{% load rapid_list %}<span class="must_reload"></span>
+<style scoped>
+    table.object_list > thead > tr > td{
+        padding-left: 0.5em;
+        padding-right: 0.5em;
+        padding-top: 0.5em;
+        padding-bottom: 0.5em;
+        vertical-align: middle;
+    }
+    table.object_list > tbody > tr > td{
+        padding-left: 0.5em;
+        padding-right: 0.5em;
+        vertical-align: middle;
+    }
+    td.list-icons > *{
+        margin-right: 1em;
+    }
+</style>
+<p>{{model.model_name_plural.title}}</p>
+{% load rapid_filters %}
+{% model_filters model %}
+<table class="object_list table table-striped">
+    <thead><tr>
+        {% for f in view.fields %}
+        {% field_header f %}
+        {% endfor %}
+        <td><!--Actions-->&nbsp;</td>
+    </tr></thead>
+    <tbody>
+    {% for o in view.values %}
+    <tr>
+        {% for v, iter in o.values %}
+        {% if iter %}
+            <td>
+            {% for val, val_data in v %}
+                {% show_value val val_data %}
+                {% if not forloop.last %}<br>{% endif %}
+            {% endfor %}
+            </td>
+        {% else %}
+            <td>{% show_value v.0 v.1 %}</td>
+        {% endif %}
+        {% endfor %}
+        {% instance_actions o %}
+    </tr>
+    </tbody>
+    {% endfor %}
+</table>
+<p>{% for a, u in model.actions %}
+	{% if a.action.name != 'list' %}
+		<a href="{{ u }}" class="better-in-overlay btn btn-default"><span class="fa {{ a.action.icon }}">{{ a.action.verbose_name.capitalize }}</span></a>
+	{% endif %}
+{% endfor %}</p>
+{% pagination %}

+ 95 - 0
src/rapid/templates/rapid/bare/select.html

@@ -0,0 +1,95 @@
+{% load rapid_list %}<span class="must_reload"></span>
+<style scoped>
+    table.object_list > thead > tr > td{
+        padding-left: 0.5em;
+        padding-right: 0.5em;
+        padding-top: 0.5em;
+        padding-bottom: 0.5em;
+        vertical-align: middle;
+    }
+    table.object_list > tbody > tr > td{
+        padding-left: 0.5em;
+        padding-right: 0.5em;
+        vertical-align: middle;
+    }
+    td.list-icons > *{
+        margin-right: 1em;
+    }
+    tr.selectable-row > .selectable-check{
+        font-family: FontAwesome;
+        margin-right: 6pt;
+        font-size: 32pt;
+    }
+    tr.selectable-row:not(.selected) > .selectable-check{
+        zoom: 1;
+        filter: alpha(opacity=0);
+        opacity: 0;
+    }
+    tr.selectable-row.selected{
+        border-radius: 5pt;
+        box-shadow: 0pt 0pt 3pt 5pt #00ff00;
+    }
+</style>
+<p>{{model.model_name_plural.capitalize}}</p>
+{% load rapid_filters %}
+{% model_filters model %}
+<table class="object_list rapid-object-selector table table-striped">
+    <thead><tr class="selectable-head">
+        <td class="selectable-check"><!-- Espaço para um check -->&nbsp;</td>
+        {% for f in view.fields %}
+        {% field_header f %}
+        {% endfor %}
+    </tr></thead>
+    <tbody>
+    {% for o in view.values %}
+    <tr class="selectable-row" id="{{ o.instance.pk }}">
+        <td class="selectable-check"><!-- Espaço para um check -->&#xf00c;
+        <input type="hidden" name="{{ o.instance }}" value="{{ o.instance.pk }}" class="rapid-select-id-marker"></inputhidden></td>
+        {% for v, iter in o.values %}
+        {% if iter %}
+            <td>
+            {% for val, val_data in v %}
+                {{ val }}
+                {% if not forloop.last %}<br>{% endif %}
+            {% endfor %}
+            </td>
+        {% else %}
+            <td>{{ v.0 }}</td>
+        {% endif %}
+        {% endfor %}
+    </tr>
+    </tbody>
+    {% endfor %}
+</table>
+<p><a class="overlay-commit btn btn-default">Adicionar na seleção</a></p>
+<p class="pagination">
+    Página:
+    {% for n,l in pages.start %}
+        <a href="l">{{n}}</a>
+    {% endfor %}
+    {% if pages.separate_start %}
+    .....
+    {% endif %}
+    {% for n,l in pages.before %}
+        <a href="l">{{n}}</a>
+    {% endfor %}
+    {{pages.page}}
+    {% for n,l in pages.after %}
+        <a href="l">{{n}}</a>
+    {% endfor %}
+    {% if pages.separate_end %}
+    .....
+    {% endif %}
+    {% for n,l in pages.end %}
+        <a href="l">{{n}}</a>
+    {% endfor %}
+</p>
+ <script>
+     $(document).ready(function(){
+        $("tr.selectable-row").click(function(){
+            $(this).toggleClass("selected");
+            //$(this).filter(".selected").children(".selectable-check").text("\f00c");
+            //$(this).not(".selected").children(".selectable-check").text(" ");
+        });
+     });
+ </script>

+ 26 - 0
src/rapid/templates/rapid/bare/update.html

@@ -0,0 +1,26 @@
+<p>{{object_data.model_name.title}}</p>
+
+<form method="POST" action="">
+    {% csrf_token %}
+    {{ form.as_p }}
+    {% for field, v, iter in object_data.related_fields_and_values %}
+        <p><span class="field_label">{{ field.name.title }}:</span>
+        {% if iter %}
+            {% for val, link in v %}
+                {% if link %}
+                    <span class="field_value"><a href="{{ link }}">{{ val }}</a></span>
+                {% else %}
+                    <span class="field_value">{{ val }}</span>
+                {% endif %}
+            {% endfor %}
+        {% else %}
+            {% if v.1 %}
+                <span class="field_value"><a href="{{ v.1 }}">{{ v.0 }}</a></span>
+            {% else %}
+                <span class="field_value">{{ v.0 }}</span>
+            {% endif %}
+        {% endif %}
+        </p>
+    {% endfor %}
+    <p><a class="btn btn-default rapid-submit-form">Alterar</a></p>
+</form>

+ 13 - 0
src/rapid/templates/rapid/filters/column_selector.html

@@ -0,0 +1,13 @@
+<div class="rapid-column-filter">
+    <span class="hidden rapid-field-name">{{ field.bare_name }}</span>
+	<p>{{ field.name.capitalize }}</p>
+	<select class="rapid-operator {{ field.bare_name }}">
+        {% for o, s in selectors %}
+        <option class="{{ o.query }}" value="{{ o.query }}">{{ o.display }}</option>
+        {% endfor %}
+    </select>
+	{% for o, s in selectors %}
+		<div class="{{ o.query }} {% if not forloop.first %}hidden{% endif %}">{{ s }}</div>
+	{% endfor %}
+    <a class="rapid-add-filter interaction">Filtrar</a>
+</div>

+ 1 - 0
src/rapid/templates/rapid/filters/icon.html

@@ -0,0 +1 @@
+<a href="" class="rapid-filter"><span class="hidden rapid-filter-field-name">{{ f.bare_name }}</span><span class="fa fa-filter" title="Filtrar"></span></a>

+ 2 - 0
src/rapid/templates/rapid/filters/key_value.html

@@ -0,0 +1,2 @@
+<label for="{{ id }}-filter-value">{{ operator.display }}</label>
+<input type="text" id="{{ id }}-filter-value" name="{{ field.bare_name }}-{{ operator.query }}" value="{{ default_value }}">

+ 22 - 0
src/rapid/templates/rapid/filters/model_filters.html

@@ -0,0 +1,22 @@
+<style scoped>
+    div.rapid-column-filter{
+        position: absolute;
+        border-radius: 15px;
+        box-shadow: 3px 3px 2px #000000;
+        padding-top: 3em;
+        padding-bottom: 3em;
+        padding-left: 2em;
+        padding-right: 2em;
+        background-color: #dddddd;
+        z-index: 200;
+    }
+</style>
+<form class="rapid-filterset">
+    <style scoped>.hidden{display: none;}</style>
+    {{ filters }}
+	<a class="rapid-submit-form btn btn-default {% if not has_filters %}hidden{% endif %}">Atualizar Filtros</a>
+</form>
+<div class="rapid-filter-selection">
+    <style scoped>.hidden{display: none;}</style>
+    {{ selectors }}
+</div>

+ 51 - 0
src/rapid/templates/rapid/filters/register.html

@@ -0,0 +1,51 @@
+<script>
+    $(document).ready(function(){
+        $("body").on("change", "select.rapid-operator", function(){
+            $(this).siblings("div").addClass("hidden");
+            var op = $(this).val();
+            $(this).siblings("div." + op).removeClass("hidden");
+        });
+        $("body").on("click", "a.rapid-add-filter", function(event){
+            event.preventDefault(true);
+            var op = $(this).siblings("select.rapid-operator").val();
+            var field = $(this).siblings("span.rapid-field-name").text();
+            var dv = $(this).siblings("div." + op);
+            var frm = $(this).closest("div.rapid-filter-selection").parent().siblings("form.rapid-filterset")
+            var target = frm.children("div.rapid-field-filters." + field);
+			var dv_clone = dv.clone();
+			//dv_clone.append($("<a>", {"class": "rapid-remove-filter"}).html('<span class="fa fa-times"></span>'));
+            target.append(dv.clone());
+            target.removeClass("hidden");
+            $(this).closest("div.rapid-filter-selection").addClass("hidden");
+            frm.submit();
+        });
+        $("body").on("click", "a.rapid-filter", function(event){
+            event.preventDefault(true);
+            var act = $(this).children(".rapid-filter-field-name").text();
+            var f = $(this).closest("table").siblings("div.rapid-filter-selection").children("div.rapid-filter-selection." + act);
+            var pos = $(this).parent().offset();
+            pos.left -= 10;
+            pos.top += $(this).parent().height() + 10;
+            f.toggleClass("hidden");
+            var w = f.children("div").outerWidth();
+            if(pos.left + w > $(window).width()){
+                pos.left = $(window).width() - w;
+                if(pos.left < 0){
+                    pos.left = 0;
+                }
+            }
+            f.offset(pos);
+        });
+		$("body").on("click", "a.rapid-remove-filter", function(event){
+			event.preventDefault(true);
+			var e = $(this).closest("div");
+			var f = $(this).closest("form");
+			e.remove();
+			f.submit();
+		});
+		$("body").on("click", ".rapid-submit-form", function(event){
+			event.preventDefault(true);
+			$(this).closest("form").submit();
+		});
+    })
+</script>

+ 8 - 0
src/rapid/templates/rapid/list/field_header.html

@@ -0,0 +1,8 @@
+{% load rapid_filters %}
+<td>{{f.name.capitalize}}<br>
+    {% if not f.is_multiple %}
+    <a href="{{ f.view.order_up_url }}"><span class="fa fa-sort-amount-asc" title="Ordem Crescente"></span></a>
+    <a href="{{ f.view.order_down_url }}"><span class="fa fa-sort-amount-desc" title="Ordem Decrescente"></span></a>
+    {% endif %}
+    {% filter_icon f %}
+</td>

+ 5 - 0
src/rapid/templates/rapid/list/instance_actions.html

@@ -0,0 +1,5 @@
+<td class="list-icons">
+    {% for a, u in o.instance_actions %}
+        <a href="{{ u }}" class="better-in-overlay"><span class="fa {{ a.action.icon }}">{{ a.action.verbose_name.capitalize }}</span></a>
+    {% endfor %}
+</td>

+ 22 - 0
src/rapid/templates/rapid/list/pagination.html

@@ -0,0 +1,22 @@
+<p class="pagination">
+    Página:
+    {% for n,l in pages.start %}
+        <a href="l" class="same-overlay">{{n}}</a>
+    {% endfor %}
+    {% if pages.separate_start %}
+    .....
+    {% endif %}
+    {% for n,l in pages.before %}
+        <a href="l" class="same-overlay">{{n}}</a>
+    {% endfor %}
+    {{pages.page}}
+    {% for n,l in pages.after %}
+        <a href="l" class="same-overlay">{{n}}</a>
+    {% endfor %}
+    {% if pages.separate_end %}
+    .....
+    {% endif %}
+    {% for n,l in pages.end %}
+        <a href="l" class="same-overlay">{{n}}</a>
+    {% endfor %}
+</p>

+ 7 - 0
src/rapid/templates/rapid/list/show_value.html

@@ -0,0 +1,7 @@
+{% if val_data and val_data.can_read %}
+    <a href="{{ val_data.view_url }}" class="better-in-overlay">{{ val }}</a>{% for a, u in val_data.list_actions %}
+        <a href={{ u }} class="better-in-overlay"><span class="fa {{ a.action.icon }}" label="{{ a.action.verbose_name.capitalize }}"></span></a>
+    {% endfor %}
+{% else %}
+    {{ val }}
+{% endif %}

+ 21 - 0
src/rapid/templates/rapid/main_window.html

@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<div class="data">
+    <style scoped>
+        a.reload{
+            display: none;
+        }
+    </style>
+    <a class="reload" href="{{ this_url }}"></a>
+    <div class="reload-here">{{ body_text|safe }}</div>
+</div>
+<script>
+    $(document).ready(function(){
+        $("body").on("click", "a.rapid-submit-form", function(event){
+            event.preventDefault();
+            $(this).closest("form").submit();
+        });
+    });
+</script>
+{% endblock %}

+ 1 - 0
src/rapid/templates/rapid/overlay/call.html

@@ -0,0 +1 @@
+show_overlay({{ target_url }}, {{ on_commit }}, {{ on_close }})

+ 112 - 0
src/rapid/templates/rapid/overlay/register.html

@@ -0,0 +1,112 @@
+<script>
+    function position_overlay(target){
+        var win_w = $(window).width();
+        var w = target.width();
+        if(w < win_w){
+            var expected_left = (win_w - w) / 2;
+            var ofs = new Object();
+            ofs.left = expected_left;
+            ofs.top = 50;
+            target.offset(ofs);
+        }else{
+            var ofs = new Object();
+            ofs.left = 5;
+            ofs.top = 30;
+            target.offset(ofs);
+        }
+    }
+    function load_overlay(target, url, on_load, on_commit){
+        $.get(url, {}, function(data){
+            target.html(data);
+            overlay_actions(target, url, on_commit);
+            on_load(target);
+            position_overlay(target.closest("div.overlay"));
+        });
+    }
+    function overlay_actions(obj, target_url, on_commit){
+        var overlay = obj.closest(".overlay");
+        var parent_div = overlay.parent();
+        overlay.find(".overlay-close").click(function(event){
+            event.preventDefault();
+            remove_overlay(parent_div);
+        });
+        overlay.find(".overlay-commit").click(function(event){
+            if(on_commit){
+                on_commit(obj);
+                event.preventDefault();
+                remove_overlay(parent_div);
+            }else{
+                remove_overlay(parent_div);
+            }
+        });
+        obj.find("form").each(function(){
+            var ac = $(this).attr("action");
+            if (!ac){
+                $(this).attr("action", target_url);
+            }
+        });
+        if(on_commit){
+            obj.find('form[method="post"]').submit(function(event){
+                event.preventDefault();
+                on_commit(obj);
+				remove_overlay(parent_div);
+            });
+            obj.find('form:not([method="post"])').ajaxForm(function(data){
+                if(data){
+                    obj.html(data);
+					overlay_actions(obj, target_url, on_commit);
+                }else{
+                    remove_overlay(parent_div);
+                }
+            });
+        }else{
+            obj.find("form").ajaxForm(function(data){
+                if(data){
+                    obj.html(data);
+                }else{
+                    remove_overlay(parent_div);
+                }
+            });
+        }
+    }
+    function remove_overlay(obj){
+        var dt_div = obj.parent().closest("div.data");
+        if(dt_div.children("div").children(".must_reload").length > 0){
+            var reload_url = dt_div.children("a.reload").attr("href");
+            $.get(reload_url, function(data){
+                var o = dt_div.children("div.reload-here");
+                o.html(data);
+                overlay_actions(o, reload_url, false);
+                obj.remove();
+            });
+        }else{
+            obj.remove();
+        }
+    }
+    function show_overlay(target_url, source, on_commit){
+        var overlay_text = "{{ overlay_text }}";
+        var o = $("<div/>").html(overlay_text);
+        load_overlay(o.find(".overlay-data"), target_url, function(){
+            source.closest("div.data").append(o);
+        }, on_commit);
+    }
+    $(document).ready(function(){
+        $("body").on("click", "a.better-in-overlay", function(event){
+            if (!event.isDefaultPrevented()){
+                event.preventDefault();
+                var href=$(this).attr("href");
+                show_overlay(href, $(this), false);
+            }
+        });
+        $("body").on("click", "a:not(.better-in-overlay):not(.overlay-close):not(.overlay-commit):not(.interaction)", function(event){
+            if (!event.isDefaultPrevented()){
+                var href=$(this).attr("href");
+                var p = $(this).closest("div.reload-here")
+                if (p.length > 0){
+                    event.preventDefault();
+                    load_overlay(p, href, function(){}, false);
+                }
+            }
+        });
+    });
+</script>

+ 44 - 0
src/rapid/templates/rapid/overlay/text.html

@@ -0,0 +1,44 @@
+<style scoped>
+    div.darkener{
+        position: fixed;
+        left: 0pt;
+        right: 0pt;
+        top: 0pt;
+        bottom: 0pt;
+        z-index: 10;
+        background-color: RGBA(0, 0, 0, 0.5);
+    }
+    a.reload{
+        display: none;
+    }
+    div.overlay{
+        //position: fixed;
+        background-color: RGBA(255, 255, 255, 1);
+        //margin: auto;
+        //top: 4em;
+        z-index: 11;
+        //width: 90%;
+        left: 5%;
+        //margin-left: -45ex;
+        //overflow: visible;
+        padding: 3ex;
+    }
+    div.overlay-data{
+        //overflow: auto;
+    }
+    div.overlay > a.overlay-close{
+        position: absolute;
+        right: -5pt;
+        top: -5pt;
+        font-size: 20pt;
+        border-radius: 20pt;
+        background-color: RGBA(255, 255, 255, 1);
+    }
+</style>
+
+<div class="darkener"></div>
+<div class="overlay data">
+    <a class="reload" href="#"></a>
+    <a href="#" class="overlay-close fa fa-times-circle-o Fechar"></a>
+    <div class="overlay-data reload-here"></div>
+</div>

+ 54 - 0
src/rapid/templates/rapid/widgets/multiple-selector.html

@@ -0,0 +1,54 @@
+<div class="rapid-select {{ id }} data reload-here">
+    <input type="hidden" id="{{ id }}" name="{{ name }}" value="{{ value }}" {% for k, v in attrs %} {{ k }} = "{{ v }}" {% endfor %}>
+    <ul class="rapid-select-selected">
+        {% for e in selected %}
+        <li name="{{ e.pk }}">{{ e }} <a class="rapid-select-remove interaction"><span class="fa {{ icon }}"></span></a></li>
+        {% endfor %}
+    </ul>
+    <a class="rapid-select-add interaction"><span class="fa fa-plus">Adicionar</span></a>
+</div>
+<script>
+    function add_ids_to_{{ id }}(source){
+        var sel = source.find("table.rapid-object-selector");
+        var markers = sel.children("tbody").children("tr.selected").children("td").children("input.rapid-select-id-marker");
+        var old_ids = [];
+        if($("input#{{ id }}").val()){
+            old_ids = $("input#{{ id }}").val().split(",");
+        }
+        var new_ids = [];
+        var new_names = [];
+        markers.each(function(){
+            if (old_ids.indexOf($(this).val()) < 0){
+                new_ids.push($(this).val());
+                new_names.push($(this).attr("name"));
+            }
+        });
+        $("input#{{ id }}").val(old_ids.concat(new_ids).join(","));
+        var widget_root = $("div.rapid-select.{{ id }}").children("ul.rapid-select-selected")
+        for(i = 0; i < new_names.length; i++){
+            var li = $("<li>", {id: new_ids[i]}).html(
+                new_names[i] + " <a class=\"rapid-select-remove\">" +
+                "<span class=\"fa {{ icon }}\"></span></a>");
+            widget_root.append(li);
+        }
+    };
+    function add_elements_to_{{ id }}(){
+        show_overlay("{{ select_url }}", $("div.rapid-select.{{ id }}").find("a.rapid-select-add"), add_ids_to_{{ id }}, false);
+    };
+    $(document).ready(function(){
+        var d = $("div.rapid-select.{{ id }}")
+        d.on("click", ".rapid-select-remove", function(){
+            var rem_id = $(this).closest("li").attr("name");
+            var old_ids = $("input#{{ id }}").val().split(",");
+            for(i = 0; i < old_ids.length; i++){
+                if(old_ids[i] = rem_id){
+                    old_ids.splice(i, 1);
+                    break;
+                }
+            }
+            $("input#{{ id }}").val(old_ids.join(","));
+            $(this).closest("li").remove();
+        });
+        d.find(".rapid-select-add").click(add_elements_to_{{ id }});
+    });
+</script>

+ 22 - 0
src/rapid/templates/rapid/widgets/single-selector.html

@@ -0,0 +1,22 @@
+<div class="rapid-select {{ id }} data reload-here">
+    <input type="hidden" id="{{ id }}" name="{{ name }}" value="{{ value }}" {% for k, v in attrs %} {{ k }} = "{{ v }}" {% endfor %}>
+    <span class="value-{{ id }}">{{ selected }}</span>
+    <a class="rapid-select-search interaction"><span class="fa {{ icon }}"></span></a></li>
+</div>
+<script>
+    function select_id_in_{{ id }}(source){
+        var sel = source.find("table.rapid-object-selector");
+        var marker = sel.children("tbody").children("tr.selected").children("td").children("input.rapid-select-id-marker");
+        var new_id = marker.val();
+        var new_name = marker.attr("name");
+        $("input#{{ id }}").val(new_id);
+        $("span.value-{{ id }}").text(new_name);
+    };
+    function search_{{ id }}(){
+        show_overlay("{{ select_url }}", $("div.rapid-select.{{ id }}").find("a.rapid-select-search"), select_id_in_{{ id }});
+    };
+    $(document).ready(function(){
+        var d = $("div.rapid-select.{{ id }}");
+        d.find(".rapid-select-search").click(search_{{ id }});
+    });
+</script>

+ 4 - 0
src/rapid/templatetags/__init__.py

@@ -0,0 +1,4 @@
+__author__ = 'marcos.medeiros'
+
+from rapid.views import register_model, register_instance_form, register_simple_select
+import rapid.permissions

+ 61 - 0
src/rapid/templatetags/rapid_crud.py

@@ -0,0 +1,61 @@
+__author__ = 'marcos.medeiros'
+
+from django import template
+from django.template import loader, Context
+import random
+from django.utils.safestring import mark_safe
+
+register = template.Library()
+
+@register.inclusion_tag('rapid/bare/list.html', takes_context=True)
+def crud_list(context):
+    return context
+
+@register.inclusion_tag('rapid/bare/select.html', takes_context=True)
+def crud_select(context):
+    return context
+
+@register.inclusion_tag('rapid/bare/detail.html', takes_context=True)
+def crud_view(context):
+    return context
+
+@register.inclusion_tag('rapid/bare/update.html', takes_context=True)
+def crud_update(context):
+    return context
+
+@register.inclusion_tag('rapid/bare/create.html', takes_context=True)
+def crud_create(context):
+    return context
+
+@register.inclusion_tag('rapid/bare/delete.html', takes_context=True)
+def crud_delete(context):
+    return context
+
+def random_name():
+    s = "abcdefghijklmnopqrustuvwxyz"
+    return "".join([random.choice(s) for x in xrange(30)])
+
+def render_to_javascript_string(template, context={}):
+    t = loader.get_template(template)
+    c = Context(context)
+    str = t.render(c)
+    str = str.replace("\"", "\\\"")
+    str = str.replace("\n", "\\n")
+    return mark_safe(str)
+
+@register.inclusion_tag('rapid/overlay/register.html')
+def register_overlay():
+    overlay_text = render_to_javascript_string('rapid/overlay/text.html')
+    return {'overlay_text': overlay_text}
+
+@register.inclusion_tag('rapid/overlay/call.html')
+def overlay(target_url, on_commit=None, on_close=None):
+    if not on_commit:
+        on_commit = 'function(){}'
+    if not on_close:
+        on_close = 'function(){}'
+    return {
+        'target_url': target_url,
+        'on_commit': on_commit,
+        'on_close': on_close,
+        }

+ 31 - 0
src/rapid/templatetags/rapid_filters.py

@@ -0,0 +1,31 @@
+__author__ = 'marcos.medeiros'
+
+from django import template
+from django.utils.safestring import mark_safe
+from django.template import loader, Context
+from rapid import filters
+
+register = template.Library()
+
+_base = 'rapid/filters/'
+
+@register.inclusion_tag(_base+'model_filters.html', takes_context=True)
+def model_filters(context, model):
+    ff = filters.FilterSet.from_request(model, context.request)
+    return {
+        'filters': mark_safe(ff.render_filters(context.request)),
+        'has_filters': ff.has_filters(),
+        'selectors': mark_safe(ff.render_selectors(context.request)),
+    }
+
+@register.inclusion_tag(_base+'register.html', takes_context=True)
+def register_filters(context):
+    return {}
+
+@register.simple_tag(takes_context=True)
+def filter_icon(context, field):
+    if filters.FilterSet.can_filter(field):
+        c = Context({'f': field})
+        t = loader.get_template(_base+'icon.html')
+        return mark_safe(t.render(c))
+    return ''

+ 28 - 0
src/rapid/templatetags/rapid_list.py

@@ -0,0 +1,28 @@
+__author__ = 'marcos.medeiros'
+
+from django import template
+from django.utils.safestring import mark_safe
+from django.template import loader, Context
+from django.utils.html import escape
+from rapid.views import registry, ModelData
+from rapid import filters
+
+register = template.Library()
+
+_base = 'rapid/list/'
+
+@register.inclusion_tag(_base+'field_header.html')
+def field_header(field):
+    return {'f': field}
+
+@register.inclusion_tag(_base+'pagination.html', takes_context=True)
+def pagination(context):
+    return context
+
+@register.inclusion_tag(_base+'show_value.html')
+def show_value(val, val_data):
+    return {'val': val, 'val_data': val_data}
+
+@register.inclusion_tag(_base+'instance_actions.html')
+def instance_actions(instance):
+    return {'o': instance}

+ 63 - 0
src/rapid/templatetags/rapid_menu.py

@@ -0,0 +1,63 @@
+__author__ = 'marcos.medeiros'
+
+import locale
+from django import template
+from django.utils.safestring import mark_safe
+from django.utils.html import escape
+from rapid.views import registry, ModelData
+
+register = template.Library()
+
+def _app_menu(app, request):
+    models = list(app.models)
+    models.sort(key=lambda m: ModelData(m).model_name(), cmp=locale.strcoll)
+    sub = '<li class="menu-group"><span>%s</span><ul class="submenu">\n' % escape(app.menu_name.capitalize())
+    has_model = False
+    for m in models:
+        st = registry.model_entry(m).get('list')
+        if st:
+            read = st.permission_set(request)
+            if read and read.exists():
+                has_model = True
+                cd = ModelData(st.model)
+                sub += '<li><a href="%s">%s</a></li>\n' % (
+                    registry.get_url_of_action(m, 'list'),
+                    escape(cd.model_name_plural().title()),
+                )
+    sub += '</ul></li>\n'
+    if has_model:
+        return sub
+    return ""
+
+
+
+@register.simple_tag
+def menu(request):
+    ret = u"""
+    <nav id="menu">
+    <style scoped>
+        nav li.menu-group{
+            cursor: pointer;
+        }
+        nav li.menu-group.collapsed > ul{
+            display: none;
+        }
+    </style>
+    """
+    ret += '<ul class="menu">\n'
+    mm = registry.modules()
+    mm.sort(key=lambda a: a.menu_name, cmp=locale.strcoll)
+    for m in mm:
+        ret += _app_menu(m, request)
+    ret += u"""
+    </ul>
+    <script>
+        $(document).ready(function(){
+            $("nav li.menu-group").addClass("collapsed");
+            $("nav li.menu-group > span").click(function(){$(this).parent().toggleClass("collapsed")});
+        });
+    </script>
+    </nav>
+    """
+    return mark_safe(ret)
+

+ 3 - 0
src/rapid/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 27 - 0
src/rapid/urls.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+__author__ = 'marcos.medeiros'
+
+from django.conf.urls import include, url
+from django.contrib.auth.models import User
+
+from rapid.registry import Action
+from rapid.models import Application, Profile
+from rapid.forms import ManageUsers
+
+import rapid
+from rapid import permissions
+
+def _can_manage_users(request):
+    if not request.user.is_authenticated:
+        return None
+    return request.user.application.managed_applications.profile_set
+
+urlpatterns = rapid.register_model(Application, 'aplicacao',
+                             write_set=permissions.to_admin(Application), read_set=permissions.to_all(Application)) +\
+    rapid.register_model(Profile, write_set=permissions.to_admins(Profile), read_set=permissions.to_staff(Profile)) +\
+    rapid.register_instance_form(Profile, 'manage_users', u'Gerenciar Usuários',
+                            ManageUsers, _can_manage_users, "fa-users",
+                            Action.Visibility.list) +\
+    rapid.register_simple_select(User, ['username'], permissions.to_staff(User), 'usuario')
+

+ 492 - 0
src/rapid/views.py

@@ -0,0 +1,492 @@
+# -*- coding: utf-8 -*-
+
+from django.shortcuts import get_object_or_404, redirect
+from django.views import generic
+from django.core.exceptions import PermissionDenied
+from django.template import RequestContext
+from django.utils.http import urlquote_plus
+from django.forms import ModelForm
+from django.template import loader
+from django.http import HttpResponse
+from rapid.registry import registry, Action, MenuEntry
+from rapid.wrappers import InstanceData, ModelData, FieldData
+from rapid import permissions
+from rapid.widgets import RapidSelector, RapidRelationReadOnly
+from rapid.filters import FilterSet
+import math
+
+def _build_uri(request, params):
+    param_string = "&".join(["%s=%s"%(urlquote_plus(k), urlquote_plus(params[k])) for k in params.keys()])
+    base = request.build_absolute_uri().split("?")[0]
+    return base + "?" + param_string
+
+def _copy_dict(dc):
+    d = {}
+    for k in dc.keys():
+        d[k] = dc[k]
+    return d
+
+def _replace_param(request, param_name, param_value):
+    get = _copy_dict(request.GET)
+    get[param_name] = param_value
+    return _build_uri(request, get)
+
+def is_ajax_request(request):
+    # X-Requested-With: XMLHttpRequest
+    w = request.META.get('HTTP_X_REQUESTED_WITH')
+    if w:
+        return True
+    return False
+
+class ListView(generic.list.ListView):
+    template_name = 'rapid/bare/list.html'
+    action_name = ''
+    registers_per_page = 50
+    number_of_edge_pages = 3
+    number_of_middle_pages = 3
+    fields = None
+
+    class Pagination:
+        def __init__(self, request, page, number_of_edge_pages, number_of_middle_pages, registers_per_page, total_pages):
+            self.request = request
+            self.number_of_edge_pages = number_of_edge_pages
+            self.number_of_middle_pages = number_of_middle_pages
+            self.registers_per_page = registers_per_page
+            self.page = page
+            self.total_pages = total_pages
+
+        def _page_and_uri(self, pages):
+            get2 = _copy_dict(self.request.GET)
+            for p in pages:
+                get2['page'] = p
+                yield (p, _build_uri(self.request, get2))
+
+        def start_numbers(self):
+            """
+            Números das páginas que serão listadas no começo da paginação.
+            """
+            e = self.number_of_edge_pages
+            for i in range(1, min(e, self.page - 1)):
+                yield i
+
+        def start(self):
+            """
+            Números e URL páginas que serão listadas no começo da paginação.
+            """
+            return self._page_and_uri(self.start_numbers())
+
+        def before_numbers(self):
+            """
+            Números das páginas que serão listadas na paginação antes da atual.
+            """
+            m = self.number_of_middle_pages
+            for i in range(max(self.page - m, m), self.page - 1):
+                yield i
+
+        def before(self):
+            """
+            Números e URL das páginas que serão listadas na paginação antes da atual.
+            """
+            return self._page_and_uri(self.before_numbers())
+
+        def after_numbers(self):
+            """
+            Números das páginas que serão listadas na paginação depois da atual.
+            """
+            m = self.number_of_middle_pages
+            e = self.number_of_edge_pages
+            for i in range(self.page + 1, min(self.page + m, self.total_pages - e)):
+                yield i
+
+        def after(self):
+            """
+            Números e URL das páginas que serão listadas na paginação depois da atual.
+            """
+            return self._page_and_uri(self.after_numbers())
+
+        def end_numbers(self):
+            """
+            Números das páginas que serão listadas no final da paginação.
+            """
+            e = self.number_of_edge_pages
+            for i in range(max(self.page + 1, self.total_pages - e), self.total_pages):
+                yield i
+
+        def end(self):
+            """
+            Números e URLs das páginas que serão listadas no final da paginação.
+            """
+            return self._page_and_uri(self.end_numbers())
+
+        def separate_end(self):
+            """
+            Indica se deve haver um separador entre o começo da paginação e as páginas antes da atual.
+            """
+            return self.page < self.total_pages - self.number_of_edge_pages - self.number_of_middle_pages
+
+        def separate_start(self):
+            """
+            Indica se deve haver um separador entre as páginas depois da atual e o fim da paginação.
+            """
+            return self.page > self.number_of_edge_pages + self.number_of_middle_pages
+
+    class View:
+        order_param = 'order'
+
+        def __init__(self, request, action_name, model, queryset, fields):
+            self.request = request
+            self.model = model
+            self.action_name = action_name
+            self._fields = fields
+
+            q = queryset
+
+            self.filters = FilterSet.from_request(ModelData(self.model), request)
+            if self.filters:
+                q = q.filter(**self.filters.query_dict())
+                self.queryset = q
+
+            order = request.GET.get(self.order_param)
+            if order:
+                q = q.order_by(order)
+            self.queryset = q
+
+        def values(self):
+            for o in self.queryset:
+                if self._fields:
+                    c = InstanceData(o, request=self.request, fields=self._fields)
+                else:
+                    c = InstanceData(o, request=self.request)
+                if c.has_permission(self.request, self.action_name):
+                    yield c
+
+        def fields(self):
+            order_param = self.order_param
+            request = self.request
+
+            class FieldParams:
+                def __init__(self, field):
+                    self.field = field
+
+                def order_up_url(self):
+                    return _replace_param(request, order_param, self.field.bare_name())
+
+                def order_down_url(self):
+                    return _replace_param(request, order_param, u'-' + self.field.bare_name())
+
+                def add_filter_url(self):
+                    pass
+
+                def del_filter_url(self):
+                    pass
+            for f in ModelData(self.model, request=self.request, fields=self._fields).fields():
+                f.view = FieldParams(f)
+                yield f
+
+    def get(self, request, **kwargs):
+        mdata = ModelData(self.model, request)
+        if not mdata.has_permission(request, self.action_name):
+            raise PermissionDenied
+        #De forma similar ao get de ListView, recupera os objetos e verifica
+        #se pode mostrar uma lista vazia.
+        object_list = self.get_queryset()
+        allow_empty = self.get_allow_empty()
+
+        # Agora, popula o contexto com os request processors e os dados específicos
+        # da listagem nos SA.
+        context = RequestContext(request).flatten()
+        context.update(kwargs)
+        total_pages = int(math.ceil(object_list.count() / self.registers_per_page)) + 1
+        page = int(request.GET.get('page', 1))
+        p = self.Pagination(request, page, self.number_of_edge_pages,
+                                      self.number_of_middle_pages, self.registers_per_page,
+                                      total_pages)
+        context['pages'] = p
+        context['model'] = ModelData(self.model, request, fields=self.fields)
+        context['view'] = self.View(request, self.action_name, self.model, object_list, self.fields)
+        default_ordering = self.model._meta.ordering if hasattr(self.model._meta, 'ordering') else ''
+        ordering = request.GET.get('order', default_ordering)
+        offset = (page - 1) * self.registers_per_page
+        self.object_list = object_list[offset: offset + self.registers_per_page]
+        if len(self.object_list) > 0:
+            context['has_objects'] = True
+            context['first_object'] = self.object_list[0]
+        else:
+            context['has_objects'] = False
+        context[self.get_context_object_name(object_list)] = object_list
+        context['object_list'] = object_list
+        context = self.get_context_data(**context)
+        self.object_list = object_list
+
+        return self.render_to_response(context)
+
+class ReadView(generic.detail.DetailView):
+    template_name = 'rapid/bare/detail.html'
+    action_name = ''
+    request = None
+
+    def get_object(self, request=None):
+        return get_object_or_404(self.model, pk=self.kwargs['pk'])
+
+    def get(self, request, pk, **kwargs):
+        context = RequestContext(request).flatten()
+        context.update(kwargs)
+
+        obj = self.get_object(self.request)
+        context['object'] = obj
+        context[self.get_context_object_name(obj)] = obj
+
+        cd = InstanceData(obj, request=request)
+        if not cd.has_permission(request, self.action_name):
+            raise PermissionDenied
+        excludes = request.GET.get('exclude')
+        if excludes:
+            cd.excludes = excludes.split(",")
+        self.object = cd
+        context['object_data'] = cd
+
+        context = self.get_context_data(**context)
+
+        return self.render_to_response(context)
+
+def _get_form(request, model):
+    default_relations_bare = request.GET.get('default')
+    widgets = []
+    default_relations = []
+    default_relations_fields = []
+    if default_relations_bare:
+        default_relations_fields = default_relations_bare.split(",")
+        default_relations = [(x, int(y)) for (x, y) in (f.split(":") for f in default_relations_fields)]
+        default_relations_fields = [x for x, y in default_relations]
+        for (x, y) in default_relations:
+            f = FieldData(getattr(model, x).field, request)
+            widgets.append((x, RapidRelationReadOnly(f.related_model())))
+    ask_relations = []
+    for f in ModelData(model).local_fields():
+        if f.is_relation() and unicode(f.bare_name()) not in default_relations_fields:
+            ask_relations.append(f)
+    widgets += [(f.bare_name(), RapidSelector(f)) for f in ask_relations if f.related_model().is_controlled()]
+    #ModelForm.Meta tem atributos com esses mesmos nomes,
+    #então eu tenho que renomear.
+    form_model = model
+    form_widgets = dict(widgets)
+
+    class CForm(ModelForm):
+        def __init__(self, *args, **kwargs):
+            initial = kwargs.get('initial', {})
+            for (k, v) in default_relations:
+                initial[k] = v
+            kwargs['initial'] = initial
+            super(CForm, self).__init__(*args, **kwargs)
+
+        class Meta:
+            model = form_model
+            fields = '__all__'
+            widgets = form_widgets
+    return CForm
+
+
+
+class CreateView(generic.edit.CreateView):
+    template_name = 'rapid/bare/create.html'
+    action_name = ''
+
+    object = None
+
+    fields = '__all__'
+
+    def request_form(self, request):
+        return _get_form(request, self.model)
+
+    def get(self, request, **kwargs):
+        context = RequestContext(request).flatten()
+        context.update(kwargs)
+
+        cd = ModelData(self.model, request=request)
+        if not cd.has_permission(request, self.action_name):
+            raise PermissionDenied
+        context['model_data'] = cd
+
+        parent_model = self.model
+        parent_fields = self.fields
+
+        if request.POST:
+            context['form'] = self.request_form(request)(request.POST)
+        else:
+            context['form'] = self.request_form(request)()
+
+        context = self.get_context_data(**context)
+
+        return self.render_to_response(context)
+
+    def post(self, request, **kwargs):
+        m = ModelData(self.model, request=request)
+        if m.has_permission(request, self.action_name):
+            f = self.request_form(request)(request.POST)
+            if f.is_valid():
+                f.save()
+                return redirect(m.list_url())
+            return self.get(request, **kwargs)
+        raise PermissionDenied()
+
+class UpdateView(generic.edit.UpdateView):
+    template_name = 'rapid/bare/update.html'
+    action_name = ''
+
+    fields = '__all__'
+
+    def get_object(self, queryset=None):
+        return get_object_or_404(self.model, pk=self.kwargs['pk'])
+
+    def request_form(self, request):
+        return _get_form(request, self.model)
+
+    def get(self, request, pk, **kwargs):
+        context = RequestContext(request).flatten()
+        context.update(kwargs)
+
+        obj = self.get_object()
+        context['object'] = obj
+        context[self.get_context_object_name(obj)] = obj
+
+        cd = InstanceData(obj, request=request)
+        if not cd.has_permission(request, self.action_name):
+            raise PermissionDenied
+        self.object = cd
+        context['object_data'] = cd
+
+        if request.POST:
+            context['form'] = self.request_form(request)(request.POST, instance=obj)
+        else:
+            context['form'] = self.request_form(request)(instance=obj)
+
+        context = self.get_context_data(**context)
+
+        return self.render_to_response(context)
+
+    def post(self, request, pk, **kwargs):
+        obj = self.get_object()
+        m = InstanceData(obj, request=request)
+        if m.has_permission(request, self.action_name):
+            f = self.request_form(request)(request.POST, instance=obj)
+            if f.is_valid():
+                if f.instance.pk != obj.pk:
+                    raise PermissionDenied
+                f.save()
+                return redirect(m.list_url())
+            self.form = f
+            return self.get(request, pk, **kwargs)
+        raise PermissionDenied
+
+
+class DeleteView(generic.edit.DeleteView):
+    template_name = 'rapid/bare/delete.html'
+    action_name = ''
+
+    fields = '__all__'
+
+    def __init__(self, **kwargs):
+        super(DeleteView, self).__init__(**kwargs)
+        self.success_url = ModelData(self.model).list_url()
+
+    def get_object(self, queryset=None):
+        return get_object_or_404(self.model, pk=self.kwargs['pk'])
+
+    def get(self, request, *args, **kwargs):
+        obj = InstanceData(self.get_object())
+        if not obj.has_permission(request, self.action_name):
+            raise PermissionDenied
+        return super(DeleteView, self).get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        obj = InstanceData(self.get_object())
+        if not obj.has_permission(request, self.action_name):
+            raise PermissionDenied
+        return super(DeleteView, self).post(request, *args, **kwargs)
+
+
+class SelectView(ListView):
+    """
+    Apresenta uma lista selecionavel de objetos, para
+    o preenchimento de relações.
+    """
+    template_name = 'rapid/bare/select.html'
+
+
+def bare_or_main(view):
+    main_window_template = "rapid/main_window.html"
+
+    def vw(request, *args, **kwargs):
+        resp = view(request, *args, **kwargs)
+        bare = is_ajax_request(request)
+        if bare:
+            if resp.status_code >= 300 and resp.status_code <= 399:
+                # I'll interpret redirects as successful POST,
+                # thus, the response'll get replaced by something with
+                # a header that says "success!"
+                ret = HttpResponse('')
+                ret['X-FORM-STATUS:'] = 'success'
+                return ret
+        if not bare and resp.status_code == 200 and request.method != "HEAD":
+            resp.render()
+            body = resp.content
+            main_win = loader.get_template(main_window_template)
+            context = RequestContext(request)
+            context.update({'body_text': body, 'this_url': request.get_full_path()})
+            resp.content = main_win.render(context, request)
+        return resp
+    return vw
+
+def _rvw(view_class, action_name):
+    view_class.action_name = action_name
+
+    def vw(model):
+        return bare_or_main(view_class.as_view(model=model))
+    return vw
+
+default_actions = [
+    (False, Action("list", "", [], _rvw(ListView, 'list'), "listar", "fa-list")),
+    (False, Action("view", "(?P<pk>[0-9]+)", ['pk'], _rvw(ReadView, 'view'), "ver", "fa-eye")),
+    (True, Action("edit", "(?P<pk>[0-9]+)", ['pk'], _rvw(UpdateView, 'edit'), "editar", "fa-pencil")),
+    (True, Action("add", "", [], _rvw(CreateView, 'add'), "adicionar", "fa-plus")),
+    (False, Action("select", "", [], _rvw(SelectView, 'select'), "selecionar", "fa-hand-o-up",
+                   visibility=Action.Visibility.hidden)),
+]
+
+def register_model(model, name=None, read_set=None, write_set=None, can_erase=False):
+    if not read_set:
+        read_set = permissions.default_read(model)
+    if not write_set:
+        write_set = permissions.default_write(model)
+    ret = []
+    for edt, a in default_actions:
+        if edt:
+            ret.append(registry.register_action(a, MenuEntry(model, write_set, name=name)))
+        else:
+            ret.append(registry.register_action(a, MenuEntry(model, read_set, name=name)))
+    if can_erase:
+        a =  Action("delete", "(?P<pk>[0-9]+)", ['pk'], _rvw(DeleteView, 'delete'), "apagar", "fa-trash-o")
+        ret.append(registry.register_action(a, MenuEntry(model, write_set, name=name)))
+    return [u for u in ret if u]
+
+def update_form_class(form):
+    class F(UpdateView):
+        def request_form(self, request):
+            return form
+    return F
+
+def register_instance_form(model, action_name, menu_name, form, permission_set, icon=None, visibility=None):
+    if not icon:
+        icon = ''
+    a = Action(action_name, "(?P<pk>[0-9]+)", ['pk'], _rvw(update_form_class(form), action_name),
+               menu_name, icon, visibility)
+    u = registry.register_action(a, MenuEntry(model, permission_set))
+    return [u] if u else []
+
+def register_simple_select(model, visible_fields, permission_set, name=None):
+    class Vw(SelectView):
+        fields = visible_fields
+    a = Action("select", "", [], _rvw(Vw, 'select'), "selecionar", "fa-hand-o-up",
+               visibility=Action.Visibility.hidden)
+    u = registry.register_action(a, MenuEntry(model, permission_set, name=name))
+    return [u] if u else []

+ 106 - 0
src/rapid/widgets.py

@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+
+__author__ = 'marcos.medeiros'
+
+from django.forms import widgets
+from django.template import loader, Context
+
+from rapid.wrappers import ModelData, InstanceData
+
+class RapidReadOnly(widgets.Widget):
+    def __init__(self, *args, **kwargs):
+        super(RapidReadOnly, self).__init__(*args, **kwargs)
+
+    def render(self, name, value, attrs=None):
+        hidden = '<input type="hidden" name="%s" value="%s" ' % (name, value)
+        for a in attrs.keys():
+            hidden += '%s="%s" ' % (a, attrs[a])
+        hidden += '>'
+        return '<span class="data-value">%s</span>%s\n' % (unicode(value), hidden)
+
+    def value_from_datadict(self, data, files, name):
+        return data[name]
+
+class RapidRelationReadOnly(widgets.Widget):
+    def __init__(self, model, *args, **kwargs):
+        super(RapidRelationReadOnly, self).__init__(*args, **kwargs)
+        self.model = ModelData(model)
+
+    def render(self, name, value, attrs=None):
+        hidden = '<input type="hidden" name="%s" value="%s" ' % (name, value)
+        for a in attrs.keys():
+            hidden += '%s="%s" ' % (a, attrs[a])
+        hidden += '>'
+        if hasattr(value, '__iter__'):
+            objs = self.model.default_manager().filter(pk__in=value)
+            ret = ''
+            for o in objs:
+                ret += '<span class="data-value multiple">%s</span>\n' % unicode(o)
+            ret += hidden
+            return ret
+        else:
+            obj = self.model.default_manager().get(pk=value)
+            return '<span class="data-value">%s</span>%s\n' % (unicode(obj), hidden)
+
+    def value_from_datadict(self, data, files, name):
+        return data[name]
+
+
+class RapidSelector(widgets.Select):
+    """
+    Selects one of the target crud type.
+    For ForeignKeyFields and OneToOneFields.
+    If the target is dependent (that means, has only
+    value when linked with this object), only displays
+    an edition link.
+    """
+    def __init__(self, relation, *args, **kwargs):
+        super(RapidSelector, self).__init__(*args, **kwargs)
+        self.relation = relation
+        self.allow_multiple_selected = relation.is_multiple()
+        self.remove_deselected = relation.is_weak()
+
+    def render(self, name, value, attrs=None, choices=()):
+        id = attrs.get('id', name)
+        related = self.relation.related_model()
+        if self.allow_multiple_selected:
+            if value:
+                v = ",".join([str(x) for x in value])
+                selected = related.default_manager().filter(pk__in=value)
+            else:
+                v = ""
+                selected = []
+        else:
+            if value:
+                v = str(value)
+                selected = related.default_manager().get(pk=value)
+            else:
+                v = ""
+                selected = ""
+        select_url = related.select_url()
+        if self.allow_multiple_selected:
+            icon = 'fa-times'
+            if self.remove_deselected:
+                icon = 'fa-trash-o'
+        else:
+            icon = 'fa-search'
+        c = Context({'id': id, 'name': name, 'value': v, 'selected': selected, 'icon': icon, 'select_url': select_url,
+                     'multiple': self.allow_multiple_selected})
+        if self.allow_multiple_selected:
+            t = loader.get_template('rapid/widgets/multiple-selector.html')
+        else:
+            t = loader.get_template('rapid/widgets/single-selector.html')
+        return t.render(c)
+
+    def value_from_datadict(self, data, files, name):
+        val = data.get(name)
+        print val
+        if self.allow_multiple_selected:
+            if val:
+                return [int(x) for x in val.split(",") if x]
+            return []
+        else:
+            if val:
+                return int(val)
+            return None
+

+ 322 - 0
src/rapid/wrappers.py

@@ -0,0 +1,322 @@
+# -*- coding: utf-8 -*-
+
+__author__ = 'marcos.medeiros'
+
+from rapid.registry import registry, Action
+from rapid import filters
+
+import itertools
+from django.db import models
+
+
+class InstanceData:
+    def __init__(self, instance, request=None, excludes=None, creator=None, fields=None):
+        excludes = [] if excludes is None else excludes
+        self.model = ModelData(type(instance), request, excludes, creator, fields)
+        self.instance = instance
+        self.request = request
+        self.excludes = excludes if excludes else []
+        self.creator = creator
+        self._fields = fields if fields else self.model.fields()
+
+    def values(self):
+        o = self.instance
+        for f in self.model.fields():
+            if f.is_relation:
+                yield self._value_of_field(o, f)
+            else:
+                yield self._value_of_field(o, f)
+
+    def _value_of_field(self, instance, field):
+        """
+        Retorna o valor do campo dado na instância informada.
+        ::return Uma tupla, onde o primeiro elemento é o valor informado, o
+        segundo elemento é um booleano que informa se o valor é iteravel ou não
+        e o terceiro elemento é uma sequencia de URLs para os itens do primeiro elemento,
+        ou um valor falso caso não haja links.
+        """
+        if hasattr(instance, field.bare_name()):
+            v = getattr(instance, field.bare_name())
+        else:  # Many to many relations without value may disappear
+            return [], True
+        if hasattr(v, '__iter__'):
+            return (v, ()), True
+        if hasattr(v, 'all'):
+            return [(x, InstanceData(x, self.request, creator=(self, field))) for x in v.all()], True
+        if isinstance(v, models.Model):
+            return (v, InstanceData(v, self.request, creator=(self, field))), False
+        return (v, ()), False
+
+    def fields_and_values(self):
+        for field in self.model.fields():
+            value, is_multiple = self._value_of_field(self.instance, field)
+            yield field, value, is_multiple
+
+    def is_controlled(self):
+        return self.model.is_controlled()
+
+    def can_read(self):
+        if self.can_write():
+            return True
+        vw = registry.model_entry(self.model.model).permission_set(self.request)
+        if vw:
+            return vw.filter(pk=self.instance.pk).exists()
+        return False
+
+    def can_write(self):
+        ed = registry.model_entry(self.model.model)['edit'].permission_set(self.request)
+        if ed:
+            return ed.filter(pk=self.instance.pk).exists()
+        return False
+
+    def view_url(self):
+        return registry.get_url_of_action(self.model.model, "view", pk=self.instance.pk)
+
+    def edit_url(self):
+        url = registry.get_url_of_action(self.model.model, "edit", pk=self.instance.pk)
+        by = self.creator
+        if by:
+            dt, fd = by
+            if fd.one_to_one or fd.one_to_many:
+                # Este objeto depende do parent.
+                # Não posso editar esta relação
+                return url + "?default=" + fd.field.name + ":" + str(dt.object.pk)
+            if fd.many_to_one or fd.many_to_many:
+                return url
+        return url
+
+    def remove_url(self):
+        return registry.get_url_of_action(self.model.model, "delete", pk=self.instance.pk)
+
+    def create_url(self):
+        return registry.get_url_of_action(self.model.model, "add")
+
+    def list_url(self):
+        return registry.get_url_of_action(self.model.model, "list")
+
+    def select_url(self):
+        return registry.get_url_of_action(self.model.model, "select")
+
+    def actions(self):
+        acts = registry.model_entry(self.model.model)
+        if self.request:
+            for a in acts.values():
+                if a.permission_set(self.request) and\
+                        a.permission_set(self.request).filter(pk=self.instance.pk).exists() and\
+                        a.action.visibility > Action.Visibility.hidden:
+                    yield (a, a.get_url(self.instance))
+
+    def model_actions(self):
+        for (a, u) in self.actions():
+            if not a.action.query_parameters:
+                yield (a, u)
+
+    def instance_actions(self):
+        for (a, u) in self.actions():
+            if a.action.query_parameters:
+                yield (a, u)
+
+    def list_actions(self):
+        for (a, u) in self.instance_actions():
+            if a.action.visibility == Action.Visibility.list:
+                yield (a, u)
+
+    def has_permission(self, request, action_name):
+        m = registry.model_entry(self.model.model).get(action_name)
+        if m:
+            return m.permission_set(request) and\
+                m.permission_set(request).filter(pk=self.instance.pk).exists()
+        return False
+
+    def __unicode__(self):
+        return unicode(self.instance)
+
+    def __str__(self):
+        return str(self.model) + ': ' + str(self.instance.pk)
+
+
+class ModelData:
+    def __init__(self, model, request=None, excludes=None, creator=None, fields=None):
+        excludes = [] if excludes is None else excludes
+        self.model = model
+        self.request = request
+        self.excludes = excludes if excludes else []
+        self.creator = creator
+        self._fields = [self.field_by_name(f) for f in fields] if fields else self.all_fields()
+
+    def model_name(self):
+        return unicode(self.model._meta.verbose_name)
+
+    def model_name_plural(self):
+        return unicode(self.model._meta.verbose_name_plural)
+
+    def default_manager(self):
+        return self.model._default_manager
+
+    def all_fields(self):
+        relations = []
+        for f in itertools.chain(self.local_fields(), self.related_fields()):
+            if f.is_relation():
+                relations.append(f)
+            else:
+                if f.name not in self.excludes:
+                    yield f
+        for f in relations:
+            if f.name not in self.excludes:
+                yield f
+
+    def fields(self):
+        for f in self._fields:
+            yield f
+
+    def local_fields(self):
+        for f in self.model._meta.local_fields:
+            if f.name not in self.excludes:
+                yield FieldData(f, self.request)
+        for f in self.model._meta.local_many_to_many:
+            if f.name not in self.excludes:
+                yield FieldData(f, self.request)
+
+    def related_fields(self):
+        for f in self.model._meta.get_all_related_objects():
+            yield FieldData(f, self.request)
+
+    def is_controlled(self):
+        return registry.is_controlled(self.model)
+
+    def can_read(self):
+        if self.can_write():
+            return True
+        vw = registry.model_entry(self.model)['view'].permission_set(self.request)
+        if vw:
+            return vw.exists()
+        return False
+
+    def can_write(self):
+        ed = registry.model_entry(self.model)['edit'].permission_set(self.request)
+        if ed:
+            return ed.exists()
+        return False
+
+    def create_url(self):
+        return registry.get_url_of_action(self.model, "add")
+
+    def list_url(self):
+        return registry.get_url_of_action(self.model, "list")
+
+    def select_url(self):
+        return registry.get_url_of_action(self.model, "select")
+
+    def actions(self):
+        acts = registry.model_entry(self.model)
+        if self.request:
+            for a in acts.values():
+                if a.permission_set(self.request) and\
+                        a.permission_set(self.request).exists() and\
+                        not a.action.query_parameters and\
+                        a.action.visibility > Action.Visibility.hidden:
+                    yield (a, a.get_url())
+
+    def has_permission(self, request, action_name):
+        m = registry.model_entry(self.model).get(action_name)
+        if m:
+            return m.permission_set(request) and\
+                m.permission_set(request).exists()
+        return False
+
+    def field_by_name(self, field_name):
+        return FieldData(self.model._meta.get_field(field_name), self.request)
+
+    def __unicode__(self):
+        return unicode(self.model)
+
+    def __str__(self):
+        return 'Model: ' + str(self.model)
+
+
+class FieldData:
+    def __init__(self, field, request=None):
+        self.field = field
+        self.request = request
+
+    @classmethod
+    def from_model(cls, model, field_name):
+        ff = ModelData(model).fields()
+        for f in ff:
+            if f.bare_name() == unicode(field_name):
+                return f
+        return None
+
+    def bare_name(self):
+        return unicode(self.field.name)
+
+    def name(self):
+        if hasattr(self.field, "verbose_name"):
+            return unicode(self.field.verbose_name)
+        return unicode(self.field.name)
+
+    def name_plural(self):
+        if hasattr(self.field, "verbose_name_plural"):
+            return unicode(self.field.verbose_name_plural)
+        return self.name() + "s"
+
+    def is_relation(self):
+        return self.field.is_relation
+
+    def is_multiple(self):
+        if not self.is_relation():
+            return False
+        if self.field.one_to_many:
+            return True
+        if self.field.many_to_many:
+            return True
+        return False
+
+    def related_model(self):
+        if hasattr(self.field, "related_model"):
+            return ModelData(self.field.related_model)
+        if hasattr(self.field, "to"):
+            return ModelData(self.field.to)
+        return None
+
+    def is_weak(self):
+        if not self.is_relation():
+            return False
+        f = self.field
+        if hasattr(f, "many_to_many") and f.many_to_many:
+            return False
+        if hasattr(f, "many_to_one") and self.field.many_to_one:
+            return False
+        if hasattr(self.field, "get_related_field"):
+            o = self.field.get_related_field
+            if self.field.one_to_one or self.field.one_to_many:
+                if hasattr(o, "required"):
+                    return o.required
+                return True
+        if isinstance(f, models.ForeignKey):
+            return self.related_model()._meta.pk.name
+        return False
+
+    def filter_html(self):
+        return filters.Filter.selection_type_html(self, self.request)
+
+    def __str__(self):
+        return self.bare_name()
+
+
+class ValueData:
+    def __init__(self, value, field):
+        self.value = value
+        self.field = field
+
+    def can_view(self):
+        if self.field.is_relation():
+            o = self.field.related_model()
+            return registry.is_controlled(o)
+        return False
+
+    def is_multiple(self):
+        return self.field.is_multiple()
+
+    def __str__(self):
+        return str(self.field) + ': ' + str(self.value)