├── tests ├── __init__.py ├── common │ ├── __init__.py │ └── utils.py ├── runtests.py ├── runtests_1_7.py ├── view_tests.py ├── sites_tests.py └── url_tests.py ├── examples ├── __init__.py ├── blog │ ├── __init__.py │ ├── articles │ │ ├── __init__.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_models.py │ │ ├── views.py │ │ ├── urls.py │ │ ├── mongoadmin.py │ │ └── models.py │ ├── requirements.txt │ ├── templates │ │ ├── base.html │ │ └── articles │ │ │ └── post_list.html │ ├── urls.py │ ├── testrunner.py │ ├── manage.py │ └── settings.py └── blog_1_7 │ ├── articles │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── admin.py │ ├── views.py │ ├── urls.py │ ├── mongoadmin.py │ ├── tests.py │ └── models.py │ ├── blog_1_7 │ ├── __init__.py │ ├── urls.py │ ├── wsgi.py │ └── settings.py │ ├── templates │ ├── base.html │ └── articles │ │ └── post_list.html │ ├── requirements.txt │ └── manage.py ├── mongonaut ├── templatetags │ ├── __init__.py │ └── mongonaut_tags.py ├── templates │ └── mongonaut │ │ ├── embedded_document_detail.html │ │ ├── actions │ │ └── action_buttons.html │ │ ├── includes │ │ ├── _delete_warning.html │ │ ├── _field.html │ │ ├── form.html │ │ └── list_add.js │ │ ├── document_add_form.html │ │ ├── document_edit_form.html │ │ ├── document_delete.html │ │ ├── index.html │ │ ├── base.html │ │ ├── document_list.html │ │ └── document_detail.html ├── models.py ├── forms │ ├── __init__.py │ ├── form_utils.py │ ├── widgets.py │ ├── forms.py │ └── form_mixins.py ├── exceptions.py ├── __init__.py ├── urls.py ├── utils.py ├── sites.py ├── mixins.py └── views.py ├── docs ├── contributing.rst ├── configuration.rst ├── future_usage.rst ├── index.rst ├── installation.rst ├── api.rst ├── make.bat ├── Makefile └── conf.py ├── MANIFEST.in ├── CONTRIBUTORS.txt ├── .travis.yml ├── .gitignore ├── LICENSE.txt ├── setup.py ├── CODE_OF_CONDUCT.md ├── CHANGELOG.rst ├── contributing.md └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/blog/articles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/blog_1_7/articles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/blog_1_7/blog_1_7/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mongonaut/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/blog_1_7/articles/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | . include:: ../contributing.md 2 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/embedded_document_detail.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/blog_1_7/articles/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /mongonaut/models.py: -------------------------------------------------------------------------------- 1 | """ Here because Django requires this as boilerplate. """ 2 | -------------------------------------------------------------------------------- /examples/blog/articles/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from articles.tests.test_views import * 2 | -------------------------------------------------------------------------------- /examples/blog/requirements.txt: -------------------------------------------------------------------------------- 1 | django==1.4 2 | mongoengine==0.6.20 3 | pymongo==2.1 4 | -------------------------------------------------------------------------------- /mongonaut/forms/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .forms import MongoModelForm 4 | -------------------------------------------------------------------------------- /examples/blog/templates/base.html: -------------------------------------------------------------------------------- 1 |

django-mongonaut Blog Example

2 | 3 | {% block content %}{% endblock %} -------------------------------------------------------------------------------- /examples/blog_1_7/templates/base.html: -------------------------------------------------------------------------------- 1 |

django-mongonaut Blog Example

2 | 3 | {% block content %}{% endblock %} -------------------------------------------------------------------------------- /examples/blog_1_7/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.7.7 2 | django-mongonaut==0.2.21 3 | mongoengine==0.8.7 4 | pymongo==2.8 5 | wsgiref==0.1.2 6 | -------------------------------------------------------------------------------- /examples/blog/articles/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from mongonaut.tests import MongoTestCase 2 | 3 | 4 | class TestModels(MongoTestCase): 5 | 6 | pass 7 | -------------------------------------------------------------------------------- /mongonaut/exceptions.py: -------------------------------------------------------------------------------- 1 | class NoMongoAdminSpecified(Exception): 2 | """ Called when no MongoAdmin is specified. Unlikely to ever be called.""" 3 | pass 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTORS.txt 2 | include LICENSE.txt 3 | include MANIFEST.in 4 | include README.rst 5 | include CHANGELOG.rst 6 | recursive-include docs * 7 | recursive-include mongonaut/templates/mongonaut * 8 | -------------------------------------------------------------------------------- /examples/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | 4 | urlpatterns = patterns('', 5 | url(r'^mongonaut/', include('mongonaut.urls')), 6 | url(r'^', include('articles.urls')), 7 | ) 8 | -------------------------------------------------------------------------------- /examples/blog_1_7/blog_1_7/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | urlpatterns = patterns('', 4 | url(r'^mongonaut/', include('mongonaut.urls')), 5 | url(r'^', include('articles.urls')), 6 | ) 7 | -------------------------------------------------------------------------------- /examples/blog/articles/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView 2 | 3 | from articles.models import Post 4 | 5 | 6 | class PostListView(ListView): 7 | 8 | template_name = "articles/post_list.html" 9 | 10 | def get_queryset(self): 11 | return Post.objects.all() 12 | -------------------------------------------------------------------------------- /examples/blog_1_7/articles/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView 2 | 3 | from articles.models import Post 4 | 5 | 6 | class PostListView(ListView): 7 | 8 | template_name = "articles/post_list.html" 9 | 10 | def get_queryset(self): 11 | return Post.objects.all() 12 | -------------------------------------------------------------------------------- /mongonaut/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Daniel Greenfeld' 2 | 3 | VERSION = (0, 2, 21) 4 | 5 | 6 | def get_version(): 7 | version = '%s.%s' % (VERSION[0], VERSION[1]) 8 | if VERSION[2]: 9 | version = '%s.%s' % (version, VERSION[2]) 10 | return version 11 | 12 | __version__ = get_version() 13 | -------------------------------------------------------------------------------- /examples/blog_1_7/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog_1_7.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/actions/action_buttons.html: -------------------------------------------------------------------------------- 1 | {% if request.user.is_staff %} 2 |
3 |
4 |

5 | 6 |

7 |
8 |
9 | {% endif %} -------------------------------------------------------------------------------- /examples/blog/templates/articles/post_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% for post in object_list %} 6 |
7 |

{{ post.title }}

8 |

{{ post.author.first_name }} {{ post.author.last_name }}

9 |
{{ content|linebreaksbr|urlize }}
10 |
11 | {% endfor %} 12 | 13 | {% endblock %} -------------------------------------------------------------------------------- /examples/blog_1_7/templates/articles/post_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% for post in object_list %} 6 |
7 |

{{ post.title }}

8 |

{{ post.author.first_name }} {{ post.author.last_name }}

9 |
{{ content|linebreaksbr|urlize }}
10 |
11 | {% endfor %} 12 | 13 | {% endblock %} -------------------------------------------------------------------------------- /examples/blog/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from django.views.generic import ListView 3 | 4 | from articles.models import Post 5 | 6 | urlpatterns = patterns('', 7 | url( 8 | regex=r'^$', 9 | view=ListView.as_view( 10 | queryset=Post.objects.all(), 11 | template_name="articles/post_list.html" 12 | ), 13 | name="article_list" 14 | ), 15 | ) 16 | -------------------------------------------------------------------------------- /examples/blog_1_7/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from django.views.generic import ListView 3 | 4 | from articles.models import Post 5 | 6 | urlpatterns = patterns('', 7 | url( 8 | regex=r'^$', 9 | view=ListView.as_view( 10 | queryset=Post.objects.all(), 11 | template_name="articles/post_list.html" 12 | ), 13 | name="article_list" 14 | ), 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /examples/blog_1_7/blog_1_7/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for blog_1_7 project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog_1_7.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Daniel Greenfeld 2 | Audrey Roy 3 | Chris Gilmer 4 | Bryan Veloso 5 | jerzyk 6 | Mario Rodas 7 | Garry Polley 8 | Swaroop C H 9 | jeff-ogmento 10 | Jeremy Johnson 11 | Fredrik Håård 12 | ashishsingh2205 13 | Sudip Kafle 14 | Maciek Lechowski @lchsk 15 | Anzel Lai @anzellai 16 | Anusha Prasanth @anushajp 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | cache: pip 4 | matrix: 5 | fast_finish: true 6 | include: 7 | - python: 2.7 8 | env: DJANGO=1.11 9 | - python: 10 | - 3.5 11 | - 3.6 12 | env: 13 | - DJANGO=1.11 14 | - DJANGO=2.0 15 | 16 | install: 17 | - pip install -q Django==$DJANGO 18 | - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then pip install unittest2; fi 19 | - pip install -q -e . 20 | services: 21 | - mongodb 22 | script: 23 | - python tests/runtests.py 24 | -------------------------------------------------------------------------------- /examples/blog/testrunner.py: -------------------------------------------------------------------------------- 1 | # Make our own testrunner that by default only tests our own apps 2 | 3 | from django.conf import settings 4 | from django.test.simple import DjangoTestSuiteRunner 5 | 6 | from django_coverage.coverage_runner import CoverageRunner 7 | 8 | 9 | class OurTestRunner(DjangoTestSuiteRunner): 10 | def build_suite(self, test_labels, *args, **kwargs): 11 | return super(OurTestRunner, self).build_suite(test_labels or settings.PROJECT_APPS, *args, **kwargs) 12 | 13 | 14 | class OurCoverageRunner(OurTestRunner, CoverageRunner): 15 | pass 16 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/includes/_delete_warning.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | # Django 30 | dev.db* 31 | dev 32 | *.log 33 | local_settings.py 34 | collected_static/ 35 | 36 | # Local file cruft/auto-backups 37 | .DS_Store 38 | *~ 39 | 40 | # Coverage 41 | coverage 42 | 43 | # Sphinx 44 | docs/_build 45 | docs/_static 46 | 47 | # Launchpad 48 | lp-cache 49 | _data 50 | 51 | # PostgreSQL 52 | logfile 53 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/document_add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mongonaut/base.html" %} 2 | 3 | {% load mongonaut_tags %} 4 | 5 | {% block breadcrumbs %} 6 | 7 | 8 | 10 | 14 | 20 | 23 | {% endblock %} 24 | 25 | {% block content %} 26 | 27 |

Edit {{ document_name }}

28 | 29 | {% include "mongonaut/includes/form.html" %} 30 | 31 | {% endblock %} 32 | 33 | {% block extrajs %} 34 |
35 | {% csrf_token %} 36 |
37 | {% endblock %} 38 | 39 | {% block inlinejs %} 40 | {% include 'mongonaut/includes/list_add.js' %} 41 | {% endblock %} 42 | 43 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/includes/form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {% csrf_token %} 5 |
6 | {% for field in form %} 7 | {% include "mongonaut/includes/_field.html" %} 8 | {% endfor %} 9 |
10 |
11 | 12 | 13 | 14 | {% if document %} 15 | 16 | Delete 17 | 18 | {% endif %} 19 |
20 | {% include "mongonaut/includes/_delete_warning.html" %} 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /mongonaut/templatetags/mongonaut_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django import template 4 | from django.urls import reverse 5 | from django.utils.safestring import mark_safe 6 | 7 | from bson.objectid import ObjectId 8 | from mongoengine import Document 9 | from mongoengine.fields import URLField 10 | 11 | register = template.Library() 12 | 13 | 14 | @register.simple_tag() 15 | def get_document_value(document, key): 16 | ''' 17 | Returns the display value of a field for a particular MongoDB document. 18 | ''' 19 | value = getattr(document, key) 20 | if isinstance(value, ObjectId): 21 | return value 22 | 23 | if isinstance(document._fields.get(key), URLField): 24 | return mark_safe("""{1}""".format(value, value)) 25 | 26 | if isinstance(value, Document): 27 | app_label = value.__module__.replace(".models", "") 28 | document_name = value._class_name 29 | url = reverse( 30 | "document_detail", 31 | kwargs={'app_label': app_label, 'document_name': document_name, 32 | 'id': value.id}) 33 | return mark_safe("""{1}""".format(url, value)) 34 | 35 | return value 36 | -------------------------------------------------------------------------------- /mongonaut/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from mongonaut import views 4 | 5 | urlpatterns = [ 6 | url( 7 | regex=r'^$', 8 | view=views.IndexView.as_view(), 9 | name="index" 10 | ), 11 | url( 12 | regex=r'^(?P[_\-\w\.]+)/(?P[_\-\w\.]+)/$', 13 | view=views.DocumentListView.as_view(), 14 | name="document_list" 15 | ), 16 | url( 17 | regex=r'^(?P[_\-\w\.]+)/(?P[_\-\w\.]+)/add/$', 18 | view=views.DocumentAddFormView.as_view(), 19 | name="document_detail_add_form" 20 | ), 21 | url( 22 | regex=r'^(?P[_\-\w\.]+)/(?P[_\-\w\.]+)/(?P[\w]+)/$', 23 | 24 | view=views.DocumentDetailView.as_view(), 25 | name="document_detail" 26 | ), 27 | url( 28 | regex=r'^(?P[_\-\w\.]+)/(?P[_\-\w\.]+)/(?P[\w]+)/edit/$', 29 | view=views.DocumentEditFormView.as_view(), 30 | name="document_detail_edit_form" 31 | ), 32 | url( 33 | regex=r'^(?P[_\-\w\.]+)/(?P[_\-\w\.]+)/(?P[\w]+)/delete/$', 34 | view=views.DocumentDeleteView.as_view(), 35 | name="document_delete" 36 | ) 37 | ] 38 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/document_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "mongonaut/base.html" %} 2 | 3 | {% load mongonaut_tags %} 4 | 5 | {% block breadcrumbs %} 6 | 10 | 14 | 20 | 23 | {% endblock %} 24 | 25 | {% block content %} 26 | 27 |

Are you sure you want to delete this {{ document_name }}?

