diff --git a/README.rst b/README.rst index f803589..6cb77a0 100644 --- a/README.rst +++ b/README.rst @@ -51,13 +51,14 @@ Our goal is to make this API work: .. code-block:: python + # myapp/admin2.py # Import your custom models from .models import Post, Comment from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.contrib.auth.models import User import djadmin2 - from djadmin2.models import ModelAdmin2 + from djadmin2.admins import ModelAdmin2 class UserAdmin2(ModelAdmin2): diff --git a/djadmin2/admins.py b/djadmin2/admins.py new file mode 100644 index 0000000..39d766b --- /dev/null +++ b/djadmin2/admins.py @@ -0,0 +1,8 @@ +""" + This serves as an easy to remember alias for types. +""" + +from . import types + +ModelAdmin2 = types.ModelAdmin2 +Admin2Inline = types.Admin2Inline diff --git a/djadmin2/core.py b/djadmin2/core.py index 19ab086..fe6c181 100644 --- a/djadmin2/core.py +++ b/djadmin2/core.py @@ -9,7 +9,7 @@ from django.utils.importlib import import_module from . import apiviews -from . import models +from . import types from . import utils from . import views @@ -46,7 +46,7 @@ class Admin2(object): if model in self.registry: raise ImproperlyConfigured('%s is already registered in django-admin2' % model) if not model_admin: - model_admin = models.ModelAdmin2 + model_admin = types.ModelAdmin2 self.registry[model] = model_admin(model, admin=self, **kwargs) # Add the model to the apps registry diff --git a/djadmin2/models.py b/djadmin2/models.py index 703fddb..214a6b1 100644 --- a/djadmin2/models.py +++ b/djadmin2/models.py @@ -1,330 +1 @@ -""" -WARNING: This file about to undergo major refactoring by @pydanny per Issue #99. - -For wont of a better name, this module is called 'models'. It's role is -synonymous with the django.contrib.admin.sites model. - -""" - -import logging - -from django.core.urlresolvers import reverse -from django.conf.urls import patterns, url -from django.contrib.auth import models as auth_app -from django.db.models import get_models, signals - -import extra_views - -from djadmin2 import apiviews -from djadmin2 import constants -from djadmin2 import views -from djadmin2 import actions -from djadmin2 import utils -from djadmin2.forms import modelform_factory - -logger = logging.getLogger(__name__) - - -class BaseAdmin2(object): - """ - Warning: This class will likely merged with ModelAdmin2 - """ - - search_fields = [] - - # Show the fields to be displayed as columns - # TODO: Confirm that this is what the Django admin uses - list_fields = [] - - #This shows up on the DocumentListView of the Posts - list_actions = [] - - # This shows up in the DocumentDetailView of the Posts. - document_actions = [] - - # shows up on a particular field - field_actions = {} - - fields = None - exclude = None - fieldsets = None - form_class = None - filter_vertical = () - filter_horizontal = () - radio_fields = {} - prepopulated_fields = {} - formfield_overrides = {} - readonly_fields = () - ordering = None - - def __init__(self, name, model, admin): - super(BaseAdmin2, self).__init__() - self.name = name - self.model = model - self.admin = admin - - -class ModelAdmin2(BaseAdmin2): - """ - Warning: This class is targeted for reduction. - It's bloated and ugly. - """ - - list_display = ('__str__',) - list_display_links = () - list_filter = () - list_select_related = False - list_per_page = 100 - list_max_show_all = 200 - list_editable = () - search_fields = () - save_as = False - save_on_top = False - verbose_name = None - verbose_name_plural = None - model_admin_attributes = constants.MODEL_ADMIN_ATTRS - - create_form_class = None - update_form_class = None - - inlines = [] - - # Views - index_view = views.ModelListView - create_view = views.ModelAddFormView - update_view = views.ModelEditFormView - detail_view = views.ModelDetailView - delete_view = views.ModelDeleteView - - # API configuration - api_serializer_class = None - - # API Views - api_list_view = apiviews.ListCreateAPIView - api_detail_view = apiviews.RetrieveUpdateDestroyAPIView - - # Actions - actions = [actions.delete_selected] - - def __init__(self, model, admin, name=None, **kwargs): - self.name = name - self.model = model - self.admin = admin - model_options = utils.model_options(model) - self.app_label = model_options.app_label - self.model_name = model_options.object_name.lower() - - if self.name is None: - self.name = '{}_{}'.format(self.app_label, self.model_name) - - if self.verbose_name is None: - self.verbose_name = model_options.verbose_name - if self.verbose_name_plural is None: - self.verbose_name_plural = model_options.verbose_name_plural - - def get_default_view_kwargs(self): - return { - 'app_label': self.app_label, - 'model': self.model, - 'model_name': self.model_name, - 'model_admin': ImmutableAdmin(self), - } - - def get_default_api_view_kwargs(self): - kwargs = self.get_default_view_kwargs() - kwargs.update({ - 'serializer_class': self.api_serializer_class, - }) - return kwargs - - def get_prefixed_view_name(self, view_name): - return '{}_{}'.format(self.name, view_name) - - def get_index_kwargs(self): - return self.get_default_view_kwargs() - - def get_create_kwargs(self): - kwargs = self.get_default_view_kwargs() - kwargs.update({ - 'inlines': self.inlines, - 'form_class': self.create_form_class if self.create_form_class else self.form_class, - }) - return kwargs - - def get_update_kwargs(self): - kwargs = self.get_default_view_kwargs() - form_class = self.update_form_class if self.update_form_class else self.form_class - if form_class is None: - form_class = modelform_factory(self.model) - kwargs.update({ - 'inlines': self.inlines, - 'form_class': form_class, - }) - return kwargs - - def get_detail_kwargs(self): - return self.get_default_view_kwargs() - - def get_delete_kwargs(self): - return self.get_default_view_kwargs() - - def get_index_url(self): - return reverse('admin2:{}'.format(self.get_prefixed_view_name('index'))) - - def get_api_list_kwargs(self): - kwargs = self.get_default_api_view_kwargs() - kwargs.update({ - 'paginate_by': self.list_per_page, - }) - return kwargs - - def get_api_detail_kwargs(self): - return self.get_default_api_view_kwargs() - - def get_urls(self): - return patterns('', - url( - regex=r'^$', - view=self.index_view.as_view(**self.get_index_kwargs()), - name=self.get_prefixed_view_name('index') - ), - url( - regex=r'^create/$', - view=self.create_view.as_view(**self.get_create_kwargs()), - name=self.get_prefixed_view_name('create') - ), - url( - regex=r'^(?P[0-9]+)/$', - view=self.detail_view.as_view(**self.get_detail_kwargs()), - name=self.get_prefixed_view_name('detail') - ), - url( - regex=r'^(?P[0-9]+)/update/$', - view=self.update_view.as_view(**self.get_update_kwargs()), - name=self.get_prefixed_view_name('update') - ), - url( - regex=r'^(?P[0-9]+)/delete/$', - view=self.delete_view.as_view(**self.get_delete_kwargs()), - name=self.get_prefixed_view_name('delete') - ), - ) - - def get_api_urls(self): - return patterns('', - url( - regex=r'^$', - view=self.api_list_view.as_view(**self.get_api_list_kwargs()), - name=self.get_prefixed_view_name('api-list'), - ), - url( - regex=r'^(?P[0-9]+)/$', - view=self.api_detail_view.as_view(**self.get_api_detail_kwargs()), - name=self.get_prefixed_view_name('api-detail'), - ), - ) - - @property - def urls(self): - # We set the application and instance namespace here - return self.get_urls(), None, None - - @property - def api_urls(self): - return self.get_api_urls(), None, None - - def get_actions(self): - actions_dict = {} - - for cls in type(self).mro()[::-1]: - class_actions = getattr(cls, 'actions', []) - for action in class_actions: - actions_dict[action.__name__] = { - 'name': action.__name__, - 'description': actions.get_description(action), - 'func': action - } - return actions_dict - - -class Admin2Inline(extra_views.InlineFormSet): - """ - A simple extension of django-extra-view's InlineFormSet that - adds some useful functionality. - """ - - def construct_formset(self): - """ - Overrides construct_formset to attach the model class as - an attribute of the returned formset instance. - """ - formset = super(Admin2Inline, self).construct_formset() - formset.model = self.inline_model - return formset - - -class ImmutableAdmin(object): - """ - Only __init__ allows setting of attributes - """ - - def __init__(self, model_admin): - """ The __init__ is the only method where the ImmutableModelAdmin allows - for setting of values. - """ - for attr_name in model_admin.model_admin_attributes: - setattr(self, attr_name, getattr(model_admin, attr_name)) - - self.__delattr__ = self._immutable - self.__setattr__ = self._immutable - - def _immutable(self, name, value): - raise TypeError("Can't modify immutable model admin") - - -def create_extra_permissions(app, created_models, verbosity, **kwargs): - """ - Creates 'view' permissions for all models. - django.contrib.auth only creates add, change and delete permissions. Since we also support read-only views, we need - to add our own extra permission. - Copied from django.contrib.auth.management.create_permissions - """ - from django.contrib.contenttypes.models import ContentType - - app_models = get_models(app) - - # This will hold the permissions we're looking for as - # (content_type, (codename, name)) - searched_perms = list() - # The codenames and ctypes that should exist. - ctypes = set() - for klass in app_models: - ctype = ContentType.objects.get_for_model(klass) - ctypes.add(ctype) - - opts = utils.model_options(klass) - perm = ('view_%s' % opts.object_name.lower(), u'Can view %s' % opts.verbose_name_raw) - searched_perms.append((ctype, perm)) - - # Find all the Permissions that have a content_type for a model we're - # looking for. We don't need to check for codenames since we already have - # a list of the ones we're going to create. - all_perms = set(auth_app.Permission.objects.filter( - content_type__in=ctypes, - ).values_list( - "content_type", "codename" - )) - - perms = [ - auth_app.Permission(codename=codename, name=name, content_type=ctype) - for ctype, (codename, name) in searched_perms - if (ctype.pk, codename) not in all_perms - ] - auth_app.Permission.objects.bulk_create(perms) - if verbosity >= 2: - for perm in perms: - logger.debug("Adding permission '%s'" % perm) - - -signals.post_syncdb.connect(create_extra_permissions, - dispatch_uid="django-admin2.djadmin2.models.create_extra_permissions") +""" Boilerplate for now, will serve a purpose soon! """ diff --git a/djadmin2/tests/test_core.py b/djadmin2/tests/test_core.py index c30d617..f7f97f3 100644 --- a/djadmin2/tests/test_core.py +++ b/djadmin2/tests/test_core.py @@ -2,7 +2,7 @@ from django.db import models from django.core.exceptions import ImproperlyConfigured from django.test import TestCase -from ..models import ModelAdmin2 +from ..admins import ModelAdmin2 from ..core import Admin2 diff --git a/djadmin2/tests/test_models.py b/djadmin2/tests/test_models.py deleted file mode 100644 index 6003ef3..0000000 --- a/djadmin2/tests/test_models.py +++ /dev/null @@ -1 +0,0 @@ -# This is just a stub file. Write moar tests! \ No newline at end of file diff --git a/djadmin2/types.py b/djadmin2/types.py index 8a3a1f2..283cf41 100644 --- a/djadmin2/types.py +++ b/djadmin2/types.py @@ -1,4 +1,298 @@ from collections import namedtuple +import logging + +from django.core.urlresolvers import reverse +from django.conf.urls import patterns, url +from django.contrib.auth import models as auth_app +from django.db.models import get_models, signals + +import extra_views + +from . import apiviews +from . import constants +from . import views +from . import actions +from . import utils +from .forms import modelform_factory + +logger = logging.getLogger(__name__) + + +class ModelAdmin2(object): + """ + Warning: This class is targeted for reduction. + It's bloated and ugly. + """ + + list_display = ('__str__',) + list_display_links = () + list_filter = () + list_select_related = False + list_per_page = 100 + list_max_show_all = 200 + list_editable = () + search_fields = () + save_as = False + save_on_top = False + verbose_name = None + verbose_name_plural = None + model_admin_attributes = constants.MODEL_ADMIN_ATTRS + + search_fields = [] + + # Show the fields to be displayed as columns + # TODO: Confirm that this is what the Django admin uses + list_fields = [] + + #This shows up on the DocumentListView of the Posts + list_actions = [] + + # This shows up in the DocumentDetailView of the Posts. + document_actions = [] + + # shows up on a particular field + field_actions = {} + + fields = None + exclude = None + fieldsets = None + form_class = None + filter_vertical = () + filter_horizontal = () + radio_fields = {} + prepopulated_fields = {} + formfield_overrides = {} + readonly_fields = () + ordering = None + + create_form_class = None + update_form_class = None + + inlines = [] + + # Views + index_view = views.ModelListView + create_view = views.ModelAddFormView + update_view = views.ModelEditFormView + detail_view = views.ModelDetailView + delete_view = views.ModelDeleteView + + # API configuration + api_serializer_class = None + + # API Views + api_list_view = apiviews.ListCreateAPIView + api_detail_view = apiviews.RetrieveUpdateDestroyAPIView + + # Actions + actions = [actions.delete_selected] + + def __init__(self, model, admin, name=None, **kwargs): + self.name = name + self.model = model + self.admin = admin + model_options = utils.model_options(model) + self.app_label = model_options.app_label + self.model_name = model_options.object_name.lower() + + if self.name is None: + self.name = '{}_{}'.format(self.app_label, self.model_name) + + if self.verbose_name is None: + self.verbose_name = model_options.verbose_name + if self.verbose_name_plural is None: + self.verbose_name_plural = model_options.verbose_name_plural + + def get_default_view_kwargs(self): + return { + 'app_label': self.app_label, + 'model': self.model, + 'model_name': self.model_name, + 'model_admin': immutable_admin_factory(self), + } + + def get_default_api_view_kwargs(self): + kwargs = self.get_default_view_kwargs() + kwargs.update({ + 'serializer_class': self.api_serializer_class, + }) + return kwargs + + def get_prefixed_view_name(self, view_name): + return '{}_{}'.format(self.name, view_name) + + def get_index_kwargs(self): + return self.get_default_view_kwargs() + + def get_create_kwargs(self): + kwargs = self.get_default_view_kwargs() + kwargs.update({ + 'inlines': self.inlines, + 'form_class': self.create_form_class if self.create_form_class else self.form_class, + }) + return kwargs + + def get_update_kwargs(self): + kwargs = self.get_default_view_kwargs() + form_class = self.update_form_class if self.update_form_class else self.form_class + if form_class is None: + form_class = modelform_factory(self.model) + kwargs.update({ + 'inlines': self.inlines, + 'form_class': form_class, + }) + return kwargs + + def get_detail_kwargs(self): + return self.get_default_view_kwargs() + + def get_delete_kwargs(self): + return self.get_default_view_kwargs() + + def get_index_url(self): + return reverse('admin2:{}'.format(self.get_prefixed_view_name('index'))) + + def get_api_list_kwargs(self): + kwargs = self.get_default_api_view_kwargs() + kwargs.update({ + 'paginate_by': self.list_per_page, + }) + return kwargs + + def get_api_detail_kwargs(self): + return self.get_default_api_view_kwargs() + + def get_urls(self): + return patterns('', + url( + regex=r'^$', + view=self.index_view.as_view(**self.get_index_kwargs()), + name=self.get_prefixed_view_name('index') + ), + url( + regex=r'^create/$', + view=self.create_view.as_view(**self.get_create_kwargs()), + name=self.get_prefixed_view_name('create') + ), + url( + regex=r'^(?P[0-9]+)/$', + view=self.detail_view.as_view(**self.get_detail_kwargs()), + name=self.get_prefixed_view_name('detail') + ), + url( + regex=r'^(?P[0-9]+)/update/$', + view=self.update_view.as_view(**self.get_update_kwargs()), + name=self.get_prefixed_view_name('update') + ), + url( + regex=r'^(?P[0-9]+)/delete/$', + view=self.delete_view.as_view(**self.get_delete_kwargs()), + name=self.get_prefixed_view_name('delete') + ), + ) + + def get_api_urls(self): + return patterns('', + url( + regex=r'^$', + view=self.api_list_view.as_view(**self.get_api_list_kwargs()), + name=self.get_prefixed_view_name('api-list'), + ), + url( + regex=r'^(?P[0-9]+)/$', + view=self.api_detail_view.as_view(**self.get_api_detail_kwargs()), + name=self.get_prefixed_view_name('api-detail'), + ), + ) + + @property + def urls(self): + # We set the application and instance namespace here + return self.get_urls(), None, None + + @property + def api_urls(self): + return self.get_api_urls(), None, None + + def get_actions(self): + actions_dict = {} + + for cls in type(self).mro()[::-1]: + class_actions = getattr(cls, 'actions', []) + for action in class_actions: + actions_dict[action.__name__] = { + 'name': action.__name__, + 'description': actions.get_description(action), + 'func': action + } + return actions_dict + + +class Admin2Inline(extra_views.InlineFormSet): + """ + A simple extension of django-extra-view's InlineFormSet that + adds some useful functionality. + """ + + def construct_formset(self): + """ + Overrides construct_formset to attach the model class as + an attribute of the returned formset instance. + """ + formset = super(Admin2Inline, self).construct_formset() + formset.model = self.inline_model + return formset + + +def create_extra_permissions(app, created_models, verbosity, **kwargs): + """ + Creates 'view' permissions for all models. + django.contrib.auth only creates add, change and delete permissions. Since we also support read-only views, we need + to add our own extra permission. + Copied from django.contrib.auth.management.create_permissions + + # TODO - determine if this is deprecated by the new permissions.py module + """ + # Is there any reason for doing this import here? + from django.contrib.contenttypes.models import ContentType + + app_models = get_models(app) + + # This will hold the permissions we're looking for as + # (content_type, (codename, name)) + searched_perms = list() + # The codenames and ctypes that should exist. + ctypes = set() + for klass in app_models: + ctype = ContentType.objects.get_for_model(klass) + ctypes.add(ctype) + + opts = utils.model_options(klass) + perm = ('view_%s' % opts.object_name.lower(), u'Can view %s' % opts.verbose_name_raw) + searched_perms.append((ctype, perm)) + + # Find all the Permissions that have a content_type for a model we're + # looking for. We don't need to check for codenames since we already have + # a list of the ones we're going to create. + all_perms = set(auth_app.Permission.objects.filter( + content_type__in=ctypes, + ).values_list( + "content_type", "codename" + )) + + perms = [ + auth_app.Permission(codename=codename, name=name, content_type=ctype) + for ctype, (codename, name) in searched_perms + if (ctype.pk, codename) not in all_perms + ] + auth_app.Permission.objects.bulk_create(perms) + if verbosity >= 2: + for perm in perms: + logger.debug("Adding permission '%s'" % perm) + + +signals.post_syncdb.connect(create_extra_permissions, + dispatch_uid="django-admin2.djadmin2.models.create_extra_permissions") def immutable_admin_factory(model_admin): diff --git a/docs/index.rst b/docs/index.rst index 430d299..428296b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,13 +24,14 @@ Our goal is to make this API work: .. code-block:: python + # myapp/admin2.py # Import your custom models from .models import Post, Comment from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.contrib.auth.models import User import djadmin2 - from djadmin2.models import ModelAdmin2 + from djadmin2.admins import ModelAdmin2 class UserAdmin2(ModelAdmin2): @@ -44,7 +45,7 @@ Our goal is to make this API work: djadmin2.default.register(User, UserAdmin2) -.. _GitHub: https://github.com/pydanny/django-admin2 +.. _GitHub: https://github.com/twoscoops/django-admin2 Content ------- diff --git a/example/blog/admin2.py b/example/blog/admin2.py index b232b7c..f39f252 100644 --- a/example/blog/admin2.py +++ b/example/blog/admin2.py @@ -7,7 +7,7 @@ from rest_framework.relations import PrimaryKeyRelatedField import djadmin2 from djadmin2.forms import floppify_form -from djadmin2.models import ModelAdmin2, Admin2Inline +from djadmin2.admins import ModelAdmin2, Admin2Inline from djadmin2.apiviews import Admin2APISerializer diff --git a/example/blog/tests/test_apiviews.py b/example/blog/tests/test_apiviews.py index 5486bb9..9ec3239 100644 --- a/example/blog/tests/test_apiviews.py +++ b/example/blog/tests/test_apiviews.py @@ -8,7 +8,7 @@ from django.utils import simplejson as json from djadmin2 import apiviews from djadmin2 import default -from djadmin2.models import ModelAdmin2 +from djadmin2.admins import ModelAdmin2 from ..models import Post diff --git a/example/blog/tests/test_auth_admin.py b/example/blog/tests/test_auth_admin.py index 0461200..0bb2b44 100644 --- a/example/blog/tests/test_auth_admin.py +++ b/example/blog/tests/test_auth_admin.py @@ -1,8 +1,10 @@ -import floppyforms from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test import TestCase from django.test.client import RequestFactory + +import floppyforms + import djadmin2 from blog.admin2 import UserAdmin2 diff --git a/example/blog/tests/test_builtin_api_resources.py b/example/blog/tests/test_builtin_api_resources.py index 5108dc4..d570623 100644 --- a/example/blog/tests/test_builtin_api_resources.py +++ b/example/blog/tests/test_builtin_api_resources.py @@ -1,5 +1,6 @@ from django.contrib.auth.models import Group, User from django.core.urlresolvers import reverse + from .test_apiviews import APITestCase diff --git a/example/blog/tests/test_modelforms.py b/example/blog/tests/test_modelforms.py index ec0107d..a797ed9 100644 --- a/example/blog/tests/test_modelforms.py +++ b/example/blog/tests/test_modelforms.py @@ -1,6 +1,8 @@ -import floppyforms from django import forms from django.test import TestCase + +import floppyforms + from djadmin2.forms import floppify_widget, floppify_form, modelform_factory from ..models import Post diff --git a/example/blog/tests/test_permissions.py b/example/blog/tests/test_permissions.py index 9b6be1f..166a37c 100644 --- a/example/blog/tests/test_permissions.py +++ b/example/blog/tests/test_permissions.py @@ -4,10 +4,10 @@ from django.template import Template, Context from django.test import TestCase from django.test.client import RequestFactory - import djadmin2 -from djadmin2.models import ModelAdmin2 +from djadmin2.admins import ModelAdmin2 from djadmin2.permissions import TemplatePermissionChecker + from blog.models import Post