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 |
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 |
91 |
92 |
93 | {% for key in embedded_documents %}
94 |
95 |
{{ key}}
96 |
{% get_document_value document key %}
97 |
98 | {% endfor %}
99 |
100 |
101 | {% endif %}
102 |
103 |
104 |
105 |
106 | {% endblock %}
107 |
108 | {% block extrajs %}
109 |
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 |
--------------------------------------------------------------------------------