28 | 29 |
30 |
31 |
32 | {% csrf_token %} 33 |
34 | Wait, I change my mind!  35 |   36 |
37 |
38 |
39 |
40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/index.html: -------------------------------------------------------------------------------- 1 | {% extends "mongonaut/base.html" %} 2 | 3 | {% load mongonaut_tags %} 4 | 5 | {% block breadcrumbs %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 |

Mongonaut

12 | 13 |
14 |
15 | {% for app in object_list %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for model in app.obj.models %} 26 | 27 | 28 | {% if request.user.is_admin or request.user.is_staff %} 29 | 30 | 31 | {% else %} 32 | 33 | 34 | {% endif %} 35 | 36 | {% endfor %} 37 | 38 |
{{ app.app_name.title }}
{{ model.name }}AddChangeAddChange
39 | {% endfor %} 40 |
41 |
42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /mongonaut/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from mongoengine.errors import ValidationError 4 | from mongoengine.base import ObjectIdField 5 | from mongoengine.fields import ReferenceField 6 | 7 | # Used to validate object_ids. 8 | # Called by is_valid_object_id 9 | OBJECT_ID = ObjectIdField() 10 | 11 | 12 | def is_valid_object_id(value): 13 | """Validates BSON IDs using mongoengine's ObjectIdField field.""" 14 | try: 15 | OBJECT_ID.validate(value) 16 | return True 17 | except ValidationError: 18 | return False 19 | 20 | 21 | def translate_value(document_field, form_value): 22 | """ 23 | Given a document_field and a form_value this will translate the value 24 | to the correct result for mongo to use. 25 | """ 26 | value = form_value 27 | if isinstance(document_field, ReferenceField): 28 | value = document_field.document_type.objects.get(id=form_value) if form_value else None 29 | return value 30 | 31 | 32 | def trim_field_key(document, field_key): 33 | """ 34 | Returns the smallest delimited version of field_key that 35 | is an attribute on document. 36 | 37 | return (key, left_over_array) 38 | """ 39 | trimming = True 40 | left_over_key_values = [] 41 | current_key = field_key 42 | while trimming and current_key: 43 | if hasattr(document, current_key): 44 | trimming = False 45 | else: 46 | key_array = current_key.split("_") 47 | left_over_key_values.append(key_array.pop()) 48 | current_key = u"_".join(key_array) 49 | 50 | left_over_key_values.reverse() 51 | return current_key, left_over_key_values 52 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Configuration 3 | ============= 4 | 5 | One of the most useful parts of `django.contrib.admin` is the ability to configure various views that touch and alter data. django-mongonaut is similar to the Django Admin, but adds in new functionality and ignores other features. The reason is that MongoDB is not a relational database, so attempting to replicate in general simply removes some of the more useful features we get from NoSQL. 6 | 7 | Basic Pattern 8 | ============== 9 | 10 | In your app, create a module called 'mongoadmin.py'. It has to be called that or django-mongonaut will not be able to find it. Then, in your new mongonaut file, simply import the mongoengine powered models you want mongonaut to touch, then import the MongoAdmin class, instantiate it, and finally attach it to your model. 11 | 12 | .. sourcecode:: python 13 | 14 | # myapp/mongoadmin.py 15 | 16 | # Import the MongoAdmin base class 17 | from mongonaut.sites import MongoAdmin 18 | 19 | # Import your custom models 20 | from blog.models import Post 21 | 22 | # Instantiate the MongoAdmin class 23 | # Then attach the mongoadmin to your model 24 | Post.mongoadmin = MongoAdmin() 25 | 26 | That's it! Now you can view, add, edit, and delete your MongoDB models! 27 | 28 | .. note:: You will notice a difference between how and `django.contrib.admin` and `django-mongonaut` do configuration. The former associates the configuration class with the model object via a registration utility, and the latter does so by adding the configuration class as an attribute of the model object. 29 | 30 | More details and features are available in the API reference document. -------------------------------------------------------------------------------- /tests/common/utils.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | """ 3 | Copied exactly from https://github.com/hmarr/mongoengine/blob/master/mongoengine/django/tests.py 4 | 5 | """ 6 | import os 7 | os.environ['DJANGO_SETTINGS_MODULE'] = 'examples.blog.settings' 8 | 9 | from django.test import TestCase 10 | from django.conf import settings 11 | 12 | from mongoengine import connect 13 | 14 | class MongoTestCase(TestCase): 15 | """ 16 | TestCase class that clear the collection between the tests 17 | """ 18 | db_name = 'test_%s' % settings.MONGO_DATABASE_NAME 19 | 20 | def __init__(self, methodName='runtest'): 21 | self.db = connect(self.db_name) 22 | super(MongoTestCase, self).__init__(methodName) 23 | 24 | def _post_teardown(self): 25 | super(MongoTestCase, self)._post_teardown() 26 | self.db.drop_database(self.db_name) 27 | 28 | class DummyUser(object): 29 | 30 | def __init__(self, is_authenticated = True, is_active=True, 31 | can_view=True, is_staff=True, is_superuser=False, 32 | has_perm=['has_view_permission']): 33 | self._is_authenticated = is_authenticated 34 | self._is_active = is_active 35 | self._is_staff = is_staff 36 | self._is_superuser = is_superuser 37 | self._has_perm = has_perm 38 | 39 | def is_authenticated(self): 40 | return self._is_authenticated 41 | 42 | def has_perm(self, perm): 43 | return perm in self._has_perm 44 | 45 | @property 46 | def is_active(self): 47 | return self._is_active 48 | 49 | @property 50 | def is_staff(self): 51 | return self._is_staff 52 | 53 | @property 54 | def is_superuser(self): 55 | return self._is_superuser 56 | -------------------------------------------------------------------------------- /docs/future_usage.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Future Usage Concepts 3 | ====================== 4 | 5 | .. warning:: This is is not implemented. It's sort of my dream of what I want this project to be able to do. 6 | 7 | Complex version. Create a mongonaut.py module in your app: 8 | 9 | .. sourcecode:: python 10 | 11 | #myapp.mongonaut 12 | from datetime import datetime 13 | 14 | from mongonaut.sites import MongoAdmin 15 | 16 | from blog.models import Post 17 | 18 | class ArticleAdmin(MongoAdmin): 19 | 20 | search_fields = ['title',] 21 | 22 | #This shows up on the DocumentListView of the Posts 23 | list_actions = [publish_all_drafts,] 24 | 25 | # This shows up in the DocumentDetailView of the Posts. 26 | document_actions = [generate_word_count,] 27 | 28 | field_actions = {confirm_images: 'image'} 29 | 30 | def publish_all_drafts(self): 31 | """ This shows up on the DocumentListView of the Posts """ 32 | for post in Post.objects.filter(published=False): 33 | post.published = True 34 | post.pub_date = datetime.now() 35 | post.save() 36 | 37 | def generate_word_count(self): 38 | """ This shows up in the DocumentDetailView of the Posts. 39 | ID in this case is somehow the ID of the Posting objecy 40 | """ 41 | return len(Post.objects.get(self.id).content.split(' ')) 42 | 43 | def confirm_images(self): 44 | """ This will be attached to a field in the generated form 45 | specified in a dictionary 46 | """ 47 | do_xyz() 48 | # TODO write this code or something like it 49 | 50 | Article.mongoadmin = ArticleAdmin() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages 5 | 6 | import mongonaut 7 | 8 | LONG_DESCRIPTION = open('README.rst').read() + "\n\n" 9 | CHANGELOG = open('CHANGELOG.rst').read() 10 | 11 | LONG_DESCRIPTION += CHANGELOG 12 | 13 | version = mongonaut.__version__ 14 | 15 | if sys.argv[-1] == 'publish': 16 | os.system("python setup.py sdist upload") 17 | print("You probably want to also tag the version now:") 18 | print(" git tag -a %s -m 'version %s'" % (version, version)) 19 | print(" git push --tags") 20 | sys.exit() 21 | 22 | setup( 23 | name='django-mongonaut', 24 | version=version, 25 | description="An introspective interface for Django and MongoDB", 26 | long_description=LONG_DESCRIPTION, 27 | classifiers=[ 28 | "Development Status :: 4 - Beta", 29 | "Environment :: Web Environment", 30 | "Framework :: Django", 31 | "License :: OSI Approved :: BSD License", 32 | "Operating System :: OS Independent", 33 | "Programming Language :: JavaScript", 34 | "Programming Language :: Python :: 2.6", 35 | "Programming Language :: Python :: 2.7", 36 | "Programming Language :: Python :: 3.3", 37 | "Programming Language :: Python :: 3.4", 38 | "Topic :: Internet :: WWW/HTTP", 39 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 40 | "Topic :: Software Development :: Libraries :: Python Modules", 41 | ], 42 | keywords='mongodb,django', 43 | author=mongonaut.__author__, 44 | author_email='pydanny@gmail.com', 45 | url='http://github.com/jazzband/django-mongonaut', 46 | license='MIT', 47 | packages=find_packages(exclude=['examples']), 48 | include_package_data=True, 49 | install_requires=[ 50 | 'six>=1.11.0', 51 | 'mongoengine>=0.15.0', 52 | ], 53 | ) 54 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | django-mongonaut 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 |
19 | 20 | {% if messages %} 21 |
22 | {% for message in messages %} 23 |
24 | × 25 |

{{ message }}

26 |
27 | {% endfor %} 28 |
29 | {% endif %} 30 | 31 | {% block content %}{% endblock %} 32 |
33 |
34 | 41 |
42 |
43 |
44 | 45 | {% block extrajs %} 46 | {% endblock %} 47 | 48 | 54 | 55 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /mongonaut/sites.py: -------------------------------------------------------------------------------- 1 | try: 2 | import floppyforms as forms 3 | except ImportError: 4 | from django import forms 5 | 6 | 7 | class BaseMongoAdmin(object): 8 | """Base model class for replication django.site.ModelAdmin class. 9 | """ 10 | 11 | search_fields = [] 12 | 13 | # Show the fields to be displayed as columns 14 | # TODO: Confirm that this is what the Django admin uses 15 | list_fields = [] 16 | 17 | #This shows up on the DocumentListView of the Posts 18 | list_actions = [] 19 | 20 | # This shows up in the DocumentDetailView of the Posts. 21 | document_actions = [] 22 | 23 | # shows up on a particular field 24 | field_actions = {} 25 | 26 | fields = None 27 | exclude = None 28 | fieldsets = None 29 | form = forms.ModelForm 30 | filter_vertical = () 31 | filter_horizontal = () 32 | radio_fields = {} 33 | prepopulated_fields = {} 34 | formfield_overrides = {} 35 | readonly_fields = () 36 | ordering = None 37 | 38 | def has_view_permission(self, request): 39 | """ 40 | Returns True if the given HttpRequest has permission to view 41 | *at least one* page in the mongonaut site. 42 | """ 43 | return request.user.is_authenticated and request.user.is_active 44 | 45 | def has_edit_permission(self, request): 46 | """ Can edit this object """ 47 | return request.user.is_authenticated and request.user.is_active and request.user.is_staff 48 | 49 | def has_add_permission(self, request): 50 | """ Can add this object """ 51 | return request.user.is_authenticated and request.user.is_active and request.user.is_staff 52 | 53 | def has_delete_permission(self, request): 54 | """ Can delete this object """ 55 | return request.user.is_authenticated and request.user.is_active and request.user.is_superuser 56 | 57 | 58 | class MongoAdmin(BaseMongoAdmin): 59 | """Serves as the controller for all the actions defined by the developer.""" 60 | 61 | list_display = ('__str__',) 62 | list_display_links = () 63 | list_filter = () 64 | list_select_related = False 65 | list_per_page = 100 66 | list_max_show_all = 200 67 | list_editable = () 68 | search_fields = () 69 | save_as = False 70 | save_on_top = False 71 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-mongonaut documentation master file, created by 2 | sphinx-quickstart on Mon Jan 2 09:26:02 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | django-mongonaut 7 | ================ 8 | 9 | This is an introspective interface for Django and MongoDB. Built from scratch to replicate some of the Django admin functionality, but for MongoDB. 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 3 15 | 16 | installation 17 | configuration 18 | api 19 | future_usage 20 | contributing 21 | 22 | 23 | 24 | 25 | Features 26 | ======== 27 | 28 | Introspection of mongoengine data 29 | ---------------------------------- 30 | 31 | * Introspection via mongo engine 32 | * Q based searches 33 | * django.contrib.admin style browsing 34 | * Automatic detection of field types 35 | * Automatic discovery of collections 36 | 37 | Introspection of pymongodata 38 | ----------------------------- 39 | 40 | * **[in progress]** Admin determination of which fields are displayed. Currently they can do so in the Document List view but not the Document Detail view. 41 | * **[in progress]** Introspection via pymongo. This is becoming very necessary. Plan: 42 | 43 | * Always guarantee the _id. 44 | * Allow devs to set 1 or more field as 'expected'. But there is no hard contract! 45 | * introspect on field types to match how pymongo pulls data. So a `str` is handled differently than a list field. 46 | 47 | Data Management 48 | ---------------------------- 49 | 50 | * **[in progress]** Admin authored Collection level document control functions 51 | * EmbeddedDocumentsFields 52 | * Editing on ListFields 53 | * Document Deletes 54 | * Editing on most other fields including ReferenceFields. 55 | * Automatic detection of widget types 56 | * Text field shorthand for letting user quickly determine type when using without mongoengine 57 | * Document Adds 58 | 59 | Permissions 60 | ---------------------------- 61 | 62 | * **[in progress]** Group defined controls 63 | * User level controls 64 | * Staff level controls 65 | * Admin defined controls 66 | 67 | 68 | Indices and tables 69 | ================== 70 | 71 | * :ref:`genindex` 72 | * :ref:`modindex` 73 | * :ref:`search` 74 | -------------------------------------------------------------------------------- /mongonaut/forms/form_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Used as a utility class for functions related to 5 | form manipulation. 6 | """ 7 | 8 | import six 9 | from collections import namedtuple 10 | 11 | 12 | # Used by form_mixin processing to allow named access to 13 | # field elements in the tuple. 14 | FieldTuple = namedtuple('FieldTuple', 'widget document_field field_type key') 15 | 16 | 17 | def has_digit(string_or_list, sep="_"): 18 | """ 19 | Given a string or a list will return true if the last word or 20 | element is a digit. sep is used when a string is given to know 21 | what separates one word from another. 22 | """ 23 | if isinstance(string_or_list, (tuple, list)): 24 | list_length = len(string_or_list) 25 | if list_length: 26 | return six.text_type(string_or_list[-1]).isdigit() 27 | else: 28 | return False 29 | else: 30 | return has_digit(string_or_list.split(sep)) 31 | 32 | 33 | def make_key(*args, **kwargs): 34 | """ 35 | Given any number of lists and strings will join them in order as one 36 | string separated by the sep kwarg. sep defaults to u"_". 37 | 38 | Add exclude_last_string=True as a kwarg to exclude the last item in a 39 | given string after being split by sep. Note if you only have one word 40 | in your string you can end up getting an empty string. 41 | 42 | Example uses: 43 | 44 | >>> from mongonaut.forms.form_utils import make_key 45 | >>> make_key('hi', 'my', 'firend') 46 | >>> u'hi_my_firend' 47 | 48 | >>> make_key('hi', 'my', 'firend', sep='i') 49 | >>> 'hiimyifirend' 50 | 51 | >>> make_key('hi', 'my', 'firend',['this', 'be', 'what'], sep='i') 52 | >>> 'hiimyifirendithisibeiwhat' 53 | 54 | >>> make_key('hi', 'my', 'firend',['this', 'be', 'what']) 55 | >>> u'hi_my_firend_this_be_what' 56 | 57 | """ 58 | sep = kwargs.get('sep', u"_") 59 | exclude_last_string = kwargs.get('exclude_last_string', False) 60 | string_array = [] 61 | 62 | for arg in args: 63 | if isinstance(arg, list): 64 | string_array.append(six.text_type(sep.join(arg))) 65 | else: 66 | if exclude_last_string: 67 | new_key_array = arg.split(sep)[:-1] 68 | if len(new_key_array) > 0: 69 | string_array.append(make_key(new_key_array)) 70 | else: 71 | string_array.append(six.text_type(arg)) 72 | return sep.join(string_array) 73 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /mongonaut/forms/widgets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ Widgets for mongonaut forms""" 4 | 5 | from django import forms 6 | 7 | from mongoengine.base import ObjectIdField 8 | from mongoengine.fields import BooleanField 9 | from mongoengine.fields import DateTimeField 10 | from mongoengine.fields import EmbeddedDocumentField 11 | from mongoengine.fields import ListField 12 | from mongoengine.fields import ReferenceField 13 | from mongoengine.fields import FloatField 14 | from mongoengine.fields import EmailField 15 | from mongoengine.fields import DecimalField 16 | from mongoengine.fields import URLField 17 | from mongoengine.fields import IntField 18 | from mongoengine.fields import StringField 19 | from mongoengine.fields import GeoPointField 20 | 21 | 22 | def get_widget(model_field, disabled=False): 23 | """Choose which widget to display for a field.""" 24 | 25 | attrs = get_attrs(model_field, disabled) 26 | 27 | if hasattr(model_field, "max_length") and not model_field.max_length: 28 | return forms.Textarea(attrs=attrs) 29 | 30 | elif isinstance(model_field, DateTimeField): 31 | return forms.DateTimeInput(attrs=attrs) 32 | 33 | elif isinstance(model_field, BooleanField): 34 | return forms.CheckboxInput(attrs=attrs) 35 | 36 | elif isinstance(model_field, ReferenceField) or model_field.choices: 37 | return forms.Select(attrs=attrs) 38 | 39 | elif (isinstance(model_field, ListField) or 40 | isinstance(model_field, EmbeddedDocumentField) or 41 | isinstance(model_field, GeoPointField)): 42 | return None 43 | 44 | else: 45 | return forms.TextInput(attrs=attrs) 46 | 47 | 48 | def get_attrs(model_field, disabled=False): 49 | """Set attributes on the display widget.""" 50 | attrs = {} 51 | attrs['class'] = 'span6 xlarge' 52 | if disabled or isinstance(model_field, ObjectIdField): 53 | attrs['class'] += ' disabled' 54 | attrs['readonly'] = 'readonly' 55 | return attrs 56 | 57 | 58 | def get_form_field_class(model_field): 59 | """Gets the default form field for a mongoenigne field.""" 60 | 61 | FIELD_MAPPING = { 62 | IntField: forms.IntegerField, 63 | StringField: forms.CharField, 64 | FloatField: forms.FloatField, 65 | BooleanField: forms.BooleanField, 66 | DateTimeField: forms.DateTimeField, 67 | DecimalField: forms.DecimalField, 68 | URLField: forms.URLField, 69 | EmailField: forms.EmailField 70 | } 71 | 72 | return FIELD_MAPPING.get(model_field.__class__, forms.CharField) 73 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | Normal Installation 6 | =================== 7 | 8 | Get MongoDB:: 9 | 10 | Download the right version per http://www.mongodb.org/downloads 11 | 12 | Get the code:: 13 | 14 | pip install django-mongonaut==0.2.20 15 | 16 | Install the dependency in your settings file (settings.py): 17 | 18 | .. sourcecode:: python 19 | 20 | INSTALLED_APPS = ( 21 | ... 22 | 'mongonaut', 23 | ... 24 | ) 25 | 26 | Add the mongonaut urls.py file to your urlconf file: 27 | 28 | .. sourcecode:: python 29 | 30 | urlpatterns = patterns('', 31 | ... 32 | url(r'^mongonaut/', include('mongonaut.urls')), 33 | ... 34 | ) 35 | 36 | Also in your settings file, you'll need something like: 37 | 38 | .. sourcecode:: python 39 | 40 | # mongodb connection 41 | from mongoengine import connect 42 | connect('example_blog') 43 | 44 | You will need the following also set up: 45 | 46 | * django.contrib.sessions 47 | * django.contrib.messages 48 | 49 | .. note:: No need for `autodiscovery()` with django-mongonaut! 50 | 51 | Static Media Installation 52 | ========================= 53 | 54 | By default, `django-mongonaut` uses static media hosted by other services such as Google or Github. 55 | If you need to point to another location, then you can change the following defaults to your new source: 56 | 57 | .. sourcecode:: python 58 | 59 | # settings.py defaults 60 | MONGONAUT_JQUERY = "http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" 61 | MONGONAUT_TWITTER_BOOTSTRAP = "http://twitter.github.com/bootstrap/assets/css/bootstrap.css" 62 | MONGONAUT_TWITTER_BOOTSTRAP_ALERT = http://twitter.github.com/bootstrap/assets/js/bootstrap-alert.js" 63 | 64 | 65 | Heroku MongoDB connection via MongoLabs 66 | ======================================= 67 | 68 | Your connection string will be provided by MongoLabs in the Heroku config. To make that work, just use the following code instead the `# mongodb connection` example: 69 | 70 | .. sourcecode:: python 71 | 72 | # in your settings file (settings.py) 73 | import os 74 | import re 75 | from mongoengine import connect 76 | regex = re.compile(r'^mongodb\:\/\/(?P[_\w]+):(?P[\w]+)@(?P[\.\w]+):(?P\d+)/(?P[_\w]+)$') 77 | mongolab_url = os.environ['MONGOLAB_URI'] 78 | match = regex.search(mongolab_url) 79 | data = match.groupdict() 80 | connect(data['database'], host=data['host'], port=int(data['port']), username=data['username'], password=data['password']) 81 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | CHANGELOG 3 | ========= 4 | 5 | * 0.3.0 ??? 6 | 7 | * Added logout link 8 | * Resolved bug with pagination (#82), thanks to @anushajp 9 | * changed MongoModelFormBaseMixin to inherit model's Meta class ordering (#74), thanks to @anzellai 10 | * Ability to handle inherited object types, thanks to @lchsk 11 | * Regex changed so that apps name with . are included, thanks to @kaflesudip 12 | * Update travis build and ci requirements 13 | 14 | * 0.2.21 (05/19/2014) 15 | 16 | * Backwards compatible templates so things work in Django 1.4 again. (@ashishsingh2205) 17 | 18 | * 0.2.20 (26/03/2014) 19 | 20 | * Python 3.3 compatibility (@haard) 21 | * Working test harness (@j1z0) 22 | * Fixed missing url function call in documentation (@JAORMX) 23 | 24 | * 0.2.19 (18/07/2013) 25 | 26 | * Use Select widget if choices defined for a field (@jeff-ogmento ) 27 | * Use ordering if defined in MongoAdmin class (@jeff-ogmento ) 28 | * Respect order of list_fields in admin class (@jeff-ogmento ) 29 | * Fixed "django.conf.urls.defaults is deprecated" (@swaroopch) 30 | * Fix search (@swaroopch) 31 | * Make index page also password-protected (@swaroopch) 32 | 33 | * 0.2.18 Various things 34 | 35 | * 0.2.17 Can now add, and modify ListFields and Embedded document fields @garrypolley 36 | 37 | * 0.2.16 ListFields can be added and updated @garrypolley 38 | 39 | * 0.2.15 Editing or Adding a document does not require all fields to be filled out @garrypolley 40 | 41 | * 0.2.14 Fixed pymongo version thanks to @marsam and pagination fixes thanks to @jerzyk 42 | 43 | * 0.2.13 Fields validation and type conversion thanks to @jerzyk 44 | 45 | * 0.2.12 Bump to mongoengine 0.6.2, PEP-8, and fixing the is_authenticated problem in default permission controls. 46 | 47 | * 0.2.11 Change style over to Twitter Bootstrap 2.0.0, Add templates to manifest 48 | 49 | * 0.2.10 Proper Reference field saves, more permission fixes 50 | 51 | * 0.2.9 Permissions correction - Do remember this is in ALPHA!!! 52 | 53 | * 0.2.8 Test components, permission controls in the views, first pass on deletes, Reference field display and some really bad SELECT widget implementations on it. 54 | 55 | * authenticated Permissions refactor, list_fields implementation, and ability to add new documents 56 | 57 | * 0.2.6 Major performance enhancement of the DocumentListView 58 | 59 | * 0.2.5 Added EmbeddedDocument to form views 60 | 61 | * 0.2.4 Installation fix 62 | 63 | * 0.2.3 Installation fix 64 | 65 | * 0.2.2 Supporting of Boolean and Datetime fields and search to boot 66 | 67 | * 0.2.1 Project description fix 68 | 69 | * 0.2.0 basic form saves, pagination, and formatting 70 | 71 | * 0.1.0 Inception and fundamentals 72 | -------------------------------------------------------------------------------- /examples/blog_1_7/articles/tests.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.test import TestCase 4 | from mongoengine import connect, errors 5 | 6 | from mongonaut.forms import MongoModelForm 7 | from blog_1_7.settings import MONGO_DATABASE_NAME 8 | from articles.models import User, Comment, Post, OrderedUser 9 | 10 | 11 | class PostAndUserTestCase(TestCase): 12 | def setUp(self): 13 | uid = uuid.uuid4().hex 14 | self.author = User.objects.create( 15 | email='{0}@test.com'.format(uid), 16 | first_name='test', 17 | last_name='user' 18 | ) 19 | self.comment = Comment( 20 | message='Default test embedded comment', 21 | author=self.author 22 | ) 23 | self.post = Post( 24 | title='Test Article {0}'.format(uid), 25 | content='I am test content', 26 | author=self.author, 27 | published=True, 28 | tags=['post', 'user', 'test'], 29 | comments=[self.comment] 30 | ) 31 | 32 | def tearDown(self): 33 | conn = connect(MONGO_DATABASE_NAME) 34 | conn.drop_database(MONGO_DATABASE_NAME) 35 | # To reserve database but remove test data 36 | #db = conn[MONGO_DATABASE_NAME] 37 | #db.post.remove({'title': self.post.title}) 38 | #db.user.remove({'email': self.author.email}) 39 | 40 | def test_user_required_field(self): 41 | invalid_author = User(first_name='test', last_name='user') 42 | self.assertRaises(errors.ValidationError, invalid_author.save) 43 | 44 | def test_post_save_method(self): 45 | self.post.save() 46 | self.assertEquals(self.post.creator.email, self.author.email) 47 | 48 | 49 | class FormMixinsGetFormFieldDictTestCase(TestCase): 50 | def setUp(self): 51 | uid = uuid.uuid4().hex 52 | self.author = User( 53 | email='{0}@test.com'.format(uid), 54 | first_name='test', 55 | last_name='user' 56 | ) 57 | 58 | def test_form_fields_default_sort_ordering(self): 59 | # At default, the form fields will be ordered by python sorted() 60 | my_form = MongoModelForm(None, model=User, instance=self.author).get_form() 61 | field_names = [field.name for field in my_form] 62 | self.assertEquals(field_names, ['email', 'first_name', 'id', 'last_name']) 63 | 64 | def test_form_fields_ordering_inherit_from_model_meta_class(self): 65 | # if form_fields_ordering is set under model's Meta class, 66 | # ordering will be prioritized, then remaining fields are sorted 67 | ordered_author = OrderedUser( 68 | email=self.author.email, 69 | first_name=self.author.first_name, 70 | last_name=self.author.last_name 71 | ) 72 | my_form = MongoModelForm(None, model=OrderedUser, instance=ordered_author).get_form() 73 | field_names = [field.name for field in my_form] 74 | self.assertEquals(field_names, ['first_name', 'last_name', 'email', 'id']) 75 | 76 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/document_list.html: -------------------------------------------------------------------------------- 1 | {% extends "mongonaut/base.html" %} 2 | 3 | {% load mongonaut_tags %} 4 | 5 | {% block breadcrumbs %} 6 | 10 | 11 | {% endblock %} 12 | 13 | 14 | {% block content %} 15 | 16 |

{{ document_name }} List

17 | 18 | {% if search_field %} 19 | 23 | {% endif %} 24 | {% if has_add_permission %} 25 |

26 | 27 | Add 28 | 29 |

30 | {% endif %} 31 | {% if request.user.is_superuser %} 32 |
33 | {% csrf_token %} 34 | {% include "mongonaut/actions/action_buttons.html" %} 35 | {% endif %} 36 | 37 | 38 | 39 | {% if request.user.is_superuser %}{% endif %} 40 | {% for key in keys %} 41 | 42 | {% endfor %} 43 | 44 | 45 | 46 | {% for obj in object_list %} 47 | 48 | {% if request.user.is_superuser %}{% endif %} 49 | {% for key in keys %} 50 | {% if key == 'id' %} 51 | 52 | {% else %} 53 | 54 | {% endif %} 55 | 56 | {% endfor %} 57 | 58 | {% endfor %} 59 |
{{ key }}
{{ obj.id }}{% get_document_value obj key %}
60 | {% if request.user.is_superuser %} 61 | {% include "mongonaut/actions/action_buttons.html" %} 62 | {% endif %} 63 |
64 | 65 | 76 | 77 | 78 | {% endblock %} 79 | 80 | {% block extrajs %} 81 | 94 | {% endblock %} 95 | -------------------------------------------------------------------------------- /examples/blog/articles/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | The main purpose of these models is to do manual testing of 5 | the mongonaut front end. Do not use this code as an actual blog 6 | backend. 7 | """ 8 | 9 | from datetime import datetime 10 | 11 | from mongoengine import BooleanField 12 | from mongoengine import DateTimeField 13 | from mongoengine import Document 14 | from mongoengine import EmbeddedDocument 15 | from mongoengine import EmbeddedDocumentField 16 | from mongoengine import ListField 17 | from mongoengine import ReferenceField 18 | from mongoengine import StringField 19 | 20 | 21 | class User(Document): 22 | email = StringField(required=True, max_length=50) 23 | first_name = StringField(max_length=50) 24 | last_name = StringField(max_length=50) 25 | 26 | meta = {'allow_inheritance': True} 27 | 28 | def __unicode__(self): 29 | return self.email 30 | 31 | class NewUser(User): 32 | new_field = StringField() 33 | 34 | def __unicode__(self): 35 | return self.email 36 | 37 | 38 | class Comment(EmbeddedDocument): 39 | message = StringField(default="DEFAULT EMBEDDED COMMENT") 40 | author = ReferenceField(User) 41 | 42 | # ListField(EmbeddedDocumentField(ListField(Something)) is not currenlty supported. 43 | # UI, and lists with list inside them need to be fixed. The extra numbers appened to 44 | # the end of the key and class need to happen correctly. 45 | # Files to fix: list_add.js, forms.py, and mixins.py need to be updated to work. 46 | # likes = ListField(ReferenceField(User)) 47 | 48 | 49 | class EmbeddedUser(EmbeddedDocument): 50 | email = StringField(max_length=50, default="default-test@test.com") 51 | first_name = StringField(max_length=50) 52 | last_name = StringField(max_length=50) 53 | created_date = DateTimeField() # Used for testing 54 | is_admin = BooleanField() # Used for testing 55 | # embedded_user_bio = EmbeddedDocumentField(Comment) 56 | friends_list = ListField(ReferenceField(User)) 57 | 58 | # Not supportted see above comment on Comment 59 | # user_comments = ListField(EmbeddedDocumentField(Comment)) 60 | 61 | 62 | class Post(Document): 63 | # See Post.title.max_length to make validation better! 64 | title = StringField(max_length=120, required=True, unique=True) 65 | content = StringField(default="I am default content") 66 | author = ReferenceField(User, required=True) 67 | created_date = DateTimeField() 68 | published = BooleanField() 69 | creator = EmbeddedDocumentField(EmbeddedUser) 70 | published_dates = ListField(DateTimeField()) 71 | tags = ListField(StringField(max_length=30)) 72 | past_authors = ListField(ReferenceField(User)) 73 | comments = ListField(EmbeddedDocumentField(Comment)) 74 | 75 | def save(self, *args, **kwargs): 76 | if not self.created_date: 77 | self.created_date = datetime.utcnow() 78 | if not self.creator: 79 | self.creator = EmbeddedUser() 80 | self.creator.email = self.author.email 81 | self.creator.first_name = self.author.first_name 82 | self.creator.last_name = self.author.last_name 83 | if self.published: 84 | self.published_dates.append(datetime.utcnow()) 85 | super(Post, self).save(*args, **kwargs) 86 | -------------------------------------------------------------------------------- /tests/sites_tests.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | import unittest 3 | 4 | from django.test import RequestFactory 5 | 6 | from mongonaut.sites import BaseMongoAdmin 7 | from common.utils import DummyUser 8 | 9 | 10 | class BaseMongoAdminTests(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.req = RequestFactory().get('/') 14 | 15 | def testHasViewPermissions(self): 16 | self.req.user = DummyUser(is_authenticated=True, is_active=True) 17 | self.assertTrue(BaseMongoAdmin().has_view_permission(self.req)) 18 | 19 | def testHasViewPermissionsInvalid(self): 20 | self.req.user = DummyUser(is_authenticated=False, is_active=True) 21 | self.assertFalse(BaseMongoAdmin().has_view_permission(self.req)) 22 | 23 | self.req.user = DummyUser(is_authenticated=True, is_active=False) 24 | self.assertFalse(BaseMongoAdmin().has_view_permission(self.req)) 25 | 26 | self.req.user = DummyUser(is_authenticated=False, is_active=False) 27 | self.assertFalse(BaseMongoAdmin().has_view_permission(self.req)) 28 | 29 | def testHasEditPerms(self): 30 | self.req.user = DummyUser(is_authenticated=True, is_active=True, 31 | is_staff=True) 32 | 33 | self.assertTrue(BaseMongoAdmin().has_edit_permission(self.req)) 34 | 35 | def testHasEditPermsInvalid(self): 36 | self.req.user = DummyUser(is_staff=False) 37 | self.assertFalse(BaseMongoAdmin().has_edit_permission(self.req)) 38 | 39 | self.req.user = DummyUser(is_active=False) 40 | self.assertFalse(BaseMongoAdmin().has_edit_permission(self.req)) 41 | 42 | self.req.user = DummyUser(is_authenticated=False) 43 | self.assertFalse(BaseMongoAdmin().has_edit_permission(self.req)) 44 | 45 | 46 | def testHasAddPerms(self): 47 | self.req.user = DummyUser(is_authenticated=True, is_active=True, 48 | is_staff=True) 49 | 50 | self.assertTrue(BaseMongoAdmin().has_add_permission(self.req)) 51 | 52 | def testHasAddPermsInvalid(self): 53 | self.req.user = DummyUser(is_staff=False) 54 | self.assertFalse(BaseMongoAdmin().has_add_permission(self.req)) 55 | 56 | self.req.user = DummyUser(is_active=False) 57 | self.assertFalse(BaseMongoAdmin().has_add_permission(self.req)) 58 | 59 | self.req.user = DummyUser(is_authenticated=False) 60 | self.assertFalse(BaseMongoAdmin().has_add_permission(self.req)) 61 | 62 | 63 | def testHasDeletPerms(self): 64 | self.req.user = DummyUser(is_authenticated=True, is_active=True, 65 | is_superuser=True) 66 | 67 | self.assertTrue(BaseMongoAdmin().has_delete_permission(self.req)) 68 | 69 | def testHasDeletePermsInvalid(self): 70 | self.req.user = DummyUser(is_superuser=False) 71 | self.assertFalse(BaseMongoAdmin().has_delete_permission(self.req)) 72 | 73 | self.req.user = DummyUser(is_active=False, is_superuser=True) 74 | self.assertFalse(BaseMongoAdmin().has_delete_permission(self.req)) 75 | 76 | self.req.user = DummyUser(is_authenticated=False, is_superuser=True) 77 | self.assertFalse(BaseMongoAdmin().has_delete_permission(self.req)) 78 | 79 | 80 | if __name__ == "__main__": 81 | unittest.main() 82 | 83 | 84 | -------------------------------------------------------------------------------- /examples/blog_1_7/articles/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | The main purpose of these models is to do manual testing of 5 | the mongonaut front end. Do not use this code as an actual blog 6 | backend. 7 | """ 8 | 9 | from datetime import datetime 10 | 11 | from mongoengine import BooleanField 12 | from mongoengine import DateTimeField 13 | from mongoengine import Document 14 | from mongoengine import EmbeddedDocument 15 | from mongoengine import EmbeddedDocumentField 16 | from mongoengine import ListField 17 | from mongoengine import ReferenceField 18 | from mongoengine import StringField 19 | 20 | 21 | class User(Document): 22 | email = StringField(required=True, max_length=50) 23 | first_name = StringField(max_length=50) 24 | last_name = StringField(max_length=50) 25 | 26 | def __unicode__(self): 27 | return self.email 28 | 29 | 30 | class Comment(EmbeddedDocument): 31 | message = StringField(default="DEFAULT EMBEDDED COMMENT") 32 | author = ReferenceField(User) 33 | 34 | # ListField(EmbeddedDocumentField(ListField(Something)) is not currenlty supported. 35 | # UI, and lists with list inside them need to be fixed. The extra numbers appened to 36 | # the end of the key and class need to happen correctly. 37 | # Files to fix: list_add.js, forms.py, and mixins.py need to be updated to work. 38 | # likes = ListField(ReferenceField(User)) 39 | 40 | 41 | class EmbeddedUser(EmbeddedDocument): 42 | email = StringField(max_length=50, default="default-test@test.com") 43 | first_name = StringField(max_length=50) 44 | last_name = StringField(max_length=50) 45 | created_date = DateTimeField() # Used for testing 46 | is_admin = BooleanField() # Used for testing 47 | # embedded_user_bio = EmbeddedDocumentField(Comment) 48 | friends_list = ListField(ReferenceField(User)) 49 | 50 | # Not supportted see above comment on Comment 51 | # user_comments = ListField(EmbeddedDocumentField(Comment)) 52 | 53 | 54 | class Post(Document): 55 | # See Post.title.max_length to make validation better! 56 | title = StringField(max_length=120, required=True, unique=True) 57 | content = StringField(default="I am default content") 58 | author = ReferenceField(User, required=True) 59 | created_date = DateTimeField() 60 | published = BooleanField() 61 | creator = EmbeddedDocumentField(EmbeddedUser) 62 | published_dates = ListField(DateTimeField()) 63 | tags = ListField(StringField(max_length=30)) 64 | past_authors = ListField(ReferenceField(User)) 65 | comments = ListField(EmbeddedDocumentField(Comment)) 66 | 67 | def save(self, *args, **kwargs): 68 | if not self.created_date: 69 | self.created_date = datetime.utcnow() 70 | if not self.creator: 71 | self.creator = EmbeddedUser() 72 | self.creator.email = self.author.email 73 | self.creator.first_name = self.author.first_name 74 | self.creator.last_name = self.author.last_name 75 | if self.published: 76 | self.published_dates.append(datetime.utcnow()) 77 | super(Post, self).save(*args, **kwargs) 78 | 79 | 80 | 81 | class OrderedUser(Document): 82 | email = StringField(required=True, max_length=50) 83 | first_name = StringField(max_length=50) 84 | last_name = StringField(max_length=50) 85 | 86 | def __unicode__(self): 87 | return self.email 88 | 89 | class Meta: 90 | # At default, the form fields will be ordered by python sorted() 91 | # if form_fields_ordering is set under model's Meta class, 92 | # ordering will be prioritized, then remaining fields are sorted 93 | 94 | form_fields_ordering = ('first_name', 'last_name', 'email', ) 95 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 4 | 5 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). 6 | 7 | 8 | Contributions are welcome, and they are greatly appreciated! Every 9 | little bit helps, and credit will always be given. 10 | 11 | You can contribute in many ways: 12 | 13 | ## Types of Contributions 14 | 15 | ### Report Bugs 16 | 17 | Report bugs at https://github.com/jazzband/django-mongonaut/issues. 18 | 19 | If you are reporting a bug, please include: 20 | 21 | * Your operating system name and version. 22 | * Any details about your local setup that might be helpful in troubleshooting. 23 | * Detailed steps to reproduce the bug. 24 | 25 | ### Fix Bugs 26 | 27 | 28 | Look through the GitHub issues for bugs. Anything tagged with "bug" 29 | is open to whoever wants to implement it. 30 | 31 | ### Implement Features 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | ### Write Documentation 37 | 38 | django-mongonaut could always use more documentation, whether as part of the 39 | official django-mongonaut docs, in docstrings, or even on the web in blog posts, 40 | articles, and such. 41 | 42 | ### Submit Feedback 43 | 44 | The best way to send feedback is to file an issue at https://github.com/jazzband/django-mongonaut/issues. 45 | 46 | If you are proposing a feature: 47 | 48 | * Explain in detail how it would work. 49 | * Keep the scope as narrow as possible, to make it easier to implement. 50 | * Remember that this is a volunteer-driven project, and that contributions 51 | are welcome :) 52 | 53 | Get Started! 54 | ------------ 55 | 56 | Ready to contribute? Here's how to set up `django-mongonaut` for local development. 57 | 58 | 1. Fork the `django-mongonaut` repo on GitHub. 59 | 2. Clone your fork locally:: 60 | 61 | $ git clone git@github.com:your_name_here/django-mongonaut.git 62 | 63 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 64 | 65 | $ mkvirtualenv django-mongonaut 66 | $ cd django-mongonaut/ 67 | $ python setup.py develop 68 | 69 | 4. Create a branch for local development:: 70 | 71 | $ git checkout -b name-of-your-bugfix-or-feature 72 | 73 | Now you can make your changes locally. 74 | 75 | 5. Install and Start MongoDB 76 | 77 | 6. When you're done making changes, check that your changes pass flake8 and the 78 | tests:: 79 | 80 | $ flake8 django-mongonauttests 81 | $ python tests/runtests.py 82 | 83 | To get flake8, just `pip install flake8` into your virtualenv. 84 | 85 | 6. Commit your changes and push your branch to GitHub:: 86 | 87 | $ git add . 88 | $ git commit -m "Your detailed description of your changes." 89 | $ git push origin name-of-your-bugfix-or-feature 90 | 91 | 7. Submit a pull request through the GitHub website. 92 | 93 | Pull Request Guidelines 94 | ----------------------- 95 | 96 | Before you submit a pull request, check that it meets these guidelines: 97 | 98 | 1. The pull request should include tests. 99 | 2. If the pull request adds functionality, the docs should be updated. Put 100 | your new functionality into a function with a docstring, and add the 101 | feature to the list in README.rst. 102 | 3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6. Check 103 | https://travis-ci.org/jazzband/django-mongonaut/pull_requests 104 | and make sure that the tests pass for all supported Python versions. 105 | -------------------------------------------------------------------------------- /tests/url_tests.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | import unittest 4 | from importlib import import_module 5 | 6 | from django.test import RequestFactory 7 | from bson.objectid import ObjectId 8 | from django.core.urlresolvers import NoReverseMatch 9 | from django.conf import settings 10 | import django 11 | 12 | from mongonaut.views import DocumentDetailView 13 | from common.utils import DummyUser 14 | from examples.blog.articles.models import Post, NewUser 15 | from mongonaut.templatetags.mongonaut_tags import get_document_value 16 | 17 | 18 | class IndexViewTests(unittest.TestCase): 19 | 20 | def setUp(self): 21 | self.req = RequestFactory().get('/') 22 | django.setup() 23 | 24 | def testURLResolver(self): 25 | ''' 26 | Tests whether reverse function inside get_document_value can 27 | correctly return a document_detail url when given a set of: 28 | and 29 | Both and will contain dots, eg. 30 | : 'User.NewUser' 31 | : 'examples.blog.articles' 32 | ''' 33 | 34 | urls_tmp = settings.ROOT_URLCONF 35 | settings.ROOT_URLCONF = 'examples.blog.urls' 36 | 37 | u = NewUser(email='test@test.com') 38 | u.id=ObjectId('abcabcabcabc') 39 | 40 | p = Post(author=u, title='Test') 41 | p.id = ObjectId('abcabcabcabc') 42 | 43 | match_found = True 44 | 45 | try: 46 | v = get_document_value(p, 'author') 47 | except NoReverseMatch as e: 48 | match_found = False 49 | 50 | settings.ROOT_URLCONF = urls_tmp 51 | 52 | self.assertEquals(match_found, True) 53 | 54 | def testDetailViewRendering(self): 55 | ''' 56 | Tries to render a detail view byt giving it data 57 | from examples.blog. As and 58 | may contain dots, it checks whether NoReverseMatch exception 59 | was raised. 60 | ''' 61 | 62 | self.req.user = DummyUser() 63 | 64 | urls_tmp = settings.ROOT_URLCONF 65 | settings.ROOT_URLCONF = 'examples.blog.urls' 66 | 67 | self.view = DocumentDetailView.as_view()( 68 | app_label='examples.blog.articles', 69 | document_name='Post', 70 | id=ObjectId('abcabcabcabc'), 71 | request=self.req, 72 | models=import_module('examples.blog.articles.models') 73 | ) 74 | 75 | match_found = True 76 | 77 | try: 78 | self.view.render() 79 | except NoReverseMatch as e: 80 | match_found = False 81 | 82 | settings.ROOT_URLCONF = urls_tmp 83 | 84 | self.assertEquals(match_found, True) 85 | 86 | def testUnicodeURLResolver(self): 87 | ''' 88 | Similarly to testURLResolver, it tests whether get_document_value does not throw an exception. 89 | This time, the value with unicode characters is provided. 90 | ''' 91 | 92 | settings.ROOT_URLCONF = 'examples.blog.urls' 93 | 94 | # Some unicode characters 95 | 96 | email = u"ąćźżńłóśę@gmail.com" 97 | 98 | u = NewUser(email=email) 99 | u.id=ObjectId('abcabcabcabc') 100 | 101 | p = Post(author=u, title='Test Post') 102 | p.id = ObjectId('abcabcabcabc') 103 | 104 | unicode_ok = False 105 | 106 | try: 107 | res = get_document_value(p, 'author') 108 | unicode_ok = True 109 | except UnicodeEncodeError as e: 110 | pass 111 | 112 | self.assertTrue(unicode_ok) 113 | 114 | if __name__ == "__main__": 115 | unittest.main() 116 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | API 3 | ===== 4 | 5 | The following are advanced configuration features for django-mongonaut. Using them requires you to subclass the mongonaut.MongoAdmin class, then instantiate and attach your subclass as an attribute to the MongoEngine model. 6 | 7 | .. note:: Future versions of mongonaut will allow you to work with pymongo collections without mongoengine serving as an intermediary. 8 | 9 | MongoAdmin Objects 10 | =================== 11 | 12 | class MongoAdmin 13 | ------------------ 14 | 15 | The MongoAdmin class is the representation of a model in the mongonaut interface. These are stored in a file named mongoadmin.py in your application. Let’s take a look at a very simple example of the MongoAdmin: 16 | 17 | .. sourcecode:: python 18 | 19 | # myapp/mongoadmin.py 20 | 21 | # Import the MongoAdmin base class 22 | from mongonaut.sites import MongoAdmin 23 | 24 | # Import your custom models 25 | from blog.models import Post 26 | 27 | # Subclass MongoAdmin and add a customization 28 | class PostAdmin(MongoAdmin): 29 | 30 | # Searches on the title field. Displayed in the DocumentListView. 31 | search_fields = ('title',) 32 | 33 | # provide following fields for view in the DocumentListView 34 | list_fields = ('title', "published", "pub_date") 35 | 36 | # Instantiate the PostAdmin subclass 37 | # Then attach PostAdmin to your model 38 | Post.mongoadmin = PostAdmin() 39 | 40 | MongoAdmin Options 41 | ------------------ 42 | 43 | The MongoAdmin is very flexible. It has many options for dealing with customizing the interface. All options are defined on the MongoAdmin subclass: 44 | 45 | `has_add_permission` 46 | ~~~~~~~~~~~~~~~~~~~~ 47 | 48 | **default**: 49 | 50 | .. sourcecode:: python 51 | 52 | # myapp/mongoadmin.py 53 | class PostAdmin(MongoAdmin): 54 | 55 | def has_add_permission(self, request): 56 | """ Can add this object """ 57 | return request.user.is_authenticated and request.user.is_active and request.user.is_staff) 58 | 59 | `has_edit_permission` 60 | ~~~~~~~~~~~~~~~~~~~~~~ 61 | 62 | **default**: 63 | 64 | .. sourcecode:: python 65 | 66 | # myapp/mongoadmin.py 67 | class PostAdmin(MongoAdmin): 68 | 69 | def has_delete_permission(self, request): 70 | """ Can delete this object """ 71 | return request.user.is_authenticated and request.user.is_active and request.user.is_admin() 72 | 73 | `has_edit_permission` 74 | ~~~~~~~~~~~~~~~~~~~~~~ 75 | 76 | **default**: 77 | 78 | .. sourcecode:: python 79 | 80 | # myapp/mongoadmin.py 81 | class PostAdmin(MongoAdmin): 82 | 83 | def has_edit_permission(self, request): 84 | """ Can edit this object """ 85 | return request.user.is_authenticated and request.user.is_active and request.user.is_staff) 86 | 87 | `has_view_permission` 88 | ~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | **default**: 91 | 92 | .. sourcecode:: python 93 | 94 | # myapp/mongoadmin.py 95 | class PostAdmin(MongoAdmin): 96 | 97 | def has_view_permission(self, request): 98 | """ Can view this object """ 99 | return request.user.is_authenticated and request.user.is_active 100 | 101 | `list_fields` 102 | ~~~~~~~~~~~~~~~~~~~~~~ 103 | 104 | **default**: Mongo _id 105 | 106 | Accepts an iterable of string fields that matches fields in the associated model. Displays these fields as columns. 107 | 108 | .. sourcecode:: python 109 | 110 | # myapp/mongoadmin.py 111 | class PostAdmin(MongoAdmin): 112 | 113 | # provide following fields for view in the DocumentListView 114 | list_fields = ('title', "published", "pub_date") 115 | 116 | `search_fields` 117 | ~~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | **default**: [] 120 | 121 | Accepts an iterable of string fields that matches fields in the associated model. Displays a search field in the DocumentListView. Performs an 'icontains' search with an 'OR' between evaluations. 122 | 123 | .. sourcecode:: python 124 | 125 | # myapp/mongoadmin.py 126 | class PostAdmin(MongoAdmin): 127 | 128 | # Searches on the title field. Displayed in the DocumentListView. 129 | search_fields = ('title',) 130 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/document_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "mongonaut/base.html" %} 2 | 3 | {% load mongonaut_tags %} 4 | 5 | {% block breadcrumbs %} 6 | 10 | 14 | 17 | {% endblock %} 18 | 19 | {% block content %} 20 | 21 |

{{ document_name }} Detail

22 | 23 | 24 |
25 |
26 |

27 |

28 | {% if has_edit_permission %} 29 | 30 | Edit 31 | 32 | 33 | {% endif %} 34 | {% if has_delete_permission %} 35 | Delete 36 | {% endif %} 37 |
38 |

39 | {% include "mongonaut/includes/_delete_warning.html" %} 40 |
41 |
42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {% for key in keys %} 55 | 56 | 57 | 58 | 59 | {% endfor %} 60 | 61 |
Content
KeyValue
{{ key}}{% get_document_value document key %}
62 |
63 |
64 | {% if list_fields %} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {% for key in list_fields %} 75 | 76 | 77 | 78 | 79 | {% endfor %} 80 | 81 |
List Fields
Field nameValue
{{ key}}{% get_document_value document key %}
82 | {% endif %} 83 | {% if embedded_documents %} 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {% for key in embedded_documents %} 94 | 95 | 96 | 97 | 98 | {% endfor %} 99 | 100 |
Embedded Documents
Document NameValue
{{ key}}{% get_document_value document key %}
101 | {% endif %} 102 |
103 |
104 | 105 | 106 | {% endblock %} 107 | 108 | {% block extrajs %} 109 |
110 | {% csrf_token %} 111 |
112 | {% endblock %} 113 | 114 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | django-mongonaut 3 | ================ 4 | :Info: An introspective interface for Django and MongoDB. 5 | :Version: 0.2.21 6 | :Maintainer: Jazzband (jazzband.co) 7 | 8 | .. image:: https://travis-ci.org/jazzband/django-mongonaut.png 9 | :alt: Build Status 10 | :target: https://travis-ci.org/jazzband/django-mongonaut 11 | 12 | .. image:: https://codeclimate.com/github/jazzband/django-mongonaut/badges/gpa.svg 13 | :alt: Code Climate 14 | :target: https://codeclimate.com/github/jazzband/django-mongonaut 15 | 16 | .. image:: https://jazzband.co/static/img/badge.svg 17 | :target: https://jazzband.co/ 18 | :alt: Jazzband 19 | 20 | This Project is Being Moved to Jazzband 21 | ======================================= 22 | 23 | In late 2015 `@garrypolley`_ and I agreed to move this project to the `@jazzband`_ organization. Since we've both been off MongoDB for several years, maintaining this project ourselves no longer makes sense. By handing this to Jazzband, we are: 24 | 25 | .. _`@garrypolley`: https://github.com/garrypolley 26 | .. _`@jazzband`: https://github.com/jazzband 27 | 28 | 1. Putting the project in a place where it will be maintained and extended. 29 | 2. Removes the time and effort needed to continue to accept and manage pull requests for a project we no longer wish to maintain but has a somewhat active user base. 30 | 31 | About 32 | ===== 33 | 34 | django-mongonaut is an introspective interface for working with MongoDB via mongoengine. Rather then attempt to staple this functionality into Django's Admin interface, django-mongonaut takes the approach of rolling a new framework from scratch. 35 | 36 | By writing it from scratch I get to avoid trying to staple ORM functionality on top of MongoDB, a NoSQL key/value binary-tree store. 37 | 38 | Features 39 | ========= 40 | 41 | - Automatic introspection of mongoengine documents. 42 | - Ability to constrain who sees what and can do what. 43 | - Full control to add, edit, and delete documents 44 | - More awesome stuff! See https://django-mongonaut.readthedocs.io/en/latest/index.html#features 45 | 46 | Installation 47 | ============ 48 | 49 | Made as easy as possible, setup is actually easier than `django.contrib.admin`. Furthermore, the only dependencies are mongoengine and pymongo. Eventually django-mongonaut will be able to support installations without mongoengine. 50 | 51 | Get MongoDB:: 52 | 53 | Download the right version per http://www.mongodb.org/downloads 54 | 55 | Get Django Mongonaut (and mongoengine + pymongo):: 56 | 57 | pip install -U django-mongonaut 58 | 59 | Install the dependency in your settings.py:: 60 | 61 | INSTALLED_APPS = ( 62 | ... 63 | 'mongonaut', 64 | ... 65 | ) 66 | 67 | You will need the following also set up: 68 | 69 | * django.contrib.sessions 70 | * django.contrib.messages 71 | 72 | .. note:: No need for `autodiscovery()` with django-mongonaut! 73 | 74 | Add the mongonaut urls.py file to your urlconf file: 75 | 76 | .. sourcecode:: python 77 | 78 | from djanog import urls 79 | 80 | urlpatterns = [ 81 | ... 82 | url.path('mongonaut/', urls.include('mongonaut.urls')), 83 | ... 84 | ] 85 | 86 | 87 | Configuration 88 | ============= 89 | 90 | django-mongonaut will let you duplicate much of what `django.contrib.admin` gives you, but in a way more suited for MongoDB. Still being implemented, but already works better than any other MongoDB solution for Django. A simple example:: 91 | 92 | # myapp/mongoadmin.py 93 | 94 | # Import the MongoAdmin base class 95 | from mongonaut.sites import MongoAdmin 96 | 97 | # Import your custom models 98 | from blog.models import Post 99 | 100 | # Instantiate the MongoAdmin class 101 | # Then attach the mongoadmin to your model 102 | Post.mongoadmin = MongoAdmin() 103 | 104 | * https://django-mongonaut.readthedocs.io/en/latest/api.html 105 | 106 | Documentation 107 | ============== 108 | 109 | All the documentation for this project is hosted at https://django-mongonaut.readthedocs.io. 110 | 111 | Dependencies 112 | ============ 113 | 114 | - mongoengine >=0.5.2 115 | - pymongo (comes with mongoengine) 116 | - sphinx (optional - for documentation generation) 117 | 118 | Code of Conduct 119 | =============== 120 | 121 | This project follows the `Jazzband.co Code of Conduct`_. 122 | 123 | .. _`Jazzband.co Code of Conduct`: https://jazzband.co/about/conduct 124 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-mongonaut.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-mongonaut.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-mongonaut.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-mongonaut.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-mongonaut" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-mongonaut" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /examples/blog_1_7/blog_1_7/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for blog_1_7 project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 15 | 16 | 17 | # Quick-start development settings - unsuitable for production 18 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 19 | 20 | # SECURITY WARNING: keep the secret key used in production secret! 21 | SECRET_KEY = 'akk8*fond8!j!&i$m+#w2bix#pu#9777*=nihi0y)en(l)gnyy' 22 | 23 | # SECURITY WARNING: don't run with debug turned on in production! 24 | DEBUG = True 25 | 26 | TEMPLATE_DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | #'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'articles', 41 | 'mongonaut', 42 | ) 43 | 44 | MIDDLEWARE_CLASSES = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ) 53 | 54 | ROOT_URLCONF = 'blog_1_7.urls' 55 | 56 | WSGI_APPLICATION = 'blog_1_7.wsgi.application' 57 | 58 | 59 | # Database 60 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 61 | 62 | DATABASES = { 63 | 'default': { 64 | 'ENGINE': 'django.db.backends.sqlite3', 65 | 'NAME': os.path.join(BASE_DIR, 'dev'), 66 | } 67 | } 68 | 69 | # Internationalization 70 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 71 | 72 | LANGUAGE_CODE = 'en-us' 73 | 74 | TIME_ZONE = 'UTC' 75 | 76 | USE_I18N = True 77 | 78 | USE_L10N = True 79 | 80 | USE_TZ = True 81 | 82 | SITE_ID = 1 83 | 84 | # Static files (CSS, JavaScript, Images) 85 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 86 | 87 | STATIC_URL = '/static/' 88 | 89 | MEDIA_ROOT = '' 90 | 91 | MEDIA_URL = '' 92 | 93 | ADMIN_MEDIA_PREFIX = '/static/admin/' 94 | 95 | # List of finder classes that know how to find static files in 96 | # various locations. 97 | STATICFILES_FINDERS = ( 98 | 'django.contrib.staticfiles.finders.FileSystemFinder', 99 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 100 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 101 | ) 102 | 103 | # List of callables that know how to import templates from various sources. 104 | TEMPLATE_LOADERS = ( 105 | 'django.template.loaders.filesystem.Loader', 106 | 'django.template.loaders.app_directories.Loader', 107 | # 'django.template.loaders.eggs.Loader', 108 | ) 109 | 110 | 111 | ROOT_URLCONF = 'blog_1_7.urls' 112 | 113 | TEMPLATE_DIRS = ( 114 | os.path.join(BASE_DIR, "templates"), 115 | ) 116 | 117 | LOGGING = { 118 | 'version': 1, 119 | 'disable_existing_loggers': False, 120 | 'handlers': { 121 | 'mail_admins': { 122 | 'level': 'ERROR', 123 | 'class': 'django.utils.log.AdminEmailHandler' 124 | } 125 | }, 126 | 'loggers': { 127 | 'django.request': { 128 | 'handlers': ['mail_admins'], 129 | 'level': 'ERROR', 130 | 'propagate': True, 131 | }, 132 | } 133 | } 134 | 135 | #AUTHENTICATION_BACKENDS = ( 136 | # 'mongoengine.django.auth.MongoEngineBackend', 137 | #) 138 | #SESSION_ENGINE = 'mongoengine.django.sessions' 139 | 140 | from mongoengine import connect 141 | MONGO_DATABASE_NAME = 'example_blog_1_7' 142 | connect(MONGO_DATABASE_NAME) 143 | 144 | 145 | ########## LOGGING CONFIGURATION 146 | LOGGING = { 147 | 'version': 1, 148 | 'disable_existing_loggers': False, 149 | 'handlers': { 150 | 'console': { 151 | 'level':'DEBUG', 152 | 'class':'logging.StreamHandler', 153 | } 154 | }, 155 | 'loggers': { 156 | 'django.request': { 157 | 'handlers': ['console'], 158 | 'level': 'DEBUG', 159 | 'propagate': True, 160 | }, 161 | 'cn_project': { 162 | 'handlers': ['console'], 163 | 'level': 'DEBUG', 164 | # 'filters': ['special'] 165 | } 166 | } 167 | } 168 | ########## END LOGGING CONFIGURATION 169 | 170 | ########## DJANGO-DEBUG CONFIGURATION 171 | try: 172 | import debug_toolbar 173 | MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',) 174 | INSTALLED_APPS += ('debug_toolbar',) 175 | INTERNAL_IPS = ('127.0.0.1',) 176 | 177 | DEBUG_TOOLBAR_CONFIG = { 178 | 'INTERCEPT_REDIRECTS': False, 179 | 'SHOW_TEMPLATE_CONTEXT': True, 180 | } 181 | except: 182 | pass 183 | 184 | ########## END DJANGO-DEBUG CONFIGURATION 185 | 186 | ########## DJANGO_EXTENSIONS CONFIGURATION 187 | try: 188 | import django_extensions 189 | INSTALLED_APPS += ('django_extensions',) 190 | except: 191 | pass 192 | 193 | ########## END DJANGO_EXTENSIONS CONFIGURATION 194 | -------------------------------------------------------------------------------- /mongonaut/forms/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.forms import Form 4 | from mongoengine.base import TopLevelDocumentMetaclass 5 | from mongoengine.fields import EmbeddedDocumentField 6 | from mongoengine.fields import ListField 7 | 8 | from .form_mixins import MongoModelFormBaseMixin 9 | from .form_utils import has_digit 10 | from .form_utils import make_key 11 | from .widgets import get_widget 12 | 13 | 14 | class MongoModelForm(MongoModelFormBaseMixin, Form): 15 | """ 16 | This class will take a model and generate a form for the model. 17 | Recommended use for this project only. 18 | 19 | Example: 20 | 21 | my_form = MongoModelForm(request.POST, model=self.document_type, instance=self.document).get_form() 22 | 23 | if self.form.is_valid(): 24 | # Do your processing 25 | """ 26 | 27 | def __init__(self, form_post_data=None, *args, **kwargs): 28 | """ 29 | Overriding init so we can set the post vars like a normal form and generate 30 | the form the same way Django does. 31 | """ 32 | kwargs.update({'form_post_data': form_post_data}) 33 | super(MongoModelForm, self).__init__(*args, **kwargs) 34 | 35 | def set_fields(self): 36 | """Sets existing data to form fields.""" 37 | 38 | # Get dictionary map of current model 39 | if self.is_initialized: 40 | self.model_map_dict = self.create_document_dictionary(self.model_instance) 41 | else: 42 | self.model_map_dict = self.create_document_dictionary(self.model) 43 | 44 | form_field_dict = self.get_form_field_dict(self.model_map_dict) 45 | self.set_form_fields(form_field_dict) 46 | 47 | def set_post_data(self): 48 | """ 49 | Need to set form data so that validation on all post data occurs and 50 | places newly entered form data on the form object. 51 | """ 52 | self.form.data = self.post_data_dict 53 | 54 | # Specifically adding list field keys to the form so they are included 55 | # in form.cleaned_data after the call to is_valid 56 | for field_key, field in self.form.fields.items(): 57 | if has_digit(field_key): 58 | # We have a list field. 59 | base_key = make_key(field_key, exclude_last_string=True) 60 | 61 | # Add new key value with field to form fields so validation 62 | # will work correctly 63 | for key in self.post_data_dict.keys(): 64 | if base_key in key: 65 | self.form.fields.update({key: field}) 66 | 67 | def get_form(self): 68 | """ 69 | Generate the form for view. 70 | """ 71 | self.set_fields() 72 | if self.post_data_dict is not None: 73 | self.set_post_data() 74 | return self.form 75 | 76 | def create_doc_dict(self, document, doc_key=None, owner_document=None): 77 | """ 78 | Generate a dictionary representation of the document. (no recursion) 79 | 80 | DO NOT CALL DIRECTLY 81 | """ 82 | # Get doc field for top level documents 83 | if owner_document: 84 | doc_field = owner_document._fields.get(doc_key, None) if doc_key else None 85 | else: 86 | doc_field = document._fields.get(doc_key, None) if doc_key else None 87 | 88 | # Generate the base fields for the document 89 | doc_dict = {"_document": document if owner_document is None else owner_document, 90 | "_key": document.__class__.__name__.lower() if doc_key is None else doc_key, 91 | "_document_field": doc_field} 92 | 93 | if not isinstance(document, TopLevelDocumentMetaclass) and doc_key: 94 | doc_dict.update({"_field_type": EmbeddedDocumentField}) 95 | 96 | for key, field in document._fields.items(): 97 | doc_dict[key] = field 98 | 99 | return doc_dict 100 | 101 | def create_list_dict(self, document, list_field, doc_key): 102 | """ 103 | Genereates a dictionary representation of the list field. Document 104 | should be the document the list_field comes from. 105 | 106 | DO NOT CALL DIRECTLY 107 | """ 108 | list_dict = {"_document": document} 109 | 110 | if isinstance(list_field.field, EmbeddedDocumentField): 111 | list_dict.update(self.create_document_dictionary(document=list_field.field.document_type_obj, 112 | owner_document=document)) 113 | 114 | # Set the list_dict after it may have been updated 115 | list_dict.update({"_document_field": list_field.field, 116 | "_key": doc_key, 117 | "_field_type": ListField, 118 | "_widget": get_widget(list_field.field), 119 | "_value": getattr(document, doc_key, None)}) 120 | 121 | return list_dict 122 | 123 | def create_document_dictionary(self, document, document_key=None, 124 | owner_document=None): 125 | """ 126 | Given document generates a dictionary representation of the document. 127 | Includes the widget for each for each field in the document. 128 | """ 129 | doc_dict = self.create_doc_dict(document, document_key, owner_document) 130 | 131 | for doc_key, doc_field in doc_dict.items(): 132 | # Base fields should not be evaluated 133 | if doc_key.startswith("_"): 134 | continue 135 | 136 | if isinstance(doc_field, ListField): 137 | doc_dict[doc_key] = self.create_list_dict(document, doc_field, doc_key) 138 | 139 | elif isinstance(doc_field, EmbeddedDocumentField): 140 | doc_dict[doc_key] = self.create_document_dictionary(doc_dict[doc_key].document_type_obj, 141 | doc_key) 142 | else: 143 | doc_dict[doc_key] = {"_document": document, 144 | "_key": doc_key, 145 | "_document_field": doc_field, 146 | "_widget": get_widget(doc_dict[doc_key], getattr(doc_field, 'disabled', False))} 147 | 148 | return doc_dict 149 | -------------------------------------------------------------------------------- /examples/blog/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for blog project. 2 | 3 | import os 4 | 5 | PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 6 | 7 | DEBUG = True 8 | TEMPLATE_DEBUG = DEBUG 9 | 10 | ADMINS = ( 11 | # ('Your Name', 'your_email@example.com'), 12 | ) 13 | 14 | MANAGERS = ADMINS 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 19 | 'NAME': 'dev', # Or path to database file if using sqlite3. 20 | 'USER': '', # Not used with sqlite3. 21 | 'PASSWORD': '', # Not used with sqlite3. 22 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 23 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 24 | } 25 | } 26 | 27 | # Local time zone for this installation. Choices can be found here: 28 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 29 | # although not all choices may be available on all operating systems. 30 | # On Unix systems, a value of None will cause Django to use the same 31 | # timezone as the operating system. 32 | # If running in a Windows environment this must be set to the same as your 33 | # system time zone. 34 | TIME_ZONE = 'America/Chicago' 35 | 36 | # Language code for this installation. All choices can be found here: 37 | # http://www.i18nguy.com/unicode/language-identifiers.html 38 | LANGUAGE_CODE = 'en-us' 39 | 40 | SITE_ID = 1 41 | 42 | # If you set this to False, Django will make some optimizations so as not 43 | # to load the internationalization machinery. 44 | USE_I18N = True 45 | 46 | # If you set this to False, Django will not format dates, numbers and 47 | # calendars according to the current locale 48 | USE_L10N = True 49 | 50 | # Absolute filesystem path to the directory that will hold user-uploaded files. 51 | # Example: "/home/media/media.lawrence.com/media/" 52 | MEDIA_ROOT = '' 53 | 54 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 55 | # trailing slash. 56 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 57 | MEDIA_URL = '' 58 | 59 | # Absolute path to the directory static files should be collected to. 60 | # Don't put anything in this directory yourself; store your static files 61 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 62 | # Example: "/home/media/media.lawrence.com/static/" 63 | STATIC_ROOT = '' 64 | 65 | # URL prefix for static files. 66 | # Example: "http://media.lawrence.com/static/" 67 | STATIC_URL = '/static/' 68 | 69 | # URL prefix for admin static files -- CSS, JavaScript and images. 70 | # Make sure to use a trailing slash. 71 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 72 | ADMIN_MEDIA_PREFIX = '/static/admin/' 73 | 74 | # Additional locations of static files 75 | STATICFILES_DIRS = ( 76 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 77 | # Always use forward slashes, even on Windows. 78 | # Don't forget to use absolute paths, not relative paths. 79 | ) 80 | 81 | # List of finder classes that know how to find static files in 82 | # various locations. 83 | STATICFILES_FINDERS = ( 84 | 'django.contrib.staticfiles.finders.FileSystemFinder', 85 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 86 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 87 | ) 88 | 89 | # Make this unique, and don't share it with anybody. 90 | SECRET_KEY = '7=_4bgp$qrt-1(32h27dhqv#c33cef)0yu1s()yq0=whg3kym4' 91 | 92 | # List of callables that know how to import templates from various sources. 93 | TEMPLATE_LOADERS = ( 94 | 'django.template.loaders.filesystem.Loader', 95 | 'django.template.loaders.app_directories.Loader', 96 | # 'django.template.loaders.eggs.Loader', 97 | ) 98 | 99 | MIDDLEWARE_CLASSES = ( 100 | 'django.middleware.common.CommonMiddleware', 101 | 'django.contrib.sessions.middleware.SessionMiddleware', 102 | 'django.middleware.csrf.CsrfViewMiddleware', 103 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 104 | 'django.contrib.messages.middleware.MessageMiddleware', 105 | ) 106 | 107 | ROOT_URLCONF = 'blog.urls' 108 | 109 | TEMPLATE_DIRS = ( 110 | os.path.join(PROJECT_ROOT.replace("examples", "examples/blog"), 'templates'), 111 | ) 112 | 113 | INSTALLED_APPS = ( 114 | 'django.contrib.auth', 115 | 'django.contrib.contenttypes', 116 | 'django.contrib.sessions', 117 | 'django.contrib.sites', 118 | 'django.contrib.messages', 119 | 'django.contrib.staticfiles', 120 | 'articles', 121 | 'mongonaut', 122 | ) 123 | 124 | LOGGING = { 125 | 'version': 1, 126 | 'disable_existing_loggers': False, 127 | 'handlers': { 128 | 'mail_admins': { 129 | 'level': 'ERROR', 130 | 'class': 'django.utils.log.AdminEmailHandler' 131 | } 132 | }, 133 | 'loggers': { 134 | 'django.request': { 135 | 'handlers': ['mail_admins'], 136 | 'level': 'ERROR', 137 | 'propagate': True, 138 | }, 139 | } 140 | } 141 | 142 | #AUTHENTICATION_BACKENDS = ( 143 | # 'mongoengine.django.auth.MongoEngineBackend', 144 | #) 145 | #SESSION_ENGINE = 'mongoengine.django.sessions' 146 | 147 | from mongoengine import connect 148 | MONGO_DATABASE_NAME = 'example_blog' 149 | connect(MONGO_DATABASE_NAME) 150 | 151 | 152 | ########## LOGGING CONFIGURATION 153 | LOGGING = { 154 | 'version': 1, 155 | 'disable_existing_loggers': False, 156 | 'handlers': { 157 | 'console': { 158 | 'level':'DEBUG', 159 | 'class':'logging.StreamHandler', 160 | } 161 | }, 162 | 'loggers': { 163 | 'django.request': { 164 | 'handlers': ['console'], 165 | 'level': 'DEBUG', 166 | 'propagate': True, 167 | }, 168 | 'cn_project': { 169 | 'handlers': ['console'], 170 | 'level': 'DEBUG', 171 | # 'filters': ['special'] 172 | } 173 | } 174 | } 175 | ########## END LOGGING CONFIGURATION 176 | 177 | ########## DJANGO-DEBUG CONFIGURATION 178 | try: 179 | import debug_toolbar 180 | MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',) 181 | INSTALLED_APPS += ('debug_toolbar',) 182 | INTERNAL_IPS = ('127.0.0.1',) 183 | 184 | DEBUG_TOOLBAR_CONFIG = { 185 | 'INTERCEPT_REDIRECTS': False, 186 | 'SHOW_TEMPLATE_CONTEXT': True, 187 | } 188 | except: 189 | pass 190 | 191 | ########## END DJANGO-DEBUG CONFIGURATION 192 | 193 | ########## DJANGO_EXTENSIONS CONFIGURATION 194 | try: 195 | import django_extensions 196 | INSTALLED_APPS += ('django_extensions',) 197 | except: 198 | pass 199 | 200 | ########## END DJANGO_EXTENSIONS CONFIGURATION 201 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-mongonaut documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Jan 2 09:26:02 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | # If extensions (or modules to document with autodoc) are in another directory, 15 | # add these directories to sys.path here. If the directory is relative to the 16 | # documentation root, use os.path.abspath to make it absolute, like shown here. 17 | #sys.path.insert(0, os.path.abspath('.')) 18 | 19 | # -- General configuration ----------------------------------------------------- 20 | 21 | # If your documentation needs a minimal Sphinx version, state it here. 22 | #needs_sphinx = '1.0' 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ['_templates'] 30 | 31 | # The suffix of source filenames. 32 | source_suffix = '.rst' 33 | 34 | # The encoding of source files. 35 | #source_encoding = 'utf-8-sig' 36 | 37 | # The master toctree document. 38 | master_doc = 'index' 39 | 40 | # General information about the project. 41 | project = u'django-mongonaut' 42 | copyright = u'2012, Daniel Greenfeld' 43 | 44 | # The version info for the project you're documenting, acts as replacement for 45 | # |version| and |release|, also used in various other places throughout the 46 | # built documents. 47 | # 48 | # The short X.Y version. 49 | version = '0.2' 50 | # The full version, including alpha/beta/rc tags. 51 | release = '0.2.20' 52 | 53 | # The language for content autogenerated by Sphinx. Refer to documentation 54 | # for a list of supported languages. 55 | #language = None 56 | 57 | # There are two options for replacing |today|: either, you set today to some 58 | # non-false value, then it is used: 59 | #today = '' 60 | # Else, today_fmt is used as the format for a strftime call. 61 | #today_fmt = '%B %d, %Y' 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | exclude_patterns = ['_build'] 66 | 67 | # The reST default role (used for this markup: `text`) to use for all documents. 68 | #default_role = None 69 | 70 | # If true, '()' will be appended to :func: etc. cross-reference text. 71 | #add_function_parentheses = True 72 | 73 | # If true, the current module name will be prepended to all description 74 | # unit titles (such as .. function::). 75 | #add_module_names = True 76 | 77 | # If true, sectionauthor and moduleauthor directives will be shown in the 78 | # output. They are ignored by default. 79 | #show_authors = False 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # A list of ignored prefixes for module index sorting. 85 | #modindex_common_prefix = [] 86 | 87 | 88 | # -- Options for HTML output --------------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | html_theme = 'default' 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | #html_theme_options = {} 98 | 99 | # Add any paths that contain custom themes here, relative to this directory. 100 | #html_theme_path = [] 101 | 102 | # The name for this set of Sphinx documents. If None, it defaults to 103 | # " v documentation". 104 | #html_title = None 105 | 106 | # A shorter title for the navigation bar. Default is the same as html_title. 107 | #html_short_title = None 108 | 109 | # The name of an image file (relative to this directory) to place at the top 110 | # of the sidebar. 111 | #html_logo = None 112 | 113 | # The name of an image file (within the static path) to use as favicon of the 114 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 115 | # pixels large. 116 | #html_favicon = None 117 | 118 | # Add any paths that contain custom static files (such as style sheets) here, 119 | # relative to this directory. They are copied after the builtin static files, 120 | # so a file named "default.css" will overwrite the builtin "default.css". 121 | html_static_path = ['_static'] 122 | 123 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 124 | # using the given strftime format. 125 | #html_last_updated_fmt = '%b %d, %Y' 126 | 127 | # If true, SmartyPants will be used to convert quotes and dashes to 128 | # typographically correct entities. 129 | #html_use_smartypants = True 130 | 131 | # Custom sidebar templates, maps document names to template names. 132 | #html_sidebars = {} 133 | 134 | # Additional templates that should be rendered to pages, maps page names to 135 | # template names. 136 | #html_additional_pages = {} 137 | 138 | # If false, no module index is generated. 139 | #html_domain_indices = True 140 | 141 | # If false, no index is generated. 142 | #html_use_index = True 143 | 144 | # If true, the index is split into individual pages for each letter. 145 | #html_split_index = False 146 | 147 | # If true, links to the reST sources are added to the pages. 148 | #html_show_sourcelink = True 149 | 150 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 151 | #html_show_sphinx = True 152 | 153 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 154 | #html_show_copyright = True 155 | 156 | # If true, an OpenSearch description file will be output, and all pages will 157 | # contain a tag referring to it. The value of this option must be the 158 | # base URL from which the finished HTML is served. 159 | #html_use_opensearch = '' 160 | 161 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 162 | #html_file_suffix = None 163 | 164 | # Output file base name for HTML help builder. 165 | htmlhelp_basename = 'django-mongonautdoc' 166 | 167 | 168 | # -- Options for LaTeX output -------------------------------------------------- 169 | 170 | # The paper size ('letter' or 'a4'). 171 | #latex_paper_size = 'letter' 172 | 173 | # The font size ('10pt', '11pt' or '12pt'). 174 | #latex_font_size = '10pt' 175 | 176 | # Grouping the document tree into LaTeX files. List of tuples 177 | # (source start file, target name, title, author, documentclass [howto/manual]). 178 | latex_documents = [ 179 | ('index', 'django-mongonaut.tex', u'django-mongonaut Documentation', 180 | u'Daniel Greenfeld', 'manual'), 181 | ] 182 | 183 | # The name of an image file (relative to this directory) to place at the top of 184 | # the title page. 185 | #latex_logo = None 186 | 187 | # For "manual" documents, if this is true, then toplevel headings are parts, 188 | # not chapters. 189 | #latex_use_parts = False 190 | 191 | # If true, show page references after internal links. 192 | #latex_show_pagerefs = False 193 | 194 | # If true, show URL addresses after external links. 195 | #latex_show_urls = False 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #latex_preamble = '' 199 | 200 | # Documents to append as an appendix to all manuals. 201 | #latex_appendices = [] 202 | 203 | # If false, no module index is generated. 204 | #latex_domain_indices = True 205 | 206 | 207 | # -- Options for manual page output -------------------------------------------- 208 | 209 | # One entry per manual page. List of tuples 210 | # (source start file, name, description, authors, manual section). 211 | man_pages = [ 212 | ('index', 'django-mongonaut', u'django-mongonaut Documentation', 213 | [u'Daniel Greenfeld'], 1) 214 | ] 215 | -------------------------------------------------------------------------------- /mongonaut/templates/mongonaut/includes/list_add.js: -------------------------------------------------------------------------------- 1 | function addInputFromPrevious(input, embeddedDoc){ 2 | /* This is a fuction to insert an input after an existing 3 | input. It will correclty update the id and name so the 4 | input can be posted as desired. 5 | 6 | embeddedDoc must be an array. 7 | */ 8 | var newInput = input.clone(); 9 | var currentNameArray = newInput.attr('name').split('_'); 10 | var lastArrayString = currentNameArray.pop(); 11 | var newIdNum = 1; 12 | var baseName = currentNameArray.join("_"); 13 | var currentName = ""; 14 | 15 | // Compute the name attribute for the input 16 | if (!isNaN(lastArrayString)) { 17 | newIdNum += +lastArrayString; 18 | currentName = baseName + '_' + newIdNum; 19 | newInput.attr('name', currentName); 20 | } else { 21 | if (currentNameArray.length > 0) { 22 | baseName = baseName + "_" + lastArrayString; 23 | currentName = baseName + "_" + newIdNum; 24 | newInput.attr('name', currentName); 25 | } else { 26 | baseName = lastArrayString; 27 | currentName = baseName + "_" + newIdNum; 28 | newInput.attr('name', currentName); 29 | } 30 | } 31 | 32 | // Default to no value for newly added fields 33 | var inputType = $(newInput).attr('type'); 34 | if (inputType == "text") { 35 | $(newInput).attr('value', ''); 36 | } else if (inputType == "checkbox" ) { 37 | $(newInput).attr('checked', false); 38 | } else { 39 | $(newInput).children("option[value='']").attr('selected', 'selected'); 40 | } 41 | $(newInput).attr('id', "id_" + currentName); 42 | 43 | if (embeddedDoc){ 44 | embeddedDoc.push($(newInput)); 45 | } else { 46 | $(newInput).insertAfter(input); 47 | $(newInput).before('
'); 48 | } 49 | // Cleanly add the new input field to the DOM 50 | 51 | return newInput; 52 | } 53 | 54 | function attachButtonList(input) { 55 | /* Used to place the add button on the page. Upon each click it 56 | copies the previous input and increments the id and name by using 57 | the addInputFromPrevious function. */ 58 | var currentInput = input; 59 | var inputName = currentInput.attr('name'); 60 | var button = "
add
"; 61 | $(button).insertAfter(currentInput); 62 | 63 | $(document).on('click', "div[class~=" + inputName +"]", function(e){ 64 | e.preventDefault(); 65 | var newInput = addInputFromPrevious(currentInput); 66 | currentInput = newInput; 67 | }); 68 | } 69 | 70 | function attachButtonEmbedded(inputs) { 71 | /* Used to place the add button on the page. Upon each click it 72 | copies the previous input and increments the id and name by using 73 | the addInputFromPrevious function. */ 74 | var finalInput = null; 75 | for (var j = 0; j < inputs.length; j++) { 76 | if (j + 1 == inputs.length) { 77 | finalInput = inputs[j]; 78 | } 79 | } 80 | var finalInputName = $(finalInput).attr('name'); 81 | var button = "
add
"; 82 | var embeddedParentDIV = inputs.parent().parent().parent(); 83 | $(button).insertAfter(finalInput); 84 | 85 | 86 | $(document).on('click', "div[class~=" + finalInputName +"]", function(e){ 87 | e.preventDefault(); 88 | var addedInputs = []; 89 | var embeddedDocs = []; 90 | 91 | for (var j = 0; j < inputs.length; j++) { 92 | var newInput = addInputFromPrevious($(inputs[j]), embeddedDocs); 93 | addedInputs.push(newInput); 94 | } 95 | 96 | var newEmbeddedDoc = $("
"); 97 | for (var k = 0; k < embeddedDocs.length; k++) { 98 | var newFieldSet = $("
"); 99 | var newInputField = embeddedDocs[k]; 100 | var newLabel = ""; 101 | 102 | newFieldSet.append(newLabel); 103 | newFieldSet.append(newInputField); 104 | newEmbeddedDoc.append(newFieldSet); 105 | 106 | if (k + 1 == embeddedDocs.length) { 107 | finalInput = newInputField; 108 | } 109 | } 110 | 111 | // Move button to next document 112 | $("[class*='btn btn-primary " + finalInputName + "']").remove(); 113 | $(button).insertAfter(finalInput); 114 | 115 | $(embeddedParentDIV).append(newEmbeddedDoc); 116 | inputs = addedInputs; 117 | }); 118 | } 119 | 120 | var listFields = $('.listField'); 121 | var listClasses = []; 122 | var initialInputs = []; 123 | for (var i = 0; i < listFields.length; i++) { 124 | var field = listFields[i]; 125 | var listClass = $(field).attr('class').split(' ').pop(); 126 | 127 | // Make sure to only get the base class from the grop of list fields 128 | if ($.inArray(listClass, listClasses) == -1) { 129 | listClasses.push(listClass); 130 | } 131 | } 132 | 133 | // Get the last list item in the group 134 | for (var i = 0; i < listClasses.length; i++) { 135 | var inputs = $('.' + listClasses[i]); 136 | var listDiv = inputs.parent().parent(); 137 | $(listDiv).wrapAll("
"); 138 | listDiv = $(listDiv[listDiv.length - 1]); 139 | initialInputs.push(listDiv.children('div').children('.listField')); 140 | } 141 | 142 | // Process embedded documents 143 | var embeddedFields = $('.embeddedField'); 144 | var embeddedClasses = []; 145 | 146 | // Group Embedded docs together 147 | for (var i = 0; i < embeddedFields.length; i++) { 148 | var field = embeddedFields[i]; 149 | var allClasses = $(field).attr('class').split(' '); 150 | 151 | for (j = 0; j < allClasses.length; j++) { 152 | var embeddedClass = allClasses[j]; 153 | if ($.inArray(embeddedClass, ['listField', 'embeddedField', 'span6', 'xlarge', '']) != -1) { 154 | continue; 155 | } 156 | // Make sure to only get the base class from the group of embedded fields 157 | if ($.inArray(embeddedClass, embeddedClasses) == -1) { 158 | embeddedClasses.push(embeddedClass); 159 | } 160 | } 161 | } 162 | 163 | for (var i = 0; i < embeddedClasses.length; i++){ 164 | var inputDivs = $("[class~='" + embeddedClasses[i] + "']").parent().parent(); 165 | var idBase = embeddedClasses[i]; 166 | 167 | $(inputDivs).wrapAll("
"); 168 | $(inputDivs[0]).parent().before("
" + embeddedClasses[i] + "
"); 169 | } 170 | 171 | // Attach the buttons to the correct fields. 172 | var alreadyAttached = []; 173 | 174 | // Go in reverse order to correctly attach buttons 175 | for (var i = initialInputs.length - 1; i >= 0; i--) { 176 | var inputField = initialInputs[i]; 177 | 178 | // Remove the trailing number from our name so we can track what has 179 | // already been added. 180 | var inputNameArray = inputField.attr("name").split("_"); 181 | inputNameArray.pop(); 182 | var newInputName = inputNameArray.join("_"); 183 | 184 | // Make sure we do not add a button to the same field twice 185 | if ($.inArray(newInputName, alreadyAttached) != -1) { 186 | continue; 187 | } 188 | 189 | if (inputField.hasClass("embeddedField")) { 190 | var newFields = $("[class*='" + inputField.attr("class") + "']"); 191 | attachButtonEmbedded(newFields); 192 | } else { 193 | attachButtonList(initialInputs[i]); 194 | } 195 | alreadyAttached.push(newInputName); 196 | } 197 | 198 | // Remove any empty divs leftover by the re-organization of the page. 199 | $(document).ready(function() { 200 | $('div:empty').remove(); 201 | var allDivs = $("div"); 202 | allDivs.each( function(index, element) { 203 | if (element.innerHTML === "") { 204 | $(element).remove(); 205 | } 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /mongonaut/mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import types 3 | from importlib import import_module 4 | 5 | from django.conf import settings 6 | from django.contrib import messages 7 | from django.http import HttpResponseForbidden 8 | from mongoengine.fields import EmbeddedDocumentField 9 | 10 | from mongonaut.exceptions import NoMongoAdminSpecified 11 | from mongonaut.forms import MongoModelForm 12 | from mongonaut.forms.form_utils import has_digit 13 | from mongonaut.forms.form_utils import make_key 14 | from mongonaut.utils import translate_value 15 | from mongonaut.utils import trim_field_key 16 | 17 | 18 | class AppStore(object): 19 | """Represents Django apps in the django-mongonaut admin.""" 20 | 21 | def __init__(self, module): 22 | self.models = [] 23 | self.add_module(module) 24 | 25 | def add_module(self, module, max_depth=1): 26 | for key in module.__dict__.keys(): 27 | model_candidate = getattr(module, key) 28 | if hasattr(model_candidate, 'mongoadmin'): 29 | self.add_model(model_candidate) 30 | 31 | elif (max_depth > 0 32 | and isinstance(model_candidate, types.ModuleType)): 33 | self.add_module(model_candidate, max_depth - 1) 34 | 35 | def add_model(self, model): 36 | model.name = model.__name__ 37 | self.models.append(model) 38 | 39 | 40 | class MongonautViewMixin(object): 41 | """Used for all views in the project, handles authorization to content, 42 | viewing, controlling and setting of data. 43 | """ 44 | 45 | def render_to_response(self, context, **response_kwargs): 46 | if hasattr(self, 'permission') and not self.request.user.has_perm(self.permission): 47 | return HttpResponseForbidden("You do not have permissions to access this content. Login as a superuser to view and edit data.") 48 | 49 | return self.response_class( 50 | request=self.request, 51 | template=self.get_template_names(), 52 | context=context, 53 | **response_kwargs 54 | ) 55 | 56 | def get_context_data(self, **kwargs): 57 | context = super(MongonautViewMixin, self).get_context_data(**kwargs) 58 | context['MONGONAUT_JQUERY'] = getattr(settings, "MONGONAUT_JQUERY", 59 | "http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js") 60 | context['MONGONAUT_TWITTER_BOOTSTRAP'] = getattr(settings, "MONGONAUT_TWITTER_BOOTSTRAP", 61 | "//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css") 62 | context['MONGONAUT_TWITTER_BOOTSTRAP_ALERT'] = getattr(settings, 63 | "MONGONAUT_TWITTER_BOOTSTRAP_ALERT", 64 | "//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js") 65 | return context 66 | 67 | def get_mongoadmins(self): 68 | """ Returns a list of all mongoadmin implementations for the site """ 69 | apps = [] 70 | for app_name in settings.INSTALLED_APPS: 71 | mongoadmin = "{0}.mongoadmin".format(app_name) 72 | try: 73 | module = import_module(mongoadmin) 74 | except ImportError as e: 75 | if str(e).startswith("No module named"): 76 | continue 77 | raise e 78 | 79 | app_store = AppStore(module) 80 | apps.append(dict( 81 | app_name=app_name, 82 | obj=app_store 83 | )) 84 | return apps 85 | 86 | def set_mongonaut_base(self): 87 | """ Sets a number of commonly used attributes """ 88 | if hasattr(self, "app_label"): 89 | # prevents us from calling this multiple times 90 | return None 91 | self.app_label = self.kwargs.get('app_label') 92 | self.document_name = self.kwargs.get('document_name') 93 | 94 | # TODO Allow this to be assigned via url variable 95 | self.models_name = self.kwargs.get('models_name', 'models') 96 | 97 | # import the models file 98 | self.model_name = "{0}.{1}".format(self.app_label, self.models_name) 99 | self.models = import_module(self.model_name) 100 | 101 | def set_mongoadmin(self): 102 | """ Returns the MongoAdmin object for an app_label/document_name style view 103 | """ 104 | if hasattr(self, "mongoadmin"): 105 | return None 106 | 107 | if not hasattr(self, "document_name"): 108 | self.set_mongonaut_base() 109 | 110 | for mongoadmin in self.get_mongoadmins(): 111 | for model in mongoadmin['obj'].models: 112 | if model.name == self.document_name: 113 | self.mongoadmin = model.mongoadmin 114 | break 115 | # TODO change this to use 'finally' or 'else' or something 116 | if not hasattr(self, "mongoadmin"): 117 | raise NoMongoAdminSpecified("No MongoAdmin for {0}.{1}".format(self.app_label, self.document_name)) 118 | 119 | def set_permissions_in_context(self, context={}): 120 | """ Provides permissions for mongoadmin for use in the context""" 121 | 122 | context['has_view_permission'] = self.mongoadmin.has_view_permission(self.request) 123 | context['has_edit_permission'] = self.mongoadmin.has_edit_permission(self.request) 124 | context['has_add_permission'] = self.mongoadmin.has_add_permission(self.request) 125 | context['has_delete_permission'] = self.mongoadmin.has_delete_permission(self.request) 126 | return context 127 | 128 | 129 | class MongonautFormViewMixin(object): 130 | """ 131 | View used to help with processing of posted forms. 132 | Must define self.document_type for process_post_form to work. 133 | """ 134 | 135 | def process_post_form(self, success_message=None): 136 | """ 137 | As long as the form is set on the view this method will validate the form 138 | and save the submitted data. Only call this if you are posting data. 139 | The given success_message will be used with the djanog messages framework 140 | if the posted data sucessfully submits. 141 | """ 142 | 143 | # When on initial args are given we need to set the base document. 144 | if not hasattr(self, 'document') or self.document is None: 145 | self.document = self.document_type() 146 | self.form = MongoModelForm(model=self.document_type, instance=self.document, 147 | form_post_data=self.request.POST).get_form() 148 | self.form.is_bound = True 149 | if self.form.is_valid(): 150 | 151 | self.document_map_dict = MongoModelForm(model=self.document_type).create_document_dictionary(self.document_type) 152 | self.new_document = self.document_type 153 | 154 | # Used to keep track of embedded documents in lists. Keyed by the list and the number of the 155 | # document. 156 | self.embedded_list_docs = {} 157 | 158 | if self.new_document is None: 159 | messages.error(self.request, u"Failed to save document") 160 | else: 161 | self.new_document = self.new_document() 162 | 163 | for form_key in self.form.cleaned_data.keys(): 164 | if form_key == 'id' and hasattr(self, 'document'): 165 | self.new_document.id = self.document.id 166 | continue 167 | self.process_document(self.new_document, form_key, None) 168 | 169 | self.new_document.save() 170 | if success_message: 171 | messages.success(self.request, success_message) 172 | 173 | return self.form 174 | 175 | def process_document(self, document, form_key, passed_key): 176 | """ 177 | Given the form_key will evaluate the document and set values correctly for 178 | the document given. 179 | """ 180 | if passed_key is not None: 181 | current_key, remaining_key_array = trim_field_key(document, passed_key) 182 | else: 183 | current_key, remaining_key_array = trim_field_key(document, form_key) 184 | 185 | key_array_digit = remaining_key_array[-1] if remaining_key_array and has_digit(remaining_key_array) else None 186 | remaining_key = make_key(remaining_key_array) 187 | 188 | if current_key.lower() == 'id': 189 | raise KeyError(u"Mongonaut does not work with models which have fields beginning with id_") 190 | 191 | # Create boolean checks to make processing document easier 192 | is_embedded_doc = (isinstance(document._fields.get(current_key, None), EmbeddedDocumentField) 193 | if hasattr(document, '_fields') else False) 194 | is_list = not key_array_digit is None 195 | key_in_fields = current_key in document._fields.keys() if hasattr(document, '_fields') else False 196 | 197 | # This ensures you only go through each documents keys once, and do not duplicate data 198 | if key_in_fields: 199 | if is_embedded_doc: 200 | self.set_embedded_doc(document, form_key, current_key, remaining_key) 201 | elif is_list: 202 | self.set_list_field(document, form_key, current_key, remaining_key, key_array_digit) 203 | else: 204 | value = translate_value(document._fields[current_key], 205 | self.form.cleaned_data[form_key]) 206 | setattr(document, current_key, value) 207 | 208 | def set_embedded_doc(self, document, form_key, current_key, remaining_key): 209 | """Get the existing embedded document if it exists, else created it.""" 210 | 211 | embedded_doc = getattr(document, current_key, False) 212 | if not embedded_doc: 213 | embedded_doc = document._fields[current_key].document_type_obj() 214 | 215 | new_key, new_remaining_key_array = trim_field_key(embedded_doc, remaining_key) 216 | self.process_document(embedded_doc, form_key, make_key(new_key, new_remaining_key_array)) 217 | setattr(document, current_key, embedded_doc) 218 | 219 | def set_list_field(self, document, form_key, current_key, remaining_key, key_array_digit): 220 | """1. Figures out what value the list ought to have 221 | 2. Sets the list 222 | """ 223 | 224 | document_field = document._fields.get(current_key) 225 | 226 | # Figure out what value the list ought to have 227 | # None value for ListFields make mongoengine very un-happy 228 | list_value = translate_value(document_field.field, self.form.cleaned_data[form_key]) 229 | if list_value is None or (not list_value and not bool(list_value)): 230 | return None 231 | 232 | current_list = getattr(document, current_key, None) 233 | 234 | if isinstance(document_field.field, EmbeddedDocumentField): 235 | embedded_list_key = u"{0}_{1}".format(current_key, key_array_digit) 236 | 237 | # Get the embedded document if it exists, else create it. 238 | embedded_list_document = self.embedded_list_docs.get(embedded_list_key, None) 239 | if embedded_list_document is None: 240 | embedded_list_document = document_field.field.document_type_obj() 241 | 242 | new_key, new_remaining_key_array = trim_field_key(embedded_list_document, remaining_key) 243 | self.process_document(embedded_list_document, form_key, new_key) 244 | 245 | list_value = embedded_list_document 246 | self.embedded_list_docs[embedded_list_key] = embedded_list_document 247 | 248 | if isinstance(current_list, list): 249 | # Do not add the same document twice 250 | if embedded_list_document not in current_list: 251 | current_list.append(embedded_list_document) 252 | else: 253 | setattr(document, current_key, [embedded_list_document]) 254 | 255 | elif isinstance(current_list, list): 256 | current_list.append(list_value) 257 | else: 258 | setattr(document, current_key, [list_value]) 259 | -------------------------------------------------------------------------------- /mongonaut/forms/form_mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | from copy import deepcopy 5 | 6 | from django import forms 7 | from mongoengine.base import BaseList 8 | from mongoengine.base import TopLevelDocumentMetaclass 9 | from mongoengine.fields import Document 10 | from mongoengine.fields import EmbeddedDocumentField 11 | from mongoengine.fields import ListField 12 | from mongoengine.fields import ReferenceField 13 | 14 | from .form_utils import FieldTuple 15 | from .form_utils import has_digit 16 | from .form_utils import make_key 17 | from .widgets import get_form_field_class 18 | from mongonaut.utils import trim_field_key 19 | 20 | try: 21 | # OrderedDict New in version 2.7 22 | from collections import OrderedDict 23 | except ImportError: 24 | OrderedDict = dict 25 | 26 | CHECK_ATTRS = {'required': 'required', 27 | 'help_text': 'help_text', 28 | 'name': 'name'} 29 | 30 | 31 | def get_document_unicode(document): 32 | """Safely converts MongoDB document strings to unicode.""" 33 | try: 34 | return document.__unicode__() 35 | except AttributeError: 36 | return six.text_type(document) 37 | 38 | 39 | class MongoModelFormBaseMixin(object): 40 | """ 41 | For use with mongoengine. 42 | 43 | This mixin should not be used alone it should be used to inherit from. 44 | 45 | This mixin provides functionality for generating a form. Provides 4 methods 46 | useful for putting data on a form: 47 | 48 | get_form_field_dict -- creates a keyed tuple representation of a model field used 49 | to create form fields 50 | set_form_fields -- takes the form field dictionary and sets all values on a form 51 | set_form_field -- sets an individual form field 52 | get_field_value -- returns the value for the field 53 | 54 | If you inherit from this class you will need to call the above methods 55 | with the correct values, see forms.py for an example. 56 | """ 57 | 58 | def __init__(self, model, instance=None, form_post_data=None): 59 | """ 60 | Params: 61 | model -- The model class to create the form with 62 | instance -- An instance of the model class can be used to 63 | initialize data. 64 | form_post_data -- Values given by request.POST 65 | """ 66 | self.model = model 67 | self.model_instance = instance 68 | self.post_data_dict = form_post_data 69 | # Preferred for symantic checks of model_instance 70 | self.is_initialized = False if instance is None else True 71 | self.form = forms.Form() 72 | 73 | if not isinstance(self.model, TopLevelDocumentMetaclass): 74 | raise TypeError(u"The model supplied must be a mongoengine Document") 75 | 76 | if self.is_initialized and not isinstance(self.model_instance, self.model): 77 | raise TypeError(u"The provided instance must be an instance of the given model") 78 | 79 | if self.post_data_dict is not None and not isinstance(self.post_data_dict, dict): 80 | raise TypeError(u"You must pass in a dictionary for form_post_data") 81 | 82 | def get_form_field_dict(self, model_dict): 83 | """ 84 | Takes a model dictionary representation and creates a dictionary 85 | keyed by form field. Each value is a keyed 4 tuple of: 86 | (widget, mode_field_instance, model_field_type, field_key) 87 | """ 88 | return_dict = OrderedDict() 89 | # Workaround: mongoengine doesn't preserve form fields ordering from metaclass __new__ 90 | if hasattr(self.model, 'Meta') and hasattr(self.model.Meta, 'form_fields_ordering'): 91 | field_order_list = tuple(form_field for form_field 92 | in self.model.Meta.form_fields_ordering 93 | if form_field in model_dict.iterkeys()) 94 | order_dict = OrderedDict.fromkeys(field_order_list) 95 | return_dict = order_dict 96 | 97 | for field_key, field_dict in sorted(model_dict.items()): 98 | if not field_key.startswith("_"): 99 | widget = field_dict.get('_widget', None) 100 | if widget is None: 101 | return_dict[field_key] = self.get_form_field_dict(field_dict) 102 | return_dict[field_key].update({'_field_type': field_dict.get('_field_type', None)}) 103 | else: 104 | return_dict[field_key] = FieldTuple(widget, 105 | field_dict.get('_document_field', None), 106 | field_dict.get('_field_type', None), 107 | field_dict.get('_key', None)) 108 | return return_dict 109 | 110 | def set_form_fields(self, form_field_dict, parent_key=None, field_type=None): 111 | """ 112 | Set the form fields for every key in the form_field_dict. 113 | 114 | Params: 115 | form_field_dict -- a dictionary created by get_form_field_dict 116 | parent_key -- the key for the previous key in the recursive call 117 | field_type -- used to determine what kind of field we are setting 118 | """ 119 | for form_key, field_value in form_field_dict.items(): 120 | form_key = make_key(parent_key, form_key) if parent_key is not None else form_key 121 | if isinstance(field_value, tuple): 122 | 123 | set_list_class = False 124 | base_key = form_key 125 | 126 | # Style list fields 127 | if ListField in (field_value.field_type, field_type): 128 | 129 | # Nested lists/embedded docs need special care to get 130 | # styles to work out nicely. 131 | if parent_key is None or ListField == field_value.field_type: 132 | if field_type != EmbeddedDocumentField: 133 | field_value.widget.attrs['class'] += ' listField {0}'.format(form_key) 134 | set_list_class = True 135 | else: 136 | field_value.widget.attrs['class'] += ' listField' 137 | 138 | # Compute number value for list key 139 | list_keys = [field_key for field_key in self.form.fields.keys() 140 | if has_digit(field_key)] 141 | 142 | key_int = 0 143 | while form_key in list_keys: 144 | key_int += 1 145 | form_key = make_key(form_key, key_int) 146 | 147 | if parent_key is not None: 148 | 149 | # Get the base key for our embedded field class 150 | valid_base_keys = [model_key for model_key in self.model_map_dict.keys() 151 | if not model_key.startswith("_")] 152 | while base_key not in valid_base_keys and base_key: 153 | base_key = make_key(base_key, exclude_last_string=True) 154 | 155 | # We need to remove the trailing number from the key 156 | # so that grouping will occur on the front end when we have a list. 157 | embedded_key_class = None 158 | if set_list_class: 159 | field_value.widget.attrs['class'] += " listField".format(base_key) 160 | embedded_key_class = make_key(field_key, exclude_last_string=True) 161 | 162 | field_value.widget.attrs['class'] += " embeddedField" 163 | 164 | # Setting the embedded key correctly allows to visually nest the 165 | # embedded documents on the front end. 166 | if base_key == parent_key: 167 | field_value.widget.attrs['class'] += ' {0}'.format(base_key) 168 | else: 169 | field_value.widget.attrs['class'] += ' {0} {1}'.format(base_key, parent_key) 170 | 171 | if embedded_key_class is not None: 172 | field_value.widget.attrs['class'] += ' {0}'.format(embedded_key_class) 173 | 174 | default_value = self.get_field_value(form_key) 175 | 176 | # Style embedded documents 177 | if isinstance(default_value, list) and len(default_value) > 0: 178 | key_index = int(form_key.split("_")[-1]) 179 | new_base_key = make_key(form_key, exclude_last_string=True) 180 | 181 | for list_value in default_value: 182 | # Note, this is copied every time so each widget gets a different class 183 | list_widget = deepcopy(field_value.widget) 184 | new_key = make_key(new_base_key, six.text_type(key_index)) 185 | list_widget.attrs['class'] += " {0}".format(make_key(base_key, key_index)) 186 | self.set_form_field(list_widget, field_value.document_field, new_key, list_value) 187 | key_index += 1 188 | else: 189 | self.set_form_field(field_value.widget, field_value.document_field, 190 | form_key, default_value) 191 | 192 | elif isinstance(field_value, dict): 193 | self.set_form_fields(field_value, form_key, field_value.get("_field_type", None)) 194 | 195 | def set_form_field(self, widget, model_field, field_key, default_value): 196 | """ 197 | Parmams: 198 | widget -- the widget to use for displyaing the model_field 199 | model_field -- the field on the model to create a form field with 200 | field_key -- the name for the field on the form 201 | default_value -- the value to give for the field 202 | Default: None 203 | """ 204 | # Empty lists cause issues on form validation 205 | if default_value == []: 206 | default_value = None 207 | 208 | if widget and isinstance(widget, forms.widgets.Select): 209 | self.form.fields[field_key] = forms.ChoiceField(label=model_field.name, 210 | required=model_field.required, 211 | widget=widget) 212 | else: 213 | field_class = get_form_field_class(model_field) 214 | self.form.fields[field_key] = field_class(label=model_field.name, 215 | required=model_field.required, 216 | widget=widget) 217 | 218 | if default_value is not None: 219 | if isinstance(default_value, Document): 220 | # Probably a reference field, therefore, add id 221 | self.form.fields[field_key].initial = getattr(default_value, 'id', None) 222 | else: 223 | self.form.fields[field_key].initial = default_value 224 | else: 225 | self.form.fields[field_key].initial = getattr(model_field, 'default', None) 226 | 227 | if isinstance(model_field, ReferenceField): 228 | self.form.fields[field_key].choices = [(six.text_type(x.id), get_document_unicode(x)) 229 | for x in model_field.document_type.objects.all()] 230 | # Adding in blank choice so a reference field can be deleted by selecting blank 231 | self.form.fields[field_key].choices.insert(0, ("", "")) 232 | 233 | elif model_field.choices: 234 | self.form.fields[field_key].choices = model_field.choices 235 | 236 | for key, form_attr in CHECK_ATTRS.items(): 237 | if hasattr(model_field, key): 238 | value = getattr(model_field, key) 239 | setattr(self.form.fields[field_key], key, value) 240 | 241 | def get_field_value(self, field_key): 242 | """ 243 | Given field_key will return value held at self.model_instance. If 244 | model_instance has not been provided will return None. 245 | """ 246 | 247 | def get_value(document, field_key): 248 | # Short circuit the function if we do not have a document 249 | if document is None: 250 | return None 251 | 252 | current_key, new_key_array = trim_field_key(document, field_key) 253 | key_array_digit = int(new_key_array[-1]) if new_key_array and has_digit(new_key_array) else None 254 | new_key = make_key(new_key_array) 255 | 256 | if key_array_digit is not None and len(new_key_array) > 0: 257 | # Handleing list fields 258 | if len(new_key_array) == 1: 259 | return_data = document._data.get(current_key, []) 260 | elif isinstance(document, BaseList): 261 | return_list = [] 262 | if len(document) > 0: 263 | return_list = [get_value(doc, new_key) for doc in document] 264 | return_data = return_list 265 | else: 266 | return_data = get_value(getattr(document, current_key), new_key) 267 | 268 | elif len(new_key_array) > 0: 269 | return_data = get_value(document._data.get(current_key), new_key) 270 | else: 271 | # Handeling all other fields and id 272 | try: # Added try except otherwise we get "TypeError: getattr(): attribute name must be string" error from mongoengine/base/datastructures.py 273 | return_data = (document._data.get(None, None) if current_key == "id" else 274 | document._data.get(current_key, None)) 275 | except: 276 | return_data = document._data.get(current_key, None) 277 | 278 | return return_data 279 | 280 | if self.is_initialized: 281 | return get_value(self.model_instance, field_key) 282 | else: 283 | return None 284 | -------------------------------------------------------------------------------- /mongonaut/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | TODO move permission checks to the dispatch view thingee 4 | """ 5 | import math 6 | 7 | from django.contrib import messages 8 | from django.urls import reverse 9 | from django.forms import Form 10 | from django.http import HttpResponseForbidden 11 | from django.http import Http404 12 | from django.utils.functional import cached_property 13 | from django.views.generic.edit import DeletionMixin 14 | from django.views.generic import ListView 15 | from django.views.generic import TemplateView 16 | from django.views.generic.edit import FormView 17 | from mongoengine.fields import EmbeddedDocumentField, ListField 18 | 19 | from mongonaut.forms import MongoModelForm 20 | from mongonaut.mixins import MongonautFormViewMixin 21 | from mongonaut.mixins import MongonautViewMixin 22 | from mongonaut.utils import is_valid_object_id 23 | 24 | 25 | class IndexView(MongonautViewMixin, ListView): 26 | """Lists all the apps with mongoadmins attached.""" 27 | 28 | template_name = "mongonaut/index.html" 29 | queryset = [] 30 | permission = 'has_view_permission' 31 | 32 | def get_queryset(self): 33 | return self.get_mongoadmins() 34 | 35 | 36 | class DocumentListView(MongonautViewMixin, FormView): 37 | """ 38 | Lists individual mongoengine documents for a model. 39 | :args: 40 | 41 | TODO - Make a generic document fetcher method 42 | """ 43 | form_class = Form 44 | success_url = '/' 45 | template_name = "mongonaut/document_list.html" 46 | permission = 'has_view_permission' 47 | 48 | documents_per_page = 25 49 | 50 | #def dispatch(self, *args, **kwargs): 51 | # self.set_mongoadmin() 52 | # self.set_permissions() 53 | # return super(DocumentListView, self).dispatch(*args, **kwargs) 54 | 55 | def get_qset(self, queryset, q): 56 | """Performs filtering against the default queryset returned by 57 | mongoengine. 58 | """ 59 | if self.mongoadmin.search_fields and q: 60 | params = {} 61 | for field in self.mongoadmin.search_fields: 62 | if field == 'id': 63 | # check to make sure this is a valid ID, otherwise we just continue 64 | if is_valid_object_id(q): 65 | return queryset.filter(pk=q) 66 | continue 67 | search_key = "{field}__icontains".format(field=field) 68 | params[search_key] = q 69 | 70 | queryset = queryset.filter(**params) 71 | return queryset 72 | 73 | @cached_property 74 | def get_queryset(self): 75 | """Replicates Django CBV `get_queryset()` method, but for MongoEngine. 76 | """ 77 | if hasattr(self, "queryset") and self.queryset: 78 | return self.queryset 79 | 80 | self.set_mongonaut_base() 81 | self.set_mongoadmin() 82 | self.document = getattr(self.models, self.document_name) 83 | queryset = self.document.objects.all() 84 | 85 | if self.mongoadmin.ordering: 86 | queryset = queryset.order_by(*self.mongoadmin.ordering) 87 | 88 | # search. move this to get_queryset 89 | # search. move this to get_queryset 90 | q = self.request.GET.get('q') 91 | queryset = self.get_qset(queryset, q) 92 | 93 | ### Start pagination 94 | ### Note: 95 | ### Can't use Paginator in Django because mongoengine querysets are 96 | ### not the same as Django ORM querysets and it broke. 97 | # Make sure page request is an int. If not, deliver first page. 98 | try: 99 | self.page = int(self.request.GET.get('page', '1')) 100 | except ValueError: 101 | self.page = 1 102 | 103 | obj_count = queryset.count() 104 | self.total_pages = math.ceil(obj_count / self.documents_per_page) 105 | 106 | if self.page > self.total_pages: 107 | self.page = self.total_pages 108 | 109 | if self.page < 1: 110 | self.page = 1 111 | 112 | start = (self.page - 1) * self.documents_per_page 113 | end = self.page * self.documents_per_page 114 | 115 | queryset = queryset[start:end] if obj_count else queryset 116 | self.queryset = queryset 117 | return queryset 118 | 119 | def get_initial(self): 120 | """Used during adding/editing of data.""" 121 | self.query = self.get_queryset() 122 | mongo_ids = {'mongo_id': [str(x.id) for x in self.query]} 123 | return mongo_ids 124 | 125 | def get_context_data(self, **kwargs): 126 | """Injects data into the context to replicate CBV ListView.""" 127 | context = super(DocumentListView, self).get_context_data(**kwargs) 128 | context = self.set_permissions_in_context(context) 129 | 130 | if not context['has_view_permission']: 131 | return HttpResponseForbidden("You do not have permissions to view this content.") 132 | 133 | context['object_list'] = self.get_queryset() 134 | 135 | context['document'] = self.document 136 | context['app_label'] = self.app_label 137 | context['document_name'] = self.document_name 138 | context['request'] = self.request 139 | 140 | # pagination bits 141 | context['page'] = self.page 142 | context['documents_per_page'] = self.documents_per_page 143 | 144 | if self.page > 1: 145 | previous_page_number = self.page - 1 146 | else: 147 | previous_page_number = None 148 | 149 | if self.page < self.total_pages: 150 | next_page_number = self.page + 1 151 | else: 152 | next_page_number = None 153 | 154 | context['previous_page_number'] = previous_page_number 155 | context['has_previous_page'] = previous_page_number is not None 156 | context['next_page_number'] = next_page_number 157 | context['has_next_page'] = next_page_number is not None 158 | context['total_pages'] = self.total_pages 159 | 160 | # Part of upcoming list view form functionality 161 | if self.queryset.count(): 162 | context['keys'] = ['id', ] 163 | 164 | # Show those items for which we've got list_fields on the mongoadmin 165 | for key in [x for x in self.mongoadmin.list_fields if x != 'id' and x in self.document._fields.keys()]: 166 | 167 | # TODO - Figure out why this EmbeddedDocumentField and ListField breaks this view 168 | # Note - This is the challenge part, right? :) 169 | if isinstance(self.document._fields[key], EmbeddedDocumentField): 170 | continue 171 | if isinstance(self.document._fields[key], ListField): 172 | continue 173 | context['keys'].append(key) 174 | 175 | if self.mongoadmin.search_fields: 176 | context['search_field'] = True 177 | 178 | return context 179 | 180 | def post(self, request, *args, **kwargs): 181 | """Creates new mongoengine records.""" 182 | # TODO - make sure to check the rights of the poster 183 | #self.get_queryset() # TODO - write something that grabs the document class better 184 | form_class = self.get_form_class() 185 | form = self.get_form(form_class) 186 | mongo_ids = self.get_initial()['mongo_id'] 187 | for form_mongo_id in form.data.getlist('mongo_id'): 188 | for mongo_id in mongo_ids: 189 | if form_mongo_id == mongo_id: 190 | self.document.objects.get(pk=mongo_id).delete() 191 | 192 | return self.form_invalid(form) 193 | 194 | 195 | class DocumentDetailView(MongonautViewMixin, TemplateView): 196 | """ :args: """ 197 | template_name = "mongonaut/document_detail.html" 198 | permission = 'has_view_permission' 199 | 200 | def get_context_data(self, **kwargs): 201 | context = super(DocumentDetailView, self).get_context_data(**kwargs) 202 | self.set_mongoadmin() 203 | context = self.set_permissions_in_context(context) 204 | self.document_type = getattr(self.models, self.document_name) 205 | self.ident = self.kwargs.get('id') 206 | self.document = self.document_type.objects.get(pk=self.ident) 207 | 208 | context['document'] = self.document 209 | context['app_label'] = self.app_label 210 | context['document_name'] = self.document_name 211 | context['keys'] = ['id', ] 212 | context['embedded_documents'] = [] 213 | context['list_fields'] = [] 214 | for key in sorted([x for x in self.document._fields.keys() if x != 'id']): 215 | # TODO - Figure out why this EmbeddedDocumentField and ListField breaks this view 216 | # Note - This is the challenge part, right? :) 217 | if isinstance(self.document._fields[key], EmbeddedDocumentField): 218 | context['embedded_documents'].append(key) 219 | continue 220 | if isinstance(self.document._fields[key], ListField): 221 | context['list_fields'].append(key) 222 | continue 223 | context['keys'].append(key) 224 | return context 225 | 226 | 227 | class DocumentEditFormView(MongonautViewMixin, FormView, MongonautFormViewMixin): 228 | """ :args: """ 229 | 230 | template_name = "mongonaut/document_edit_form.html" 231 | form_class = Form 232 | success_url = '/' 233 | permission = 'has_edit_permission' 234 | 235 | def get_success_url(self): 236 | self.set_mongonaut_base() 237 | return reverse('document_detail_edit_form', kwargs={'app_label': self.app_label, 'document_name': self.document_name, 'id': self.kwargs.get('id')}) 238 | 239 | def get_context_data(self, **kwargs): 240 | context = super(DocumentEditFormView, self).get_context_data(**kwargs) 241 | self.set_mongoadmin() 242 | context = self.set_permissions_in_context(context) 243 | self.document_type = getattr(self.models, self.document_name) 244 | self.ident = self.kwargs.get('id') 245 | self.document = self.document_type.objects.get(pk=self.ident) 246 | 247 | context['document'] = self.document 248 | context['app_label'] = self.app_label 249 | context['document_name'] = self.document_name 250 | context['form_action'] = reverse('document_detail_edit_form', args=[self.kwargs.get('app_label'), 251 | self.kwargs.get('document_name'), 252 | self.kwargs.get('id')]) 253 | 254 | return context 255 | 256 | def get_form(self): #get_form(self, Form) leads to "get_form() missing 1 required positional argument: 'Form'" error." 257 | self.set_mongoadmin() 258 | context = self.set_permissions_in_context({}) 259 | 260 | if not context['has_edit_permission']: 261 | return HttpResponseForbidden("You do not have permissions to edit this content.") 262 | 263 | self.document_type = getattr(self.models, self.document_name) 264 | self.ident = self.kwargs.get('id') 265 | try: 266 | self.document = self.document_type.objects.get(pk=self.ident) 267 | except self.document_type.DoesNotExist: 268 | raise Http404 269 | self.form = Form() 270 | 271 | if self.request.method == 'POST': 272 | self.form = self.process_post_form('Your changes have been saved.') 273 | else: 274 | self.form = MongoModelForm(model=self.document_type, instance=self.document).get_form() 275 | return self.form 276 | 277 | 278 | class DocumentAddFormView(MongonautViewMixin, FormView, MongonautFormViewMixin): 279 | """ :args: """ 280 | 281 | template_name = "mongonaut/document_add_form.html" 282 | form_class = Form 283 | success_url = '/' 284 | permission = 'has_add_permission' 285 | 286 | def get_success_url(self): 287 | self.set_mongonaut_base() 288 | return reverse('document_detail', kwargs={'app_label': self.app_label, 'document_name': self.document_name, 'id': str(self.new_document.id)}) 289 | 290 | def get_context_data(self, **kwargs): 291 | """ TODO - possibly inherit this from DocumentEditFormView. This is same thing minus: 292 | self.ident = self.kwargs.get('id') 293 | self.document = self.document_type.objects.get(pk=self.ident) 294 | """ 295 | context = super(DocumentAddFormView, self).get_context_data(**kwargs) 296 | self.set_mongoadmin() 297 | context = self.set_permissions_in_context(context) 298 | self.document_type = getattr(self.models, self.document_name) 299 | 300 | context['app_label'] = self.app_label 301 | context['document_name'] = self.document_name 302 | context['form_action'] = reverse('document_detail_add_form', args=[self.kwargs.get('app_label'), 303 | self.kwargs.get('document_name')]) 304 | 305 | return context 306 | 307 | def get_form(self): 308 | self.set_mongonaut_base() 309 | self.document_type = getattr(self.models, self.document_name) 310 | self.form = Form() 311 | 312 | if self.request.method == 'POST': 313 | self.form = self.process_post_form('Your new document has been added and saved.') 314 | else: 315 | self.form = MongoModelForm(model=self.document_type).get_form() 316 | return self.form 317 | 318 | 319 | class DocumentDeleteView(DeletionMixin, MongonautViewMixin, TemplateView): 320 | """ :args: 321 | 322 | TODO - implement a GET view for confirmation 323 | """ 324 | 325 | success_url = "/" 326 | template_name = "mongonaut/document_delete.html" 327 | 328 | def get_success_url(self): 329 | self.set_mongonaut_base() 330 | messages.add_message(self.request, messages.INFO, 'Your document has been deleted.') 331 | return reverse('document_list', kwargs={'app_label': self.app_label, 'document_name': self.document_name}) 332 | 333 | def get_object(self): 334 | self.set_mongoadmin() 335 | self.document_type = getattr(self.models, self.document_name) 336 | self.ident = self.kwargs.get('id') 337 | self.document = self.document_type.objects.get(pk=self.ident) 338 | return self.document 339 | --------------------------------------------------------------------------------