├── .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 |
{% 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