├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.txt ├── mongoadmin ├── __init__.py ├── actions.py ├── auth │ ├── __init__.py │ ├── admin.py │ └── forms.py ├── contenttypes │ ├── __init__.py │ ├── models.py │ ├── utils.py │ └── views.py ├── mongohelpers.py ├── options.py ├── sites.py ├── templates │ └── admin │ │ ├── change_document_list.html │ │ └── mongo_change_form.html ├── templatetags │ ├── __init__.py │ ├── documenttags.py │ └── mongoadmintags.py ├── util.py ├── validation.py ├── views.py └── widgets.py ├── readme.md └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .project 3 | .pydevproject 4 | dist/* 5 | *~ 6 | MANIFEST 7 | *egg* 8 | *.bak 9 | *.tmproj 10 | .tm_properties 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Jan Schrewe. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-mongoadmin nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include readme.md 2 | include mongoadmin/templates/admin/* 3 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | django mongoadmin 2 | ================= 3 | 4 | This a drop in replacement for the django admin that works with monodb. 5 | It uses the django admin stuff wherever possible and can be used 6 | together with normal django models and a SQL database. 7 | 8 | Requirements 9 | ------------ 10 | 11 | - Django >= 1.3 12 | - `mongoengine `__ >= 0.6 13 | - `django-mongodbforms `__ 14 | 15 | Usage 16 | ----- 17 | 18 | Add mongoadmin to ``INSTALLED_APPS`` settings 19 | 20 | .. code:: python 21 | 22 | INSTALLED_APPS = ( 23 | ... 24 | 'mongoadmin', 25 | 'django.contrib.admin', 26 | ... 27 | ) 28 | 29 | Add mongoadmin to ``urls.py`` 30 | 31 | .. code:: python 32 | 33 | from django.contrib import admin 34 | admin.autodiscover() 35 | 36 | from mongoadmin import site 37 | 38 | urlpatterns = patterns('', 39 | # Uncomment the next line to enable the admin: 40 | url(r'^admin/', include(site.urls)), 41 | ) 42 | 43 | The ``admin.py`` for your app needs to use mongoadmin instead of 44 | django's admin: 45 | 46 | .. code:: python 47 | 48 | from mongoadmin import site, DocumentAdmin 49 | 50 | from app.models import AppDocument 51 | 52 | class AppDocumentAdmin(DocumentAdmin): 53 | pass 54 | site.register(AppDocument, AppDocumentAdmin) 55 | 56 | Now the document should appear as usual in django's admin. 57 | 58 | Using third party apps with mongoadmin 59 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 60 | 61 | To use third party apps (i.e. apps that register their admin classes in 62 | ``django.contrib.admin.site``) with mongoadmin you have to add 63 | ``MONGOADMIN_OVERRIDE_ADMIN = True`` to your settings file. This 64 | overrides the django admin site with mongoadmin's admin site. 65 | 66 | What works and doesn't work 67 | --------------------------- 68 | 69 | django-mongoadmin currently only supports the most basic things and even 70 | they are not really tested. 71 | 72 | You probably won't be able to use all of the nice stuff Django provides 73 | for relations. The problem is that Django bi-directional relations with 74 | a lot of magic, while mongoengine has a uni-directional ReferenceField. 75 | So in order to make relations really work one would either have to 76 | inject so much code into the documents and querysets that they become 77 | clones of Django's stuff or rewrite huge parts of the admin. If you feel 78 | that either approach is worth it, go for it and submit a pull request. 79 | Otherwise feel free to submit an issue but don't get your hopes up for a 80 | fix. 81 | -------------------------------------------------------------------------------- /mongoadmin/__init__.py: -------------------------------------------------------------------------------- 1 | from .options import * 2 | 3 | from mongoadmin.sites import site 4 | 5 | from django.conf import settings 6 | 7 | if getattr(settings, 'MONGOADMIN_OVERRIDE_ADMIN', False): 8 | import django.contrib.admin 9 | # copy already registered model admins 10 | # without that the already registered models 11 | # don't show up in the new admin 12 | site._registry = django.contrib.admin.site._registry 13 | 14 | django.contrib.admin.site = site -------------------------------------------------------------------------------- /mongoadmin/actions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Built-in, globally-available admin actions. 3 | """ 4 | 5 | from django import template 6 | from django.core.exceptions import PermissionDenied 7 | from django.contrib.admin import helpers 8 | from django.contrib.admin.util import get_deleted_objects, model_ngettext 9 | from django.db import router 10 | from django.shortcuts import render_to_response 11 | try: 12 | from django.utils.encoding import force_text as force_unicode 13 | except ImportError: 14 | from django.utils.encoding import force_unicode 15 | from django.utils.translation import ugettext_lazy, ugettext as _ 16 | from django.db import models 17 | 18 | from django.contrib.admin.actions import delete_selected as django_delete_selected 19 | 20 | def delete_selected(modeladmin, request, queryset): 21 | if issubclass(modeladmin.model, models.Model): 22 | return django_delete_selected(modeladmin, request, queryset) 23 | else: 24 | return _delete_selected(modeladmin, request, queryset) 25 | 26 | def _delete_selected(modeladmin, request, queryset): 27 | """ 28 | Default action which deletes the selected objects. 29 | 30 | This action first displays a confirmation page whichs shows all the 31 | deleteable objects, or, if the user has no permission one of the related 32 | childs (foreignkeys), a "permission denied" message. 33 | 34 | Next, it delets all selected objects and redirects back to the change list. 35 | """ 36 | opts = modeladmin.model._meta 37 | app_label = opts.app_label 38 | 39 | # Check that the user has delete permission for the actual model 40 | if not modeladmin.has_delete_permission(request): 41 | raise PermissionDenied 42 | 43 | using = router.db_for_write(modeladmin.model) 44 | 45 | # Populate deletable_objects, a data structure of all related objects that 46 | # will also be deleted. 47 | # TODO: Permissions would be so cool... 48 | deletable_objects, perms_needed, protected = get_deleted_objects( 49 | queryset, opts, request.user, modeladmin.admin_site, using) 50 | 51 | # The user has already confirmed the deletion. 52 | # Do the deletion and return a None to display the change list view again. 53 | if request.POST.get('post'): 54 | if perms_needed: 55 | raise PermissionDenied 56 | n = len(queryset) 57 | if n: 58 | for obj in queryset: 59 | obj_display = force_unicode(obj) 60 | modeladmin.log_deletion(request, obj, obj_display) 61 | # call the objects delete method to ensure signals are 62 | # processed. 63 | obj.delete() 64 | # This is what you get if you have to monkey patch every object in a changelist 65 | # No queryset object, I can tell ya. So we get a new one and delete that. 66 | #pk_list = [o.pk for o in queryset] 67 | #klass = queryset[0].__class__ 68 | #qs = klass.objects.filter(pk__in=pk_list) 69 | #qs.delete() 70 | modeladmin.message_user(request, _("Successfully deleted %(count)d %(items)s.") % { 71 | "count": n, "items": model_ngettext(modeladmin.opts, n) 72 | }) 73 | # Return None to display the change list page again. 74 | return None 75 | 76 | if len(queryset) == 1: 77 | objects_name = force_unicode(opts.verbose_name) 78 | else: 79 | objects_name = force_unicode(opts.verbose_name_plural) 80 | 81 | if perms_needed or protected: 82 | title = _("Cannot delete %(name)s") % {"name": objects_name} 83 | else: 84 | title = _("Are you sure?") 85 | 86 | context = { 87 | "title": title, 88 | "objects_name": objects_name, 89 | "deletable_objects": [deletable_objects], 90 | 'queryset': queryset, 91 | "perms_lacking": perms_needed, 92 | "protected": protected, 93 | "opts": opts, 94 | "root_path": modeladmin.admin_site.root_path, 95 | "app_label": app_label, 96 | 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, 97 | } 98 | 99 | # Display the confirmation page 100 | return render_to_response(modeladmin.delete_selected_confirmation_template or [ 101 | "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.object_name.lower()), 102 | "admin/%s/delete_selected_confirmation.html" % app_label, 103 | "admin/delete_selected_confirmation.html" 104 | ], context, context_instance=template.RequestContext(request)) 105 | 106 | delete_selected.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s") 107 | -------------------------------------------------------------------------------- /mongoadmin/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschrewe/django-mongoadmin/c53b8a0e7d3b96c9dd03126576b53ec9602f0a20/mongoadmin/auth/__init__.py -------------------------------------------------------------------------------- /mongoadmin/auth/admin.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext, ugettext_lazy as _ 2 | from django.contrib.auth.admin import csrf_protect_m 3 | try: 4 | from django.contrib.auth.admin import sensitive_post_parameters_m 5 | except ImportError: 6 | from django.utils.decorators import method_decorator 7 | from django.views.decorators.debug import sensitive_post_parameters 8 | sensitive_post_parameters_m = method_decorator(sensitive_post_parameters()) 9 | from django.contrib.auth.forms import AdminPasswordChangeForm 10 | from django.http import HttpResponseRedirect, Http404 11 | from django.contrib import admin 12 | from django.utils.html import escape 13 | from django.template.response import TemplateResponse 14 | from django.contrib.auth import get_user_model 15 | from django.conf import settings 16 | from django.contrib import messages 17 | from django.core.exceptions import PermissionDenied 18 | 19 | from mongoengine.django.auth import User 20 | from mongoengine import DoesNotExist 21 | from mongoengine.django.mongo_auth.models import MongoUser 22 | 23 | from mongoadmin import site, DocumentAdmin 24 | 25 | from .forms import UserCreationForm, UserChangeForm 26 | 27 | class MongoUserAdmin(DocumentAdmin): 28 | add_form_template = 'admin/auth/user/add_form.html' 29 | change_user_password_template = None 30 | fieldsets = ( 31 | (None, {'fields': ('username', 'password')}), 32 | (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), 33 | (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',)}), 34 | (_('Important dates'), {'fields': ('last_login', 'date_joined')}), 35 | ) 36 | add_fieldsets = ( 37 | (None, { 38 | 'classes': ('wide',), 39 | 'fields': ('username', 'password1', 'password2')} 40 | ), 41 | ) 42 | form = UserChangeForm 43 | add_form = UserCreationForm 44 | change_password_form = AdminPasswordChangeForm 45 | list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') 46 | list_filter = () 47 | search_fields = ('username', 'first_name', 'last_name', 'email') 48 | ordering = ('username',) 49 | filter_horizontal = () 50 | 51 | def get_user_or_404(self, request, id): 52 | qs = self.queryset(request) 53 | try: 54 | user = qs.filter(pk=id)[0] 55 | except (IndexError, DoesNotExist): 56 | raise Http404 57 | return user 58 | 59 | def get_fieldsets(self, request, obj=None): 60 | if not obj: 61 | return self.add_fieldsets 62 | return super(MongoUserAdmin, self).get_fieldsets(request, obj) 63 | 64 | def get_form(self, request, obj=None, **kwargs): 65 | """ 66 | Use special form during user creation 67 | """ 68 | defaults = {} 69 | if obj is None: 70 | defaults['form'] = self.add_form 71 | defaults.update(kwargs) 72 | return super(MongoUserAdmin, self).get_form(request, obj, **defaults) 73 | 74 | def get_urls(self): 75 | from django.conf.urls import patterns 76 | return patterns('', 77 | (r'^([0-9a-f]{24})/password/$', 78 | self.admin_site.admin_view(self.user_change_password)) 79 | ) + super(MongoUserAdmin, self).get_urls() 80 | 81 | def lookup_allowed(self, lookup, value): 82 | # See #20078: we don't want to allow any lookups involving passwords. 83 | if lookup.startswith('password'): 84 | return False 85 | return super(MongoUserAdmin, self).lookup_allowed(lookup, value) 86 | 87 | @sensitive_post_parameters_m 88 | @csrf_protect_m 89 | def add_view(self, request, form_url='', extra_context=None): 90 | # It's an error for a user to have add permission but NOT change 91 | # permission for users. If we allowed such users to add users, they 92 | # could create superusers, which would mean they would essentially have 93 | # the permission to change users. To avoid the problem entirely, we 94 | # disallow users from adding users if they don't have change 95 | # permission. 96 | if not self.has_change_permission(request): 97 | if self.has_add_permission(request) and settings.DEBUG: 98 | # Raise Http404 in debug mode so that the user gets a helpful 99 | # error message. 100 | raise Http404( 101 | 'Your user does not have the "Change user" permission. In ' 102 | 'order to add users, Django requires that your user ' 103 | 'account have both the "Add user" and "Change user" ' 104 | 'permissions set.') 105 | raise PermissionDenied 106 | if extra_context is None: 107 | extra_context = {} 108 | username_field = self.model._meta.get_field(self.model.USERNAME_FIELD) 109 | defaults = { 110 | 'auto_populated_fields': (), 111 | 'username_help_text': username_field.help_text, 112 | } 113 | extra_context.update(defaults) 114 | return super(MongoUserAdmin, self).add_view(request, form_url, 115 | extra_context) 116 | 117 | @sensitive_post_parameters_m 118 | def user_change_password(self, request, id, form_url=''): 119 | if not self.has_change_permission(request): 120 | raise PermissionDenied 121 | user = self.get_user_or_404(request, id) 122 | if request.method == 'POST': 123 | form = self.change_password_form(user, request.POST) 124 | if form.is_valid(): 125 | form.save() 126 | msg = ugettext('Password changed successfully.') 127 | messages.success(request, msg) 128 | return HttpResponseRedirect('..') 129 | else: 130 | form = self.change_password_form(user) 131 | 132 | fieldsets = [(None, {'fields': list(form.base_fields)})] 133 | adminForm = admin.helpers.AdminForm(form, fieldsets, {}) 134 | 135 | context = { 136 | 'title': _('Change password: %s') % escape(getattr(user, user.USERNAME_FIELD)),#user.get_username()), 137 | 'adminForm': adminForm, 138 | 'form_url': form_url, 139 | 'form': form, 140 | 'is_popup': '_popup' in request.REQUEST, 141 | 'add': True, 142 | 'change': False, 143 | 'has_delete_permission': False, 144 | 'has_change_permission': True, 145 | 'has_absolute_url': False, 146 | 'opts': self.model._meta, 147 | 'original': user, 148 | 'save_as': False, 149 | 'show_save': True, 150 | } 151 | return TemplateResponse(request, 152 | self.change_user_password_template or 153 | 'admin/auth/user/change_password.html', 154 | context, current_app=self.admin_site.name) 155 | 156 | def response_add(self, request, obj, post_url_continue=None): 157 | """ 158 | Determines the HttpResponse for the add_view stage. It mostly defers to 159 | its superclass implementation but is customized because the User model 160 | has a slightly different workflow. 161 | """ 162 | # We should allow further modification of the user just added i.e. the 163 | # 'Save' button should behave like the 'Save and continue editing' 164 | # button except in two scenarios: 165 | # * The user has pressed the 'Save and add another' button 166 | # * We are adding a user in a popup 167 | if '_addanother' not in request.POST and '_popup' not in request.POST: 168 | request.POST['_continue'] = 1 169 | return super(MongoUserAdmin, self).response_add(request, obj, 170 | post_url_continue) 171 | 172 | if MongoUser == get_user_model() and \ 173 | getattr(settings, 'MONGOENGINE_USER_DOCUMENT', '') == 'mongoengine.django.auth.User': 174 | site.register(User, MongoUserAdmin) 175 | -------------------------------------------------------------------------------- /mongoadmin/auth/forms.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | from django import forms 3 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 4 | 5 | from mongoengine.django.auth import User 6 | 7 | from mongodbforms import DocumentForm 8 | 9 | class UserCreationForm(DocumentForm): 10 | """ 11 | A form that creates a user, with no privileges, from the given username and 12 | password. 13 | """ 14 | error_messages = { 15 | 'duplicate_username': _("A user with that username already exists."), 16 | 'password_mismatch': _("The two password fields didn't match."), 17 | } 18 | username = forms.RegexField(label=_("Username"), max_length=30, 19 | regex=r'^[\w.@+-]+$', 20 | help_text=_("Required. 30 characters or fewer. Letters, digits and " 21 | "@/./+/-/_ only."), 22 | error_messages={ 23 | 'invalid': _("This value may contain only letters, numbers and " 24 | "@/./+/-/_ characters.")}) 25 | password1 = forms.CharField(label=_("Password"), 26 | widget=forms.PasswordInput) 27 | password2 = forms.CharField(label=_("Password confirmation"), 28 | widget=forms.PasswordInput, 29 | help_text=_("Enter the same password as above, for verification.")) 30 | 31 | class Meta: 32 | model = User 33 | fields = ("username",) 34 | 35 | def clean_username(self): 36 | # Since User.username is unique, this check is redundant, 37 | # but it sets a nicer error message than the ORM. See #13147. 38 | username = self.cleaned_data["username"] 39 | try: 40 | User.objects.get(username=username) 41 | except User.DoesNotExist: 42 | return username 43 | raise forms.ValidationError( 44 | self.error_messages['duplicate_username'], 45 | code='duplicate_username', 46 | ) 47 | 48 | def clean_password2(self): 49 | password1 = self.cleaned_data.get("password1") 50 | password2 = self.cleaned_data.get("password2") 51 | if password1 and password2 and password1 != password2: 52 | raise forms.ValidationError( 53 | self.error_messages['password_mismatch'], 54 | code='password_mismatch', 55 | ) 56 | return password2 57 | 58 | def save(self, commit=True): 59 | user = super(UserCreationForm, self).save(commit=False) 60 | self.instance = user.set_password(self.cleaned_data["password1"]) 61 | return self.instance 62 | 63 | 64 | class UserChangeForm(DocumentForm): 65 | username = forms.RegexField( 66 | label=_("Username"), max_length=30, regex=r"^[\w.@+-]+$", 67 | help_text=_("Required. 30 characters or fewer. Letters, digits and " 68 | "@/./+/-/_ only."), 69 | error_messages={ 70 | 'invalid': _("This value may contain only letters, numbers and " 71 | "@/./+/-/_ characters.")}) 72 | password = ReadOnlyPasswordHashField(label=_("Password"), 73 | help_text=_("Raw passwords are not stored, so there is no way to see " 74 | "this user's password, but you can change the password " 75 | "using this form.")) 76 | 77 | class Meta: 78 | model = User 79 | 80 | def __init__(self, *args, **kwargs): 81 | super(UserChangeForm, self).__init__(*args, **kwargs) 82 | f = self.fields.get('user_permissions', None) 83 | if f is not None: 84 | f.queryset = f.queryset.select_related('content_type') 85 | 86 | def clean_password(self): 87 | # Regardless of what the user provides, return the initial value. 88 | # This is done here, rather than on the field, because the 89 | # field does not have access to the initial value 90 | return self.initial["password"] 91 | 92 | def clean_email(self): 93 | email = self.cleaned_data.get("email") 94 | if email == '': 95 | return None 96 | return email 97 | -------------------------------------------------------------------------------- /mongoadmin/contenttypes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschrewe/django-mongoadmin/c53b8a0e7d3b96c9dd03126576b53ec9602f0a20/mongoadmin/contenttypes/__init__.py -------------------------------------------------------------------------------- /mongoadmin/contenttypes/models.py: -------------------------------------------------------------------------------- 1 | from .utils import has_rel_db, get_model_or_document 2 | 3 | if has_rel_db(): 4 | from django.contrib.contenttypes.models import ContentType, ContentTypeManager 5 | else: 6 | from django.contrib.contenttypes.models import ContentTypeManager as DjangoContentTypeManager 7 | 8 | from mongoengine.queryset import QuerySet 9 | from mongoengine.django.auth import ContentType 10 | 11 | from mongodbforms import init_document_options 12 | from mongodbforms.documentoptions import patch_document 13 | 14 | 15 | class ContentTypeManager(DjangoContentTypeManager): 16 | def get_query_set(self): 17 | """Returns a new QuerySet object. Subclasses can override this method 18 | to easily customize the behavior of the Manager. 19 | """ 20 | return QuerySet(self.model, self.model._get_collection()) 21 | 22 | def contribute_to_class(self, model, name): 23 | init_document_options(model) 24 | super(ContentTypeManager, self).contribute_to_class(model, name) 25 | 26 | def get_object_for_this_type(self, **kwargs): 27 | """ 28 | Returns an object of this type for the keyword arguments given. 29 | Basically, this is a proxy around this object_type's get_object() model 30 | method. The ObjectNotExist exception, if thrown, will not be caught, 31 | so code that calls this method should catch it. 32 | """ 33 | return self.model_class().objects.get(**kwargs) 34 | 35 | def model_class(self): 36 | return get_model_or_document(str(self.app_label), str(self.model)) 37 | 38 | patch_document(get_object_for_this_type, ContentType, bound=False) 39 | patch_document(model_class, ContentType, bound=False) 40 | 41 | manager = ContentTypeManager() 42 | manager.contribute_to_class(ContentType, 'objects') 43 | 44 | try: 45 | from grappelli.templatetags import grp_tags 46 | grp_tags.ContentType = ContentType 47 | except ImportError: 48 | pass 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /mongoadmin/contenttypes/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.conf import settings 4 | from django.db.models import get_model 5 | 6 | from mongoengine.base.common import _document_registry 7 | 8 | # if there is a relational db and we can load a content type 9 | # object from it, we simply export Django's stuff and are done. 10 | # Otherwise we roll our own (mostly) compatible version 11 | # using mongoengine. 12 | 13 | def has_rel_db(): 14 | if not getattr(settings, 'MONGOADMIN_CHECK_CONTENTTYPE', True): 15 | return True 16 | 17 | engine = settings.DATABASES.get('default', {}).get('ENGINE', 'django.db.backends.dummy') 18 | if engine.endswith('dummy'): 19 | return False 20 | return True 21 | 22 | def get_model_or_document(app_label, model): 23 | if has_rel_db(): 24 | return get_model(app_label, model, only_installed=False) 25 | else: 26 | # mongoengine's document registry is case sensitive 27 | # while all models are stored in lowercase in the 28 | # content types. So we can't use get_document. 29 | model = str(model).lower() 30 | possible_docs = [v for k, v in _document_registry.items() if k.lower() == model] 31 | if len(possible_docs) == 1: 32 | return possible_docs[0] 33 | if len(possible_docs) > 1: 34 | for doc in possible_docs: 35 | module = sys.modules[doc.__module__] 36 | doc_app_label = module.__name__.split('.')[-2] 37 | if doc_app_label.lower() == app_label.lower(): 38 | return doc 39 | return None -------------------------------------------------------------------------------- /mongoadmin/contenttypes/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django import http 4 | from django.contrib.sites.models import Site, get_current_site 5 | from django.utils.translation import ugettext as _ 6 | 7 | from mongoadmin.contenttypes.models import ContentType 8 | 9 | def shortcut(request, content_type_id, object_id): 10 | """ 11 | Redirect to an object's page based on a content-type ID and an object ID. 12 | """ 13 | # Look up the object, making sure it's got a get_absolute_url() function. 14 | try: 15 | content_type = ContentType.objects.get(pk=content_type_id) 16 | except (ContentType.DoesNotExist, ValueError): 17 | raise http.Http404(_("Content type %(ct_id)s object %(obj_id)s doesn't exist") % 18 | {'ct_id': content_type_id, 'obj_id': object_id}) 19 | 20 | if not content_type.model_class(): 21 | raise http.Http404(_("Content type %(ct_id)s object has no associated model") % 22 | {'ct_id': content_type_id}) 23 | try: 24 | obj = content_type.get_object_for_this_type(pk=object_id) 25 | except (content_type.model_class().DoesNotExist, ValueError): 26 | raise http.Http404(_("Content type %(ct_id)s object %(obj_id)s doesn't exist") % 27 | {'ct_id': content_type_id, 'obj_id': object_id}) 28 | 29 | try: 30 | get_absolute_url = obj.get_absolute_url 31 | except AttributeError: 32 | raise http.Http404(_("%(ct_name)s objects don't have a get_absolute_url() method") % 33 | {'ct_name': content_type.name}) 34 | absurl = get_absolute_url() 35 | 36 | # Try to figure out the object's domain, so we can do a cross-site redirect 37 | # if necessary. 38 | 39 | # If the object actually defines a domain, we're done. 40 | if absurl.startswith('http://') or absurl.startswith('https://'): 41 | return http.HttpResponseRedirect(absurl) 42 | 43 | # Otherwise, we need to introspect the object's relationships for a 44 | # relation to the Site object 45 | object_domain = None 46 | 47 | if Site._meta.installed: 48 | opts = obj._meta 49 | 50 | # First, look for an many-to-many relationship to Site. 51 | for field in opts.many_to_many: 52 | if field.rel.to is Site: 53 | try: 54 | # Caveat: In the case of multiple related Sites, this just 55 | # selects the *first* one, which is arbitrary. 56 | object_domain = getattr(obj, field.name).all()[0].domain 57 | except IndexError: 58 | pass 59 | if object_domain is not None: 60 | break 61 | 62 | # Next, look for a many-to-one relationship to Site. 63 | if object_domain is None: 64 | for field in obj._meta.fields: 65 | if field.rel and field.rel.to is Site: 66 | try: 67 | object_domain = getattr(obj, field.name).domain 68 | except Site.DoesNotExist: 69 | pass 70 | if object_domain is not None: 71 | break 72 | 73 | # Fall back to the current site (if possible). 74 | if object_domain is None: 75 | try: 76 | object_domain = get_current_site(request).domain 77 | except Site.DoesNotExist: 78 | pass 79 | 80 | # If all that malarkey found an object domain, use it. Otherwise, fall back 81 | # to whatever get_absolute_url() returned. 82 | if object_domain is not None: 83 | protocol = 'https' if request.is_secure() else 'http' 84 | return http.HttpResponseRedirect('%s://%s%s' 85 | % (protocol, object_domain, absurl)) 86 | else: 87 | return http.HttpResponseRedirect(absurl) -------------------------------------------------------------------------------- /mongoadmin/mongohelpers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.helpers import InlineAdminForm as DjangoInlineAdminForm 2 | from django.contrib.admin.helpers import InlineAdminFormSet as DjangoInlineAdminFormSet 3 | from django.contrib.admin.helpers import AdminForm 4 | 5 | class InlineAdminFormSet(DjangoInlineAdminFormSet): 6 | """ 7 | A wrapper around an inline formset for use in the admin system. 8 | """ 9 | def __iter__(self): 10 | for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): 11 | yield InlineAdminForm(self.formset, form, self.fieldsets, 12 | self.opts.prepopulated_fields, original, self.readonly_fields, 13 | model_admin=self.opts) 14 | for form in self.formset.extra_forms: 15 | yield InlineAdminForm(self.formset, form, self.fieldsets, 16 | self.opts.prepopulated_fields, None, self.readonly_fields, 17 | model_admin=self.opts) 18 | yield InlineAdminForm(self.formset, self.formset.empty_form, 19 | self.fieldsets, self.opts.prepopulated_fields, None, 20 | self.readonly_fields, model_admin=self.opts) 21 | 22 | 23 | 24 | class InlineAdminForm(DjangoInlineAdminForm): 25 | """ 26 | A wrapper around an inline form for use in the admin system. 27 | """ 28 | def __init__(self, formset, form, fieldsets, prepopulated_fields, original, 29 | readonly_fields=None, model_admin=None): 30 | self.formset = formset 31 | self.model_admin = model_admin 32 | self.original = original 33 | self.show_url = original and hasattr(original, 'get_absolute_url') 34 | AdminForm.__init__(self, form, fieldsets, prepopulated_fields, 35 | readonly_fields, model_admin) 36 | 37 | def pk_field(self): 38 | # if there is no pk field then it's an embedded form so return none 39 | if hasattr(self.formset, "_pk_field"): 40 | return super(InlineAdminForm, self).pk_field() 41 | else: 42 | return None 43 | 44 | 45 | -------------------------------------------------------------------------------- /mongoadmin/options.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from functools import partial 3 | 4 | from django import forms 5 | from django.forms.models import modelform_defines_fields 6 | from django.contrib.admin.options import ModelAdmin, InlineModelAdmin, get_ul_class 7 | from django.contrib.admin import widgets 8 | from django.contrib.admin.util import flatten_fieldsets 9 | from django.core.exceptions import FieldError, ValidationError 10 | from django.forms.formsets import DELETION_FIELD_NAME 11 | from django.utils.translation import ugettext as _ 12 | from django.contrib.admin.util import NestedObjects 13 | from django.utils.text import get_text_list 14 | 15 | from mongoengine.fields import (DateTimeField, URLField, IntField, ListField, EmbeddedDocumentField, 16 | ReferenceField, StringField, FileField, ImageField) 17 | 18 | from mongodbforms.documents import documentform_factory, embeddedformset_factory, DocumentForm, EmbeddedDocumentFormSet, EmbeddedDocumentForm 19 | from mongodbforms.util import load_field_generator, init_document_options 20 | 21 | from mongoadmin.util import RelationWrapper, is_django_user_model 22 | from mongoadmin.widgets import ReferenceRawIdWidget, MultiReferenceRawIdWidget 23 | 24 | # Defaults for formfield_overrides. ModelAdmin subclasses can change this 25 | # by adding to ModelAdmin.formfield_overrides. 26 | FORMFIELD_FOR_DBFIELD_DEFAULTS = { 27 | DateTimeField: { 28 | 'form_class': forms.SplitDateTimeField, 29 | 'widget': widgets.AdminSplitDateTime 30 | }, 31 | URLField: {'widget': widgets.AdminURLFieldWidget}, 32 | IntField: {'widget': widgets.AdminIntegerFieldWidget}, 33 | ImageField: {'widget': widgets.AdminFileWidget}, 34 | FileField: {'widget': widgets.AdminFileWidget}, 35 | } 36 | 37 | _fieldgenerator = load_field_generator()() 38 | 39 | 40 | def formfield(field, form_class=None, **kwargs): 41 | """ 42 | Returns a django.forms.Field instance for this database Field. 43 | """ 44 | defaults = {'required': field.required} 45 | if field.default is not None: 46 | if isinstance(field.default, collections.Callable): 47 | defaults['initial'] = field.default() 48 | defaults['show_hidden_initial'] = True 49 | else: 50 | defaults['initial'] = field.default 51 | 52 | if field.choices is not None: 53 | # Many of the subclass-specific formfield arguments (min_value, 54 | # max_value) don't apply for choice fields, so be sure to only pass 55 | # the values that TypedChoiceField will understand. 56 | for k in list(kwargs.keys()): 57 | if k not in ('coerce', 'empty_value', 'choices', 'required', 58 | 'widget', 'label', 'initial', 'help_text', 59 | 'error_messages', 'show_hidden_initial'): 60 | del kwargs[k] 61 | 62 | defaults.update(kwargs) 63 | 64 | if form_class is not None: 65 | return form_class(**defaults) 66 | return _fieldgenerator.generate(field, **defaults) 67 | 68 | 69 | class MongoFormFieldMixin(object): 70 | 71 | def formfield_for_dbfield(self, db_field, **kwargs): 72 | """ 73 | Hook for specifying the form Field instance for a given database Field 74 | instance. 75 | 76 | If kwargs are given, they're passed to the form Field's constructor. 77 | """ 78 | request = kwargs.pop("request", None) 79 | 80 | # If the field specifies choices, we don't need to look for special 81 | # admin widgets - we just need to use a select widget of some kind. 82 | if db_field.choices is not None: 83 | return self.formfield_for_choice_field(db_field, request, **kwargs) 84 | 85 | if isinstance(db_field, ListField) and isinstance(db_field.field, ReferenceField): 86 | return self.formfield_for_reference_listfield(db_field, request, **kwargs) 87 | 88 | # handle RelatedFields 89 | if isinstance(db_field, ReferenceField): 90 | # For non-raw_id fields, wrap the widget with a wrapper that adds 91 | # extra HTML -- the "add other" interface -- to the end of the 92 | # rendered output. formfield can be None if it came from a 93 | # OneToOneField with parent_link=True or a M2M intermediary. 94 | form_field = self._get_formfield(db_field, **kwargs) 95 | if db_field.name not in self.raw_id_fields: 96 | related_modeladmin = self.admin_site._registry.get( 97 | db_field.document_type) 98 | can_add_related = bool(related_modeladmin and 99 | related_modeladmin.has_add_permission(request)) 100 | form_field.widget = widgets.RelatedFieldWidgetWrapper( 101 | form_field.widget, RelationWrapper( 102 | db_field.document_type), self.admin_site, 103 | can_add_related=can_add_related) 104 | return form_field 105 | elif db_field.name in self.raw_id_fields: 106 | kwargs['widget'] = ReferenceRawIdWidget( 107 | db_field.rel, self.admin_site) 108 | return self._get_formfield(db_field, **kwargs) 109 | 110 | if isinstance(db_field, StringField): 111 | if db_field.max_length is None: 112 | kwargs = dict( 113 | {'widget': widgets.AdminTextareaWidget}, **kwargs) 114 | else: 115 | kwargs = dict( 116 | {'widget': widgets.AdminTextInputWidget}, **kwargs) 117 | return self._get_formfield(db_field, **kwargs) 118 | 119 | # For any other type of field, just call its formfield() method. 120 | return self._get_formfield(db_field, **kwargs) 121 | 122 | def _get_formfield(self, db_field, **kwargs): 123 | """Return overridden formfield if exists, otherwise default formfield""" 124 | # If we've got overrides for the formfield defined, use 'em. **kwargs 125 | # passed to formfield_for_dbfield override the defaults. 126 | for klass in db_field.__class__.mro(): 127 | if klass in self.formfield_overrides: 128 | kwargs.update(self.formfield_overrides[klass]) 129 | break 130 | return formfield(db_field, **kwargs) 131 | 132 | def formfield_for_choice_field(self, db_field, request=None, **kwargs): 133 | """ 134 | Get a form Field for a database Field that has declared choices. 135 | """ 136 | # If the field is named as a radio_field, use a RadioSelect 137 | if db_field.name in self.radio_fields: 138 | # Avoid stomping on custom widget/choices arguments. 139 | if 'widget' not in kwargs: 140 | kwargs['widget'] = widgets.AdminRadioSelect(attrs={ 141 | 'class': get_ul_class(self.radio_fields[db_field.name]), 142 | }) 143 | if 'choices' not in kwargs: 144 | kwargs['choices'] = db_field.get_choices( 145 | include_blank=db_field.blank, 146 | blank_choice=[('', _('None'))] 147 | ) 148 | return formfield(db_field, **kwargs) 149 | 150 | def formfield_for_reference_listfield(self, db_field, request=None, **kwargs): 151 | """ 152 | Get a form Field for a ManyToManyField. 153 | """ 154 | if db_field.name in self.raw_id_fields: 155 | kwargs['widget'] = MultiReferenceRawIdWidget( 156 | db_field.field.rel, self.admin_site) 157 | kwargs['help_text'] = '' 158 | elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): 159 | kwargs['widget'] = widgets.FilteredSelectMultiple( 160 | forms.forms.pretty_name(db_field.name), (db_field.name in self.filter_vertical)) 161 | 162 | return formfield(db_field, **kwargs) 163 | 164 | 165 | class DocumentAdmin(MongoFormFieldMixin, ModelAdmin): 166 | change_list_template = "admin/change_document_list.html" 167 | form = DocumentForm 168 | 169 | _embedded_inlines = None 170 | 171 | def __init__(self, model, admin_site): 172 | super(DocumentAdmin, self).__init__(model, admin_site) 173 | 174 | self.inlines = self._find_embedded_inlines() 175 | 176 | def _find_embedded_inlines(self): 177 | emb_inlines = [] 178 | exclude = self.exclude or [] 179 | for name in self.model._fields_ordered: 180 | f = self.model._fields.get(name) 181 | if not (isinstance(f, ListField) and isinstance(getattr(f, 'field', None), EmbeddedDocumentField)) and not isinstance(f, EmbeddedDocumentField): 182 | continue 183 | # Should only reach here if there is an embedded document... 184 | if f.name in exclude: 185 | continue 186 | if hasattr(f, 'field') and f.field is not None: 187 | embedded_document = f.field.document_type 188 | elif hasattr(f, 'document_type'): 189 | embedded_document = f.document_type 190 | else: 191 | # For some reason we found an embedded field were either 192 | # the field attribute or the field's document type is None. 193 | # This shouldn't happen, but apparently does happen: 194 | # https://github.com/jschrewe/django-mongoadmin/issues/4 195 | # The solution for now is to ignore that field entirely. 196 | continue 197 | 198 | init_document_options(embedded_document) 199 | 200 | embedded_admin_base = EmbeddedStackedDocumentInline 201 | embedded_admin_name = "%sAdmin" % embedded_document.__name__ 202 | inline_attrs = { 203 | 'model': embedded_document, 204 | 'parent_field_name': f.name, 205 | } 206 | # if f is an EmbeddedDocumentField set the maximum allowed form 207 | # instances to one 208 | if isinstance(f, EmbeddedDocumentField): 209 | inline_attrs['max_num'] = 1 210 | embedded_admin = type( 211 | embedded_admin_name, (embedded_admin_base,), inline_attrs 212 | ) 213 | # check if there is an admin for the embedded document in 214 | # self.inlines. If there is, use this, else use default. 215 | for inline_class in self.inlines: 216 | if inline_class.document == embedded_document: 217 | embedded_admin = inline_class 218 | emb_inlines.append(embedded_admin) 219 | 220 | if f.name not in exclude: 221 | exclude.append(f.name) 222 | 223 | # sort out the declared inlines. Embedded admins take a different 224 | # set of arguments for init and are stored seperately. So the 225 | # embedded stuff has to be removed from self.inlines here 226 | inlines = [i for i in self.inlines if i not in emb_inlines] 227 | 228 | self.exclude = exclude 229 | 230 | return inlines + emb_inlines 231 | 232 | def get_queryset(self, request): 233 | """ 234 | Returns a QuerySet of all model instances that can be edited by the 235 | admin site. This is used by changelist_view. 236 | """ 237 | qs = self.model.objects.clone() 238 | # TODO: this should be handled by some parameter to the ChangeList. 239 | ordering = self.get_ordering(request) 240 | if ordering: 241 | qs = qs.order_by(*ordering) 242 | return qs 243 | 244 | def get_changelist(self, request, **kwargs): 245 | """ 246 | Returns the ChangeList class for use on the changelist page. 247 | """ 248 | from mongoadmin.views import DocumentChangeList 249 | return DocumentChangeList 250 | 251 | def get_object(self, request, object_id, from_field=None): 252 | """ 253 | Returns an instance matching the primary key provided. ``None`` is 254 | returned if no match is found (or the object_id failed validation 255 | against the primary key field). 256 | """ 257 | queryset = self.get_queryset(request) 258 | model = queryset._document 259 | field = model._meta.pk if from_field is None else model._meta.get_field(from_field) 260 | try: 261 | object_id = field.to_python(object_id) 262 | return queryset.get(**{field.name: object_id}) 263 | except (model.DoesNotExist, ValidationError, ValueError): 264 | return None 265 | 266 | def get_form(self, request, obj=None, **kwargs): 267 | """ 268 | Returns a Form class for use in the admin add view. This is used by 269 | add_view and change_view. 270 | """ 271 | if 'fields' in kwargs: 272 | fields = kwargs.pop('fields') 273 | else: 274 | fields = flatten_fieldsets(self.get_fieldsets(request, obj)) 275 | if self.exclude is None: 276 | exclude = [] 277 | else: 278 | exclude = list(self.exclude) 279 | exclude.extend(self.get_readonly_fields(request, obj)) 280 | if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude: 281 | # Take the custom ModelForm's Meta.exclude into account only if the 282 | # ModelAdmin doesn't define its own. 283 | exclude.extend(self.form._meta.exclude) 284 | # if exclude is an empty list we pass None to be consistent with the 285 | # default on modelform_factory 286 | exclude = exclude or None 287 | defaults = { 288 | "form": self.form, 289 | "fields": fields, 290 | "exclude": exclude, 291 | "formfield_callback": partial(self.formfield_for_dbfield, request=request), 292 | } 293 | defaults.update(kwargs) 294 | 295 | if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): 296 | defaults['fields'] = None 297 | 298 | try: 299 | return documentform_factory(self.model, **defaults) 300 | except FieldError as e: 301 | raise FieldError('%s. Check fields/fieldsets/exclude attributes of class %s.' 302 | % (e, self.__class__.__name__)) 303 | 304 | def save_related(self, request, form, formsets, change): 305 | """ 306 | Given the ``HttpRequest``, the parent ``ModelForm`` instance, the 307 | list of inline formsets and a boolean value based on whether the 308 | parent is being added or changed, save the related objects to the 309 | database. Note that at this point save_form() and save_model() have 310 | already been called. 311 | """ 312 | for formset in formsets: 313 | self.save_formset(request, form, formset, change=change) 314 | 315 | def log_addition(self, request, object): 316 | """ 317 | Log that an object has been successfully added. 318 | 319 | The default implementation creates an admin LogEntry object. 320 | """ 321 | if not is_django_user_model(request.user): 322 | return 323 | 324 | super(DocumentAdmin, self).log_addition(request=request, object=object) 325 | 326 | def log_change(self, request, object, message): 327 | """ 328 | Log that an object has been successfully changed. 329 | 330 | The default implementation creates an admin LogEntry object. 331 | """ 332 | if not is_django_user_model(request.user): 333 | return 334 | 335 | super(DocumentAdmin, self).log_change( 336 | request=request, object=object, message=message) 337 | 338 | def log_deletion(self, request, object, object_repr): 339 | """ 340 | Log that an object has been successfully changed. 341 | 342 | The default implementation creates an admin LogEntry object. 343 | """ 344 | if not is_django_user_model(request.user): 345 | return 346 | 347 | super(DocumentAdmin, self).log_deletion( 348 | request=request, object=object, object_repr=object_repr) 349 | 350 | 351 | class EmbeddedInlineAdmin(MongoFormFieldMixin, InlineModelAdmin): 352 | parent_field_name = None 353 | formset = EmbeddedDocumentFormSet 354 | form = EmbeddedDocumentForm 355 | 356 | def get_queryset(self, request): 357 | """ 358 | Returns a QuerySet of all model instances that can be edited by the 359 | admin site. This is used by changelist_view. 360 | """ 361 | return getattr(self.parent_model, self.parent_field_name, []) 362 | 363 | def get_formset(self, request, obj=None, **kwargs): 364 | """Returns a BaseInlineFormSet class for use in admin add/change views.""" 365 | 366 | if 'fields' in kwargs: 367 | fields = kwargs.pop('fields') 368 | else: 369 | fields = flatten_fieldsets(self.get_fieldsets(request, obj)) 370 | if self.exclude is None: 371 | exclude = [] 372 | else: 373 | exclude = list(self.exclude) 374 | exclude.extend(self.get_readonly_fields(request, obj)) 375 | if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude: 376 | # Take the custom ModelForm's Meta.exclude into account only if the 377 | # InlineModelAdmin doesn't define its own. 378 | exclude.extend(self.form._meta.exclude) 379 | # if exclude is an empty list we use None, since that's the actual 380 | # default 381 | exclude = exclude or None 382 | can_delete = self.can_delete and self.has_delete_permission(request, obj) 383 | defaults = { 384 | "form": self.form, 385 | "formset": self.formset, 386 | "embedded_name": self.parent_field_name, 387 | "fields": fields, 388 | "exclude": exclude, 389 | "formfield_callback": partial(self.formfield_for_dbfield, request=request), 390 | "extra": self.get_extra(request, obj, **kwargs), 391 | "max_num": self.get_max_num(request, obj, **kwargs), 392 | "can_delete": can_delete, 393 | } 394 | 395 | defaults.update(kwargs) 396 | base_model_form = defaults['form'] 397 | 398 | class DeleteProtectedModelForm(base_model_form): 399 | 400 | def hand_clean_DELETE(self): 401 | """ 402 | We don't validate the 'DELETE' field itself because on 403 | templates it's not rendered using the field information, but 404 | just using a generic "deletion_field" of the InlineModelAdmin. 405 | """ 406 | if self.cleaned_data.get(DELETION_FIELD_NAME, False): 407 | collector = NestedObjects() 408 | collector.collect([self.instance]) 409 | if collector.protected: 410 | objs = [] 411 | for p in collector.protected: 412 | objs.append( 413 | # Translators: Model verbose name and instance 414 | # representation, suitable to be an item in a 415 | # list 416 | _('%(class_name)s %(instance)s') % { 417 | 'class_name': p._meta.verbose_name, 418 | 'instance': p} 419 | ) 420 | params = {'class_name': self._meta.model._meta.verbose_name, 421 | 'instance': self.instance, 422 | 'related_objects': get_text_list(objs, _('and'))} 423 | msg = _("Deleting %(class_name)s %(instance)s would require " 424 | "deleting the following protected related objects: " 425 | "%(related_objects)s") 426 | raise ValidationError( 427 | msg, code='deleting_protected', params=params) 428 | 429 | def is_valid(self): 430 | result = super(DeleteProtectedModelForm, self).is_valid() 431 | self.hand_clean_DELETE() 432 | return result 433 | 434 | defaults['form'] = DeleteProtectedModelForm 435 | 436 | if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): 437 | defaults['fields'] = None 438 | 439 | return embeddedformset_factory(self.model, self.parent_model, **defaults) 440 | 441 | 442 | class EmbeddedStackedDocumentInline(EmbeddedInlineAdmin): 443 | template = 'admin/edit_inline/stacked.html' 444 | 445 | 446 | class EmbeddedTabularDocumentInline(EmbeddedInlineAdmin): 447 | template = 'admin/edit_inline/tabular.html' 448 | -------------------------------------------------------------------------------- /mongoadmin/sites.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import ModelAdmin 2 | from django.db.models.base import ModelBase 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.contrib.admin.sites import (AdminSite, NotRegistered, 5 | AlreadyRegistered) 6 | 7 | from mongoengine.base import TopLevelDocumentMetaclass 8 | 9 | from mongodbforms import init_document_options 10 | 11 | from mongoadmin import DocumentAdmin 12 | 13 | LOGIN_FORM_KEY = 'this_is_the_login_form' 14 | 15 | 16 | class MongoAdminSite(AdminSite): 17 | 18 | """ 19 | An AdminSite object encapsulates an instance of the Django admin 20 | application, ready to be hooked in to your URLconf. Models are registered 21 | with the AdminSite using the register() method, and the get_urls() method 22 | can then be used to access Django view functions that present a full admin 23 | interface for the collection of registered models. 24 | """ 25 | 26 | def register(self, model_or_iterable, admin_class=None, **options): 27 | """ 28 | Registers the given model(s) with the given admin class. 29 | 30 | The model(s) should be Model classes, not instances. 31 | 32 | If an admin class isn't given, it will use ModelAdmin (the default 33 | admin options). If keyword arguments are given -- e.g., list_display -- 34 | they'll be applied as options to the admin class. 35 | 36 | If a model is already registered, this will raise AlreadyRegistered. 37 | 38 | If a model is abstract, this will raise ImproperlyConfigured. 39 | """ 40 | if isinstance(model_or_iterable, ModelBase) and not admin_class: 41 | admin_class = ModelAdmin 42 | 43 | if isinstance(model_or_iterable, TopLevelDocumentMetaclass) and not admin_class: 44 | admin_class = DocumentAdmin 45 | 46 | # Don't import the humongous validation code unless required 47 | # if admin_class and settings.DEBUG: 48 | # from mongoadmin.validation import validate 49 | # else: 50 | validate = lambda model, adminclass: None 51 | 52 | if isinstance(model_or_iterable, ModelBase) or \ 53 | isinstance(model_or_iterable, TopLevelDocumentMetaclass): 54 | model_or_iterable = [model_or_iterable] 55 | 56 | for model in model_or_iterable: 57 | if isinstance(model, TopLevelDocumentMetaclass): 58 | init_document_options(model) 59 | 60 | if hasattr(model._meta, 'abstract') and model._meta.abstract: 61 | raise ImproperlyConfigured('The model %s is abstract, so it ' 62 | 'cannot be registered with admin.' % model.__name__) 63 | 64 | if model in self._registry: 65 | raise AlreadyRegistered( 66 | 'The model %s is already registered' % model.__name__) 67 | 68 | # Ignore the registration if the model has been 69 | # swapped out. 70 | if model._meta.swapped: 71 | continue 72 | 73 | # If we got **options then dynamically construct a subclass of 74 | # admin_class with those **options. 75 | if options: 76 | # For reasons I don't quite understand, without a __module__ 77 | # the created class appears to "live" in the wrong place, 78 | # which causes issues later on. 79 | options['__module__'] = __name__ 80 | admin_class = type( 81 | "%sAdmin" % model.__name__, (admin_class,), options) 82 | 83 | # Validate (which might be a no-op) 84 | validate(admin_class, model) 85 | 86 | # Instantiate the admin class to save in the registry 87 | self._registry[model] = admin_class(model, self) 88 | 89 | def unregister(self, model_or_iterable): 90 | """ 91 | Unregisters the given model(s). 92 | 93 | If a model isn't already registered, this will raise NotRegistered. 94 | """ 95 | if isinstance(model_or_iterable, ModelBase) or \ 96 | isinstance(model_or_iterable, TopLevelDocumentMetaclass): 97 | model_or_iterable = [model_or_iterable] 98 | for model in model_or_iterable: 99 | if model not in self._registry: 100 | raise NotRegistered( 101 | 'The model %s is not registered' % model.__name__) 102 | del self._registry[model] 103 | 104 | 105 | # This global object represents the default admin site, for the common case. 106 | # You can instantiate AdminSite in your own code to create a custom admin site. 107 | site = MongoAdminSite() 108 | -------------------------------------------------------------------------------- /mongoadmin/templates/admin/change_document_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | 3 | {% load documenttags %} 4 | {% load admin_list %} 5 | {% load mongoadmintags %} 6 | 7 | {% block result_list %} 8 | {% check_grappelli as is_grappelli %} 9 | {% if not is_grappelli and action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} 10 | {% document_result_list cl %} 11 | {% if not is_grappelli and action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /mongoadmin/templates/admin/mongo_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_urls admin_static admin_modify %} 3 | 4 | 5 | {% block object-tools-items %} 6 |
  • {% trans "History" %}
  • 7 | {% if has_absolute_url %}
  • {% trans "View on site" %}
  • {% endif%} 8 | {% endblock %} -------------------------------------------------------------------------------- /mongoadmin/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | import django.contrib.admin.templatetags.log 2 | from django.contrib.admin.models import LogEntry 3 | 4 | from mongoadmin.util import is_django_user_model 5 | 6 | class AdminLogNode(django.template.Node): 7 | def __init__(self, limit, varname, user): 8 | self.limit, self.varname, self.user = limit, varname, user 9 | 10 | def __repr__(self): 11 | return "" 12 | 13 | def render(self, context): 14 | if not is_django_user_model(self.user): 15 | context[self.varname] = None 16 | elif self.user is None: 17 | context[self.varname] = LogEntry.objects.all().select_related('content_type', 'user')[:self.limit] 18 | else: 19 | user_id = self.user 20 | if not user_id.isdigit(): 21 | user_id = context[self.user].pk 22 | context[self.varname] = LogEntry.objects.filter(user__pk__exact=user_id).select_related('content_type', 'user')[:int(self.limit)] 23 | return '' 24 | 25 | django.contrib.admin.templatetags.log.AdminLogNode = AdminLogNode -------------------------------------------------------------------------------- /mongoadmin/templatetags/documenttags.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | from django.contrib.admin.templatetags.admin_list import (result_hidden_fields, ResultList, items_for_result, 3 | result_headers) 4 | from django.db.models.fields import FieldDoesNotExist 5 | 6 | from mongodbforms.documentoptions import patch_document 7 | 8 | register = Library() 9 | 10 | def serializable_value(self, field_name): 11 | """ 12 | Returns the value of the field name for this instance. If the field is 13 | a foreign key, returns the id value, instead of the object. If there's 14 | no Field object with this name on the model, the model attribute's 15 | value is returned directly. 16 | 17 | Used to serialize a field's value (in the serializer, or form output, 18 | for example). Normally, you would just access the attribute directly 19 | and not use this method. 20 | """ 21 | try: 22 | field = self._meta.get_field_by_name(field_name)[0] 23 | except FieldDoesNotExist: 24 | return getattr(self, field_name) 25 | return getattr(self, field.name) 26 | 27 | def results(cl): 28 | """ 29 | Just like the one from Django. Only we add a serializable_value method to 30 | the document, because Django expects it and mongoengine doesn't have it. 31 | """ 32 | if cl.formset: 33 | for res, form in zip(cl.result_list, cl.formset.forms): 34 | patch_document(serializable_value, res) 35 | yield ResultList(form, items_for_result(cl, res, form)) 36 | else: 37 | for res in cl.result_list: 38 | patch_document(serializable_value, res) 39 | yield ResultList(None, items_for_result(cl, res, None)) 40 | 41 | def document_result_list(cl): 42 | """ 43 | Displays the headers and data list together 44 | """ 45 | headers = list(result_headers(cl)) 46 | try: 47 | num_sorted_fields = 0 48 | for h in headers: 49 | if h['sortable'] and h['sorted']: 50 | num_sorted_fields += 1 51 | except KeyError: 52 | pass 53 | 54 | return {'cl': cl, 55 | 'result_hidden_fields': list(result_hidden_fields(cl)), 56 | 'result_headers': headers, 57 | 'num_sorted_fields': num_sorted_fields, 58 | 'results': list(results(cl))} 59 | result_list = register.inclusion_tag("admin/change_list_results.html")(document_result_list) 60 | 61 | 62 | -------------------------------------------------------------------------------- /mongoadmin/templatetags/mongoadmintags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | register = template.Library() 5 | 6 | class CheckGrappelli(template.Node): 7 | def __init__(self, var_name): 8 | self.var_name = var_name 9 | def render(self, context): 10 | context[self.var_name] = 'grappelli' in settings.INSTALLED_APPS 11 | return '' 12 | 13 | def check_grappelli(parser, token): 14 | """ 15 | Checks weather grappelli is in installed apps and sets a variable in the context. 16 | Unfortunately there is no other way to find out if grappelli is used or not. 17 | See: https://github.com/sehmaschine/django-grappelli/issues/32 18 | 19 | Usage: {% check_grappelli as %} 20 | """ 21 | 22 | bits = token.contents.split() 23 | 24 | if len(bits) != 3: 25 | raise template.TemplateSyntaxError("'check_grappelli' tag takes exactly two arguments.") 26 | 27 | if bits[1] != 'as': 28 | raise template.TemplateSyntaxError("The second argument to 'check_grappelli' must be 'as'") 29 | varname = bits[2] 30 | 31 | return CheckGrappelli(varname) 32 | 33 | register.tag(check_grappelli) 34 | -------------------------------------------------------------------------------- /mongoadmin/util.py: -------------------------------------------------------------------------------- 1 | from django.utils.encoding import smart_str 2 | try: 3 | from django.utils.encoding import force_text as force_unicode 4 | except ImportError: 5 | from django.utils.encoding import force_unicode 6 | try: 7 | from django.utils.encoding import smart_text as smart_unicode 8 | except ImportError: 9 | try: 10 | from django.utils.encoding import smart_unicode 11 | except ImportError: 12 | from django.forms.util import smart_unicode 13 | 14 | from django.forms.forms import pretty_name 15 | from django.db.models.fields import FieldDoesNotExist 16 | from django.utils import formats 17 | 18 | from mongoengine import fields 19 | 20 | from mongodbforms.util import init_document_options 21 | import collections 22 | 23 | class RelationWrapper(object): 24 | """ 25 | Wraps a document referenced from a ReferenceField with an Interface similiar to 26 | django's ForeignKeyField.rel 27 | """ 28 | def __init__(self, document): 29 | self.to = init_document_options(document) 30 | 31 | def is_django_user_model(user): 32 | """ 33 | Checks if a user model is compatible with Django's 34 | recent changes. Django requires User models to have 35 | an int pk, so we check here if it has (mongoengine hasn't) 36 | """ 37 | try: 38 | if hasattr(user, 'pk'): 39 | int(user.pk) 40 | else: 41 | int(user) 42 | except (ValueError, TypeError): 43 | return False 44 | return True 45 | 46 | def label_for_field(name, model, model_admin=None, return_attr=False): 47 | attr = None 48 | model._meta = init_document_options(model) 49 | try: 50 | field = model._meta.get_field_by_name(name)[0] 51 | label = field.name.replace('_', ' ') 52 | except FieldDoesNotExist: 53 | if name == "__unicode__": 54 | label = force_unicode(model._meta.verbose_name) 55 | elif name == "__str__": 56 | label = smart_str(model._meta.verbose_name) 57 | else: 58 | if isinstance(name, collections.Callable): 59 | attr = name 60 | elif model_admin is not None and hasattr(model_admin, name): 61 | attr = getattr(model_admin, name) 62 | elif hasattr(model, name): 63 | attr = getattr(model, name) 64 | else: 65 | message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name) 66 | if model_admin: 67 | message += " or %s" % (model_admin.__class__.__name__,) 68 | raise AttributeError(message) 69 | 70 | 71 | if hasattr(attr, "short_description"): 72 | label = attr.short_description 73 | elif isinstance(attr, collections.Callable): 74 | if attr.__name__ == "": 75 | label = "--" 76 | else: 77 | label = pretty_name(attr.__name__) 78 | else: 79 | label = pretty_name(name) 80 | if return_attr: 81 | return (label, attr) 82 | else: 83 | return label 84 | 85 | def display_for_field(value, field): 86 | from django.contrib.admin.templatetags.admin_list import _boolean_icon 87 | from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 88 | 89 | if field.flatchoices: 90 | return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE) 91 | # NullBooleanField needs special-case null-handling, so it comes 92 | # before the general null test. 93 | elif isinstance(field, fields.BooleanField): 94 | return _boolean_icon(value) 95 | elif value is None: 96 | return EMPTY_CHANGELIST_VALUE 97 | elif isinstance(field, fields.DateTimeField): 98 | return formats.localize(value) 99 | elif isinstance(field, fields.DecimalField): 100 | return formats.number_format(value, field.decimal_places) 101 | elif isinstance(field, fields.FloatField): 102 | return formats.number_format(value) 103 | else: 104 | return smart_unicode(value) 105 | -------------------------------------------------------------------------------- /mongoadmin/validation.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | from django.db import models 3 | from django.db.models.fields import FieldDoesNotExist 4 | from django.contrib.admin.util import get_fields_from_path, NotRelationField 5 | from django.contrib.admin.validation import (check_type, check_isseq, check_isdict, 6 | get_field, BaseValidator) 7 | 8 | from mongoengine import ListField, ReferenceField, DateTimeField 9 | 10 | from mongodbforms import BaseDocumentForm, BaseDocumentFormSet 11 | 12 | """ 13 | Does basic ModelAdmin option validation. Calls custom validation 14 | classmethod in the end if it is provided in cls. The signature of the 15 | custom validation classmethod should be: def validate(cls, model). 16 | """ 17 | 18 | __all__ = ['MongoBaseValidator', 'MongoInlineValidator'] 19 | 20 | 21 | class MongoBaseValidator(BaseValidator): 22 | def validate(self, cls, model): 23 | for m in dir(self): 24 | if m.startswith('validate_'): 25 | getattr(self, m)(cls, model) 26 | 27 | def check_field_spec(self, cls, model, flds, label): 28 | """ 29 | Validate the fields specification in `flds` from a ModelAdmin subclass 30 | `cls` for the `model` model. Use `label` for reporting problems to the user. 31 | 32 | The fields specification can be a ``fields`` option or a ``fields`` 33 | sub-option from a ``fieldsets`` option component. 34 | """ 35 | for fields in flds: 36 | # The entry in fields might be a tuple. If it is a standalone 37 | # field, make it into a tuple to make processing easier. 38 | if type(fields) != tuple: 39 | fields = (fields,) 40 | for field in fields: 41 | if field in cls.readonly_fields: 42 | # Stuff can be put in fields that isn't actually a 43 | # model field if it's in readonly_fields, 44 | # readonly_fields will handle the validation of such 45 | # things. 46 | continue 47 | try: 48 | model._meta.get_field(field) 49 | except models.FieldDoesNotExist: 50 | # If we can't find a field on the model that matches, it could be an 51 | # extra field on the form; nothing to check so move on to the next field. 52 | continue 53 | 54 | def validate_raw_id_fields(self, cls, model): 55 | " Validate that raw_id_fields only contains field names that are listed on the model. " 56 | if hasattr(cls, 'raw_id_fields'): 57 | check_isseq(cls, 'raw_id_fields', cls.raw_id_fields) 58 | for idx, field in enumerate(cls.raw_id_fields): 59 | f = get_field(cls, model, 'raw_id_fields', field) 60 | if not is_relation(f): 61 | raise ImproperlyConfigured("'%s.raw_id_fields[%d]', '%s' must " 62 | "be either a ForeignKey or ManyToManyField." 63 | % (cls.__name__, idx, field)) 64 | 65 | def validate_form(self, cls, model): 66 | " Validate that form subclasses BaseModelForm. " 67 | if hasattr(cls, 'form') and not issubclass(cls.form, BaseDocumentForm): 68 | raise ImproperlyConfigured("%s.form does not inherit from " 69 | "BaseModelForm." % cls.__name__) 70 | 71 | def validate_filter_vertical(self, cls, model): 72 | " Validate that filter_vertical is a sequence of field names. " 73 | if hasattr(cls, 'filter_vertical'): 74 | check_isseq(cls, 'filter_vertical', cls.filter_vertical) 75 | for idx, field in enumerate(cls.filter_vertical): 76 | f = get_field(cls, model, 'filter_vertical', field) 77 | if not is_multi_relation(f): 78 | raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " 79 | "a ManyToManyField." % (cls.__name__, idx)) 80 | 81 | def validate_filter_horizontal(self, cls, model): 82 | " Validate that filter_horizontal is a sequence of field names. " 83 | if hasattr(cls, 'filter_horizontal'): 84 | check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) 85 | for idx, field in enumerate(cls.filter_horizontal): 86 | f = get_field(cls, model, 'filter_horizontal', field) 87 | if not is_multi_relation(f): 88 | raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " 89 | "a ManyToManyField." % (cls.__name__, idx)) 90 | 91 | def validate_radio_fields(self, cls, model): 92 | " Validate that radio_fields is a dictionary of choice or foreign key fields. " 93 | from django.contrib.admin.options import HORIZONTAL, VERTICAL 94 | if hasattr(cls, 'radio_fields'): 95 | check_isdict(cls, 'radio_fields', cls.radio_fields) 96 | for field, val in cls.radio_fields.items(): 97 | f = get_field(cls, model, 'radio_fields', field) 98 | if not (isinstance(f, ReferenceField) or f.choices): 99 | raise ImproperlyConfigured("'%s.radio_fields['%s']' " 100 | "is neither an instance of ForeignKey nor does " 101 | "have choices set." % (cls.__name__, field)) 102 | if not val in (HORIZONTAL, VERTICAL): 103 | raise ImproperlyConfigured("'%s.radio_fields['%s']' " 104 | "is neither admin.HORIZONTAL nor admin.VERTICAL." 105 | % (cls.__name__, field)) 106 | 107 | def validate_prepopulated_fields(self, cls, model): 108 | " Validate that prepopulated_fields if a dictionary containing allowed field types. " 109 | # prepopulated_fields 110 | if hasattr(cls, 'prepopulated_fields'): 111 | check_isdict(cls, 'prepopulated_fields', cls.prepopulated_fields) 112 | for field, val in cls.prepopulated_fields.items(): 113 | f = get_field(cls, model, 'prepopulated_fields', field) 114 | if isinstance(f, DateTimeField) or is_relation(f): 115 | raise ImproperlyConfigured("'%s.prepopulated_fields['%s']' " 116 | "is either a DateTimeField, ForeignKey or " 117 | "ManyToManyField. This isn't allowed." 118 | % (cls.__name__, field)) 119 | check_isseq(cls, "prepopulated_fields['%s']" % field, val) 120 | for idx, f in enumerate(val): 121 | get_field(cls, model, "prepopulated_fields['%s'][%d]" % (field, idx), f) 122 | 123 | 124 | class ModelAdminValidator(BaseValidator): 125 | def validate_save_as(self, cls, model): 126 | " Validate save_as is a boolean. " 127 | check_type(cls, 'save_as', bool) 128 | 129 | def validate_save_on_top(self, cls, model): 130 | " Validate save_on_top is a boolean. " 131 | check_type(cls, 'save_on_top', bool) 132 | 133 | def validate_inlines(self, cls, model): 134 | " Validate inline model admin classes. " 135 | from django.contrib.admin.options import BaseModelAdmin 136 | if hasattr(cls, 'inlines'): 137 | check_isseq(cls, 'inlines', cls.inlines) 138 | for idx, inline in enumerate(cls.inlines): 139 | if not issubclass(inline, BaseModelAdmin): 140 | raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit " 141 | "from BaseModelAdmin." % (cls.__name__, idx)) 142 | if not inline.model: 143 | raise ImproperlyConfigured("'model' is a required attribute " 144 | "of '%s.inlines[%d]'." % (cls.__name__, idx)) 145 | if not issubclass(inline.model, models.Model): 146 | raise ImproperlyConfigured("'%s.inlines[%d].model' does not " 147 | "inherit from models.Model." % (cls.__name__, idx)) 148 | inline.validate(inline.model) 149 | self.check_inline(inline, model) 150 | 151 | def check_inline(self, cls, parent_model): 152 | " Validate inline class's fk field is not excluded. " 153 | pass 154 | #fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) 155 | #if hasattr(cls, 'exclude') and cls.exclude: 156 | # if fk and fk.name in cls.exclude: 157 | # raise ImproperlyConfigured("%s cannot exclude the field " 158 | # "'%s' - this is the foreign key to the parent model " 159 | # "%s.%s." % (cls.__name__, fk.name, parent_model._meta.app_label, parent_model.__name__)) 160 | 161 | def validate_list_display(self, cls, model): 162 | " Validate that list_display only contains fields or usable attributes. " 163 | if hasattr(cls, 'list_display'): 164 | check_isseq(cls, 'list_display', cls.list_display) 165 | for idx, field in enumerate(cls.list_display): 166 | if not callable(field): 167 | if not hasattr(cls, field): 168 | if not hasattr(model, field): 169 | try: 170 | model._meta.get_field(field) 171 | except models.FieldDoesNotExist: 172 | raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." 173 | % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) 174 | else: 175 | # getattr(model, field) could be an X_RelatedObjectsDescriptor 176 | f = fetch_attr(cls, model, "list_display[%d]" % idx, field) 177 | if is_multi_relation(f): 178 | raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." 179 | % (cls.__name__, idx, field)) 180 | 181 | def validate_list_display_links(self, cls, model): 182 | " Validate that list_display_links either is None or a unique subset of list_display." 183 | if hasattr(cls, 'list_display_links'): 184 | if cls.list_display_links is None: 185 | return 186 | check_isseq(cls, 'list_display_links', cls.list_display_links) 187 | for idx, field in enumerate(cls.list_display_links): 188 | if field not in cls.list_display: 189 | raise ImproperlyConfigured("'%s.list_display_links[%d]' " 190 | "refers to '%s' which is not defined in 'list_display'." 191 | % (cls.__name__, idx, field)) 192 | 193 | def validate_list_filter(self, cls, model): 194 | """ 195 | Validate that list_filter is a sequence of one of three options: 196 | 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel') 197 | 2: ('field', SomeFieldListFilter) - a field-based list filter class 198 | 3: SomeListFilter - a non-field list filter class 199 | """ 200 | from django.contrib.admin import ListFilter, FieldListFilter 201 | if hasattr(cls, 'list_filter'): 202 | check_isseq(cls, 'list_filter', cls.list_filter) 203 | for idx, item in enumerate(cls.list_filter): 204 | if callable(item) and not isinstance(item, models.Field): 205 | # If item is option 3, it should be a ListFilter... 206 | if not issubclass(item, ListFilter): 207 | raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" 208 | " which is not a descendant of ListFilter." 209 | % (cls.__name__, idx, item.__name__)) 210 | # ... but not a FieldListFilter. 211 | if issubclass(item, FieldListFilter): 212 | raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" 213 | " which is of type FieldListFilter but is not" 214 | " associated with a field name." 215 | % (cls.__name__, idx, item.__name__)) 216 | else: 217 | if isinstance(item, (tuple, list)): 218 | # item is option #2 219 | field, list_filter_class = item 220 | if not issubclass(list_filter_class, FieldListFilter): 221 | raise ImproperlyConfigured("'%s.list_filter[%d][1]'" 222 | " is '%s' which is not of type FieldListFilter." 223 | % (cls.__name__, idx, list_filter_class.__name__)) 224 | else: 225 | # item is option #1 226 | field = item 227 | # Validate the field string 228 | try: 229 | get_fields_from_path(model, field) 230 | except (NotRelationField, FieldDoesNotExist): 231 | raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" 232 | " which does not refer to a Field." 233 | % (cls.__name__, idx, field)) 234 | 235 | def validate_list_select_related(self, cls, model): 236 | " Validate that list_select_related is a boolean, a list or a tuple. " 237 | list_select_related = getattr(cls, 'list_select_related', None) 238 | if list_select_related: 239 | types = (bool, tuple, list) 240 | if not isinstance(list_select_related, types): 241 | raise ImproperlyConfigured("'%s.list_select_related' should be " 242 | "either a bool, a tuple or a list" % 243 | cls.__name__) 244 | 245 | def validate_list_per_page(self, cls, model): 246 | " Validate that list_per_page is an integer. " 247 | check_type(cls, 'list_per_page', int) 248 | 249 | def validate_list_max_show_all(self, cls, model): 250 | " Validate that list_max_show_all is an integer. " 251 | check_type(cls, 'list_max_show_all', int) 252 | 253 | def validate_list_editable(self, cls, model): 254 | """ 255 | Validate that list_editable is a sequence of editable fields from 256 | list_display without first element. 257 | """ 258 | if hasattr(cls, 'list_editable') and cls.list_editable: 259 | check_isseq(cls, 'list_editable', cls.list_editable) 260 | for idx, field_name in enumerate(cls.list_editable): 261 | try: 262 | field = model._meta.get_field_by_name(field_name)[0] 263 | except models.FieldDoesNotExist: 264 | raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " 265 | "field, '%s', not defined on %s.%s." 266 | % (cls.__name__, idx, field_name, model._meta.app_label, model.__name__)) 267 | if field_name not in cls.list_display: 268 | raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " 269 | "'%s' which is not defined in 'list_display'." 270 | % (cls.__name__, idx, field_name)) 271 | if cls.list_display_links is not None: 272 | if field_name in cls.list_display_links: 273 | raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" 274 | " and '%s.list_display_links'" 275 | % (field_name, cls.__name__, cls.__name__)) 276 | if not cls.list_display_links and cls.list_display[0] in cls.list_editable: 277 | raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" 278 | " the first field in list_display, '%s', which can't be" 279 | " used unless list_display_links is set." 280 | % (cls.__name__, idx, cls.list_display[0])) 281 | if not field.editable: 282 | raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " 283 | "field, '%s', which isn't editable through the admin." 284 | % (cls.__name__, idx, field_name)) 285 | 286 | def validate_search_fields(self, cls, model): 287 | " Validate search_fields is a sequence. " 288 | if hasattr(cls, 'search_fields'): 289 | check_isseq(cls, 'search_fields', cls.search_fields) 290 | 291 | def validate_date_hierarchy(self, cls, model): 292 | " Validate that date_hierarchy refers to DateField or DateTimeField. " 293 | if cls.date_hierarchy: 294 | f = get_field(cls, model, 'date_hierarchy', cls.date_hierarchy) 295 | if not isinstance(f, (models.DateField, models.DateTimeField)): 296 | raise ImproperlyConfigured("'%s.date_hierarchy is " 297 | "neither an instance of DateField nor DateTimeField." 298 | % cls.__name__) 299 | 300 | 301 | class MongoInlineValidator(MongoBaseValidator): 302 | def validate_fk_name(self, cls, model): 303 | " Validate that fk_name refers to a ForeignKey. " 304 | if cls.fk_name: # default value is None 305 | f = get_field(cls, model, 'fk_name', cls.fk_name) 306 | if not isinstance(f, ReferenceField): 307 | raise ImproperlyConfigured("'%s.fk_name is not an instance of " 308 | "models.ForeignKey." % cls.__name__) 309 | 310 | def validate_extra(self, cls, model): 311 | " Validate that extra is an integer. " 312 | check_type(cls, 'extra', int) 313 | 314 | def validate_max_num(self, cls, model): 315 | " Validate that max_num is an integer. " 316 | check_type(cls, 'max_num', int) 317 | 318 | def validate_formset(self, cls, model): 319 | " Validate formset is a subclass of BaseModelFormSet. " 320 | if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseDocumentFormSet): 321 | raise ImproperlyConfigured("'%s.formset' does not inherit from " 322 | "BaseModelFormSet." % cls.__name__) 323 | 324 | 325 | def is_relation(field): 326 | if isinstance(field, ReferenceField) or is_multi_relation(field): 327 | return True 328 | return False 329 | 330 | 331 | def is_multi_relation(field): 332 | if isinstance(field, ListField) and isinstance(field.field, ReferenceField): 333 | return True 334 | return False 335 | 336 | 337 | def fetch_attr(cls, model, label, field): 338 | try: 339 | return model._meta.get_field(field) 340 | except models.FieldDoesNotExist: 341 | pass 342 | try: 343 | return getattr(model, field) 344 | except AttributeError: 345 | raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s.%s'." 346 | % (cls.__name__, label, field, model._meta.app_label, model.__name__)) -------------------------------------------------------------------------------- /mongoadmin/views.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured 2 | from django.contrib.admin.views.main import ChangeList, ORDER_VAR 3 | from django.contrib.admin.options import IncorrectLookupParameters 4 | from django.core.paginator import InvalidPage 5 | 6 | 7 | class DocumentChangeList(ChangeList): 8 | def get_queryset(self, request): 9 | # First, we collect all the declared list filters. 10 | (self.filter_specs, self.has_filters, remaining_lookup_params, 11 | filters_use_distinct) = self.get_filters(request) 12 | 13 | # Then, we let every list filter modify the queryset to its liking. 14 | qs = self.root_queryset 15 | for filter_spec in self.filter_specs: 16 | new_qs = filter_spec.queryset(request, qs) 17 | if new_qs is not None: 18 | qs = new_qs 19 | 20 | try: 21 | # Finally, we apply the remaining lookup parameters from the query 22 | # string (i.e. those that haven't already been processed by the 23 | # filters). 24 | qs = qs.filter(**remaining_lookup_params) 25 | except (SuspiciousOperation, ImproperlyConfigured): 26 | # Allow certain types of errors to be re-raised as-is so that the 27 | # caller can treat them in a special way. 28 | raise 29 | except Exception as e: 30 | # Every other error is caught with a naked except, because we don't 31 | # have any other way of validating lookup parameters. They might be 32 | # invalid if the keyword arguments are incorrect, or if the values 33 | # are not in the correct type, so we might get FieldError, 34 | # ValueError, ValidationError, or ?. 35 | raise IncorrectLookupParameters(e) 36 | 37 | qs = self.apply_select_related(qs) 38 | 39 | # Set ordering. 40 | ordering = self.get_ordering(request, qs) 41 | qs = qs.order_by(*ordering) 42 | 43 | # Apply search results 44 | qs, search_use_distinct = self.model_admin.get_search_results( 45 | request, qs, self.query) 46 | 47 | # Remove duplicates from results, if necessary 48 | if filters_use_distinct | search_use_distinct: 49 | return qs.distinct() 50 | else: 51 | return qs 52 | 53 | def get_ordering(self, request, queryset): 54 | """ 55 | Returns the list of ordering fields for the change list. 56 | First we check the get_ordering() method in model admin, then we check 57 | the object's default ordering. Then, any manually-specified ordering 58 | from the query string overrides anything. Finally, a deterministic 59 | order is guaranteed by ensuring the primary key is used as the last 60 | ordering field. 61 | """ 62 | params = self.params 63 | ordering = list(self.model_admin.get_ordering(request) 64 | or self._get_default_ordering()) 65 | if ORDER_VAR in params: 66 | # Clear ordering and used params 67 | ordering = [] 68 | order_params = params[ORDER_VAR].split('.') 69 | for p in order_params: 70 | try: 71 | none, pfx, idx = p.rpartition('-') 72 | field_name = self.list_display[int(idx)] 73 | order_field = self.get_ordering_field(field_name) 74 | if not order_field: 75 | continue # No 'admin_order_field', skip it 76 | ordering.append(pfx + order_field) 77 | except (IndexError, ValueError): 78 | continue # Invalid ordering specified, skip it. 79 | 80 | # Add the given query's ordering fields, if any. 81 | try: 82 | ordering.extend(queryset._ordering) 83 | except TypeError: 84 | pass 85 | 86 | # Ensure that the primary key is systematically present in the list of 87 | # ordering fields so we can guarantee a deterministic order across all 88 | # database backends. 89 | pk_name = self.lookup_opts.pk.name 90 | if not (set(ordering) & set(['pk', '-pk', pk_name, '-' + pk_name])): 91 | # The two sets do not intersect, meaning the pk isn't present. So 92 | # we add it. 93 | ordering.append('-pk') 94 | 95 | return ordering 96 | 97 | def get_results(self, request): 98 | paginator = self.model_admin.get_paginator( 99 | request, self.queryset, self.list_per_page) 100 | # Get the number of objects, with admin filters applied. 101 | result_count = paginator.count 102 | 103 | # Get the total number of objects, with no admin filters applied. 104 | # Perform a slight optimization: 105 | # full_result_count is equal to paginator.count if no filters 106 | # were applied 107 | if self.get_filters_params(): 108 | full_result_count = self.root_queryset.count() 109 | else: 110 | full_result_count = result_count 111 | can_show_all = result_count <= self.list_max_show_all 112 | multi_page = result_count > self.list_per_page 113 | 114 | # Get the list of objects to display on this page. 115 | if (self.show_all and can_show_all) or not multi_page: 116 | result_list = self.queryset.clone() 117 | else: 118 | try: 119 | result_list = paginator.page(self.page_num + 1).object_list 120 | except InvalidPage: 121 | raise IncorrectLookupParameters 122 | 123 | self.result_count = result_count 124 | self.full_result_count = full_result_count 125 | self.result_list = result_list 126 | self.can_show_all = can_show_all 127 | self.multi_page = multi_page 128 | self.paginator = paginator 129 | -------------------------------------------------------------------------------- /mongoadmin/widgets.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.widgets import ForeignKeyRawIdWidget, ManyToManyRawIdWidget 2 | from django.utils.html import escape 3 | from django.utils.text import Truncator 4 | 5 | from bson.dbref import DBRef 6 | 7 | class ReferenceRawIdWidget(ForeignKeyRawIdWidget): 8 | """ 9 | A Widget for displaying ReferenceFields in the "raw_id" interface rather than 10 | in a