├── AUTHORS ├── TODO ├── examples └── blogprj │ ├── __init__.py │ ├── apps │ ├── __init__.py │ └── blog │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ ├── templates │ │ ├── blogpost │ │ │ ├── base.html │ │ │ ├── confirm_delete.html │ │ │ ├── detail.html │ │ │ ├── form.html │ │ │ ├── list.html │ │ │ └── post.html │ │ └── tag │ │ │ ├── detail.html │ │ │ └── form.html │ │ ├── urls.py │ │ └── views.py │ ├── manage.py │ ├── mongotools │ ├── secret.txt │ ├── settings.py │ ├── templates │ └── base.html │ └── urls.py ├── mongotools ├── __init__.py ├── forms │ ├── __init__.py │ ├── fields.py │ └── utils.py └── views │ └── __init__.py └── setup.py /AUTHORS: -------------------------------------------------------------------------------- 1 | Stephan Jaekel 2 | Wilson Pinto Júnior -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Generate view with only document class 2 | * write a full documentation 3 | * support slug fields 4 | -------------------------------------------------------------------------------- /examples/blogprj/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpjunior/django-mongotools/06bfe622e766729b48d42d4865e3fb9141e433d1/examples/blogprj/__init__.py -------------------------------------------------------------------------------- /examples/blogprj/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpjunior/django-mongotools/06bfe622e766729b48d42d4865e3fb9141e433d1/examples/blogprj/apps/__init__.py -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpjunior/django-mongotools/06bfe622e766729b48d42d4865e3fb9141e433d1/examples/blogprj/apps/blog/__init__.py -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django import forms 4 | from mongotools.forms import MongoForm 5 | from models import BlogPost, Tag 6 | 7 | class TagForm(MongoForm): 8 | class Meta: 9 | document = Tag 10 | fields = ('tag',) 11 | 12 | class BlogPostForm(MongoForm): 13 | class Meta: 14 | document = BlogPost 15 | fields = ('author', 'title', 'content', 'published', 'tags',) 16 | content = forms.CharField(widget=forms.Textarea) 17 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.template.defaultfilters import slugify 4 | from django.core.urlresolvers import reverse 5 | from django.db.models import permalink 6 | 7 | from mongoengine import * 8 | 9 | class Tag(Document): 10 | tag = StringField(max_length=100, required=True) 11 | created = DateTimeField() 12 | 13 | @property 14 | def posts_for_tag(self): 15 | return BlogPost.published_posts().filter(tags=self) 16 | 17 | def save(self, **kwargs): 18 | if self.created is None: 19 | self.created = datetime.now() 20 | 21 | return super(Tag, self).save(**kwargs) 22 | 23 | @permalink 24 | def get_absolute_url(self): 25 | return ('tag_detail', [self.pk,]) 26 | 27 | def __unicode__(self): 28 | return self.tag 29 | 30 | meta = { 31 | 'ordering': ['-created'], 32 | } 33 | 34 | 35 | class BlogPost(Document): 36 | published = BooleanField(default=False) 37 | author = StringField(required=True) 38 | title = StringField(required=True) 39 | slug = StringField() 40 | content = StringField(required=True) 41 | 42 | tags = ListField(ReferenceField(Tag)) 43 | 44 | datetime_added = DateTimeField(default=datetime.now) 45 | 46 | def save(self): 47 | if self.slug is None: 48 | slug = slugify(self.title) 49 | new_slug = slug 50 | c = 1 51 | while True: 52 | try: 53 | BlogPost.objects.get(slug=new_slug) 54 | except BlogPost.DoesNotExist: 55 | break 56 | else: 57 | c += 1 58 | new_slug = '%s-%s' % (slug, c) 59 | self.slug = new_slug 60 | return super(BlogPost, self).save() 61 | 62 | def get_absolute_url(self): 63 | return '/%s/' % str(self.pk) 64 | 65 | @queryset_manager 66 | def published_posts(doc_cls, queryset): 67 | return queryset(published=True) 68 | 69 | meta = { 70 | 'ordering': ['-datetime_added'] 71 | } 72 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/templates/blogpost/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/templates/blogpost/confirm_delete.html: -------------------------------------------------------------------------------- 1 |
2 | You confirm delete ? 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/templates/blogpost/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 | Back to index 5 | 6 | {% include "blogpost/post.html" %} 7 | {% endblock %} 8 | 9 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/templates/blogpost/form.html: -------------------------------------------------------------------------------- 1 | {% extends "blogpost/base.html" %} 2 | 3 | {% block header %} 4 | 10 | {% endblock %} 11 | 12 | {% block body %} 13 | Back to index 14 | 15 |
16 | {{ form.as_p }} 17 | 18 | 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/templates/blogpost/list.html: -------------------------------------------------------------------------------- 1 | {% extends "blogpost/base.html" %} 2 | 3 | {% block body %} 4 | New post | New tag 5 | 6 | {% for object in object_list %} 7 | {% include "blogpost/post.html" %} 8 | {% endfor %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/templates/blogpost/post.html: -------------------------------------------------------------------------------- 1 |
5 |

6 | {{ object.title }} 7 | 8 | {% if not object.published %} 9 | (unpublished) 10 | {% endif %} 11 | 12 |

13 |
14 | edit 15 | delete 16 |
17 | 18 | by {{ object.author }}, {{ object.datetime_added|date:"d.m.Y H:i:s" }} | Tags: 19 | {% for t in object.tags %}{{ t.tag }}{% if not forloop.last %}, {% endif %}{% endfor %} 20 | 21 | 22 |

{{ object.content }}

23 |
24 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/templates/tag/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "blogpost/base.html" %} 2 | 3 | {% block body %} 4 | New post | New tag | Edit tag 5 | 6 |

Tag: {{ object.tag }}

7 | 8 | {% for object in object.posts_for_tag %} 9 | {% include "blogpost/post.html" %} 10 | {% endfor %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/templates/tag/form.html: -------------------------------------------------------------------------------- 1 | {% extends "blogpost/base.html" %} 2 | 3 | {% block header %} 4 | 10 | {% endblock %} 11 | 12 | {% block body %} 13 | Back to index 14 | 15 |
16 | {{ form.as_p }} 17 | 18 | 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls.defaults import * 4 | from django.views.generic.simple import redirect_to, direct_to_template 5 | from views import (AddPostView, UpdatePostView, PostIndexView, 6 | PostDetailView, DeletePostView, AddTagView, 7 | UpdateTagView, TagDetailView) 8 | 9 | entry_pattern = patterns('', 10 | (r'^$', PostDetailView.as_view()), 11 | (r'^edit/$', UpdatePostView.as_view()), 12 | (r'^delete/$', DeletePostView.as_view() ), 13 | ) 14 | 15 | tag_pattern = patterns('', 16 | url(r'^$', TagDetailView.as_view(), name='tag_detail'), 17 | (r'^edit/$', UpdateTagView.as_view()), 18 | ) 19 | 20 | urlpatterns = patterns('apps.blog.views', 21 | (r'^$', PostIndexView.as_view()), 22 | (r'^new/$', AddPostView.as_view()), 23 | (r'^newtag/$', AddTagView.as_view()), 24 | (r'^(?P\w{24})/', include(entry_pattern)), 25 | (r'^tags/(?P\w+)/', include(tag_pattern)), 26 | ) 27 | -------------------------------------------------------------------------------- /examples/blogprj/apps/blog/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from mongotools.views import (CreateView, UpdateView, 4 | DeleteView, ListView, 5 | DetailView) 6 | 7 | from models import BlogPost, Tag 8 | from forms import BlogPostForm, TagForm 9 | 10 | class PostIndexView(ListView): 11 | document = BlogPost 12 | 13 | class PostDetailView(DetailView): 14 | document = BlogPost 15 | 16 | class AddPostView(CreateView): 17 | document = BlogPost 18 | success_url = '/' 19 | form_class = BlogPostForm 20 | 21 | class DeletePostView(DeleteView): 22 | document = BlogPost 23 | success_url = '/' 24 | 25 | class UpdatePostView(UpdateView): 26 | document = BlogPost 27 | form_class = BlogPostForm 28 | 29 | class TagDetailView(DetailView): 30 | document = Tag 31 | 32 | class AddTagView(CreateView): 33 | document = Tag 34 | success_url = '/' 35 | form_class = TagForm 36 | 37 | class UpdateTagView(UpdateView): 38 | document = Tag 39 | form_class = TagForm 40 | -------------------------------------------------------------------------------- /examples/blogprj/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /examples/blogprj/mongotools: -------------------------------------------------------------------------------- 1 | IntxLNK../../mongotools -------------------------------------------------------------------------------- /examples/blogprj/secret.txt: -------------------------------------------------------------------------------- 1 | %ug9%u&ps4@mg*pxux*cxa^*0+mmvg*+swklwp_^9-lc*p6#ax -------------------------------------------------------------------------------- /examples/blogprj/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import platform 4 | import sys 5 | import os 6 | 7 | PROJECT_ROOT = os.path.dirname(__file__) 8 | sys.path.append(os.path.join(PROJECT_ROOT, '../../../')) 9 | from mongoengine import connect 10 | 11 | connect('mongotools_test') 12 | 13 | DEBUG = True 14 | TEMPLATE_DEBUG = DEBUG 15 | 16 | sys.path.append(os.path.join(PROJECT_ROOT, '../../')) 17 | 18 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'site_media') 19 | TEMPLATE_DIRS = [os.path.join(PROJECT_ROOT, 'templates')] 20 | ADMIN_MEDIA_PREFIX = '/media/' 21 | ROOT_URLCONF = 'blogprj.urls' 22 | TIME_ZONE = 'Europe/Berlin' 23 | LANGUAGE_CODE = 'de-de' 24 | 25 | TEMPLATE_LOADERS = ( 26 | 'django.template.loaders.app_directories.Loader', 27 | 'django.template.loaders.filesystem.Loader', 28 | ) 29 | 30 | INSTALLED_APPS = ( 31 | 'apps.blog', 32 | ) 33 | 34 | MIDDLEWARE_CLASSES = ( 35 | 'django.middleware.common.CommonMiddleware', 36 | 'django.contrib.sessions.middleware.SessionMiddleware', 37 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 38 | 'django.contrib.messages.middleware.MessageMiddleware', 39 | ) 40 | 41 | TEMPLATE_CONTEXT_PROCESSORS = ( 42 | "django.core.context_processors.request", 43 | "django.core.context_processors.auth", 44 | "django.core.context_processors.media", 45 | "django.core.context_processors.debug", 46 | ) 47 | 48 | try: 49 | SECRET_KEY 50 | except NameError: 51 | SECRET_FILE = os.path.join(PROJECT_ROOT, 'secret.txt') 52 | try: 53 | SECRET_KEY = open(SECRET_FILE).read().strip() 54 | except IOError: 55 | try: 56 | from random import choice 57 | SECRET_KEY = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)]) 58 | secret = file(SECRET_FILE, 'w') 59 | secret.write(SECRET_KEY) 60 | secret.close() 61 | except IOError: 62 | Exception('Please create a %s file with random characters to generate your secret key!' % SECRET_FILE) 63 | 64 | -------------------------------------------------------------------------------- /examples/blogprj/templates/base.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | My little blog 6 | 7 | 31 | 32 | {% block header %}{% endblock %} 33 | 41 | 42 | 43 | {% block body %}Content goes here{% endblock %} 44 | 45 | -------------------------------------------------------------------------------- /examples/blogprj/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('', 4 | (r'^', include('apps.blog.urls')), 5 | ) -------------------------------------------------------------------------------- /mongotools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpjunior/django-mongotools/06bfe622e766729b48d42d4865e3fb9141e433d1/mongotools/__init__.py -------------------------------------------------------------------------------- /mongotools/forms/__init__.py: -------------------------------------------------------------------------------- 1 | import types 2 | from django import forms 3 | from django.core.files.uploadedfile import UploadedFile 4 | from django.utils.datastructures import SortedDict 5 | from django.forms.widgets import media_property 6 | 7 | from mongoengine.base import BaseDocument 8 | from mongotools.forms.fields import MongoFormFieldGenerator 9 | from mongotools.forms.utils import mongoengine_validate_wrapper, iter_valid_fields, save_file 10 | from mongoengine.fields import ReferenceField, FileField, ListField 11 | 12 | __all__ = ('MongoForm',) 13 | 14 | 15 | class MongoFormMetaClass(type): 16 | 17 | """Metaclass to create a new MongoForm.""" 18 | 19 | def __new__(cls, name, bases, attrs): 20 | # get all valid existing Fields and sort them 21 | fields = [(field_name, attrs.pop(field_name)) for field_name, obj in 22 | attrs.items() if isinstance(obj, forms.Field)] 23 | fields.sort( 24 | lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 25 | 26 | # get all Fields from base classes 27 | for base in bases[::-1]: 28 | if hasattr(base, 'base_fields'): 29 | fields = base.base_fields.items() + fields 30 | 31 | # add the fields as "our" base fields 32 | attrs['base_fields'] = SortedDict(fields) 33 | 34 | # Meta class available? 35 | if 'Meta' in attrs and hasattr(attrs['Meta'], 'document') and \ 36 | issubclass(attrs['Meta'].document, BaseDocument): 37 | doc_fields = SortedDict() 38 | 39 | formfield_generator = getattr(attrs['Meta'], 'formfield_generator', 40 | MongoFormFieldGenerator)() 41 | 42 | widgets = getattr(attrs["Meta"], "widgets", {}) 43 | 44 | # walk through the document fields 45 | for field_name, field in iter_valid_fields(attrs['Meta']): 46 | # add field and override clean method to respect 47 | # mongoengine-validator 48 | 49 | # use to get a custom widget 50 | if hasattr(field, 'get_custom_widget'): 51 | widget = field.get_custom_widget() 52 | else: 53 | widget = widgets.get(field_name, None) 54 | 55 | if widget: 56 | doc_fields[field_name] = formfield_generator.generate( 57 | field, widget=widget) 58 | else: 59 | doc_fields[field_name] = formfield_generator.generate( 60 | field) 61 | 62 | if not isinstance(field, FileField): 63 | doc_fields[field_name].clean = mongoengine_validate_wrapper( 64 | field, 65 | doc_fields[field_name].clean, field._validate) 66 | 67 | # write the new document fields to base_fields 68 | doc_fields.update(attrs['base_fields']) 69 | attrs['base_fields'] = doc_fields 70 | 71 | # maybe we need the Meta class later 72 | attrs['_meta'] = attrs.get('Meta', object()) 73 | 74 | new_class = super(MongoFormMetaClass, cls).__new__( 75 | cls, name, bases, attrs) 76 | 77 | if 'media' not in attrs: 78 | new_class.media = media_property(new_class) 79 | 80 | return new_class 81 | 82 | 83 | class MongoForm(forms.BaseForm): 84 | 85 | """Base MongoForm class. Used to create new MongoForms""" 86 | __metaclass__ = MongoFormMetaClass 87 | 88 | def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, 89 | error_class=forms.util.ErrorList, label_suffix=':', 90 | empty_permitted=False, instance=None): 91 | """ initialize the form""" 92 | 93 | assert isinstance(instance, (types.NoneType, BaseDocument)), \ 94 | 'instance must be a mongoengine document, not %s' % \ 95 | type(instance).__name__ 96 | 97 | assert hasattr(self, 'Meta'), 'Meta class is needed to use MongoForm' 98 | # new instance or updating an existing one? 99 | if instance is None: 100 | if self._meta.document is None: 101 | raise ValueError('MongoForm has no document class specified.') 102 | self.instance = self._meta.document() 103 | object_data = {} 104 | self.instance._adding = True 105 | else: 106 | self.instance = instance 107 | self.instance._adding = False 108 | object_data = {} 109 | 110 | # walk through the document fields 111 | for field_name, field in iter_valid_fields(self._meta): 112 | # add field data if needed 113 | field_data = getattr(instance, field_name) 114 | if isinstance(self._meta.document._fields[field_name], ReferenceField): 115 | # field data could be None for not populated refs 116 | field_data = field_data and str(field_data.id) 117 | object_data[field_name] = field_data 118 | 119 | # additional initial data available? 120 | if initial is not None: 121 | object_data.update(initial) 122 | 123 | self._validate_unique = False 124 | super(MongoForm, self).__init__(data, files, auto_id, prefix, object_data, 125 | error_class, label_suffix, empty_permitted) 126 | 127 | def save(self, commit=True): 128 | """save the instance or create a new one..""" 129 | # walk through the document fields 130 | for field_name, field in iter_valid_fields(self._meta): 131 | # FileFields need some more work to ensure the filename is unique 132 | if isinstance(self.instance._fields[field_name], FileField): 133 | io = self.cleaned_data.get(field_name) 134 | 135 | if isinstance(io, UploadedFile): 136 | field = save_file(self.instance, field_name, io) 137 | setattr(self.instance, field_name, field) 138 | 139 | continue 140 | 141 | setattr( 142 | self.instance, field_name, self.cleaned_data.get(field_name)) 143 | 144 | if commit: 145 | self.instance.save() 146 | 147 | return self.instance 148 | -------------------------------------------------------------------------------- /mongotools/forms/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.encoding import smart_unicode 3 | from pymongo.errors import InvalidId 4 | from bson import ObjectId 5 | from django.core.validators import EMPTY_VALUES 6 | from django.utils.encoding import smart_unicode, force_unicode 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from mongoengine import ReferenceField as MongoReferenceField 10 | 11 | from mongoengine.fields import ( 12 | IntField, SequenceField) 13 | 14 | BLANK_CHOICE_DASH = [("", "---------")] 15 | 16 | class MongoChoiceIterator(object): 17 | def __init__(self, field): 18 | self.field = field 19 | self.queryset = field.queryset 20 | 21 | def __iter__(self): 22 | if self.field.empty_label is not None: 23 | yield (u"", self.field.empty_label) 24 | 25 | for obj in self.queryset.all(): 26 | yield self.choice(obj) 27 | 28 | def __len__(self): 29 | return len(self.queryset) 30 | 31 | def choice(self, obj): 32 | return (self.field.prepare_value(obj), self.field.label_from_instance(obj)) 33 | 34 | 35 | class MongoCharField(forms.CharField): 36 | def to_python(self, value): 37 | if value in EMPTY_VALUES: 38 | return None 39 | return smart_unicode(value) 40 | 41 | class ReferenceField(forms.TypedChoiceField): 42 | """ 43 | Reference field for mongo forms. Inspired by `django.forms.models.ModelChoiceField`. 44 | """ 45 | def __init__(self, queryset, empty_label=u"---------", 46 | *aargs, **kwaargs): 47 | 48 | super(ReferenceField, self).__init__(*aargs, **kwaargs) 49 | self.queryset = queryset 50 | self.empty_label = empty_label 51 | 52 | def _get_queryset(self): 53 | return self._queryset 54 | 55 | def prepare_value(self, value): 56 | if hasattr(value, '_meta'): 57 | return value.pk 58 | 59 | return super(ReferenceField, self).prepare_value(value) 60 | 61 | def __deepcopy__(self, memo): 62 | result = super(forms.ChoiceField, self).__deepcopy__(memo) 63 | result.queryset = result.queryset 64 | result.empty_label = result.empty_label 65 | return result 66 | 67 | 68 | def _set_queryset(self, queryset): 69 | self._queryset = queryset 70 | self.widget.choices = self.choices 71 | 72 | queryset = property(_get_queryset, _set_queryset) 73 | 74 | def _get_choices(self): 75 | return MongoChoiceIterator(self) 76 | 77 | choices = property(_get_choices, forms.ChoiceField._set_choices) 78 | 79 | def label_from_instance(self, obj): 80 | """ 81 | This method is used to convert objects into strings; it's used to 82 | generate the labels for the choices presented by this object. Subclasses 83 | can override this method to customize the display of the choices. 84 | """ 85 | return smart_unicode(obj) 86 | 87 | def clean(self, oid): 88 | if oid in EMPTY_VALUES and not self.required: 89 | return None 90 | 91 | try: 92 | if self.coerce != int: 93 | oid = ObjectId(oid) 94 | 95 | oid = super(ReferenceField, self).clean(oid) 96 | 97 | queryset = self.queryset.clone() 98 | obj = queryset.get(pk=oid) 99 | except (TypeError, InvalidId, self.queryset._document.DoesNotExist): 100 | raise forms.ValidationError(self.error_messages['invalid_choice'] % {'value': oid}) 101 | return obj 102 | 103 | class DocumentMultipleChoiceField(ReferenceField): 104 | """A MultipleChoiceField whose choices are a model QuerySet.""" 105 | widget = forms.SelectMultiple 106 | hidden_widget = forms.MultipleHiddenInput 107 | default_error_messages = { 108 | 'list': _(u'Enter a list of values.'), 109 | 'invalid_choice': _(u'Select a valid choice. %s is not one of the' 110 | u' available choices.'), 111 | 'invalid_pk_value': _(u'"%s" is not a valid value for a primary key.') 112 | } 113 | 114 | def __init__(self, queryset, *args, **kwargs): 115 | super(DocumentMultipleChoiceField, self).__init__(queryset, empty_label=None, *args, **kwargs) 116 | 117 | def clean(self, value): 118 | if self.required and not value: 119 | raise forms.ValidationError(self.error_messages['required']) 120 | elif not self.required and not value: 121 | return [] 122 | if not isinstance(value, (list, tuple)): 123 | raise forms.ValidationError(self.error_messages['list']) 124 | key = 'pk' 125 | 126 | filter_ids = [] 127 | for pk in value: 128 | try: 129 | oid = ObjectId(pk) 130 | filter_ids.append(oid) 131 | except InvalidId: 132 | raise forms.ValidationError(self.error_messages['invalid_pk_value'] % pk) 133 | qs = self.queryset.clone() 134 | qs = qs.filter(**{'%s__in' % key: filter_ids}) 135 | pks = set([force_unicode(getattr(o, key)) for o in qs]) 136 | for val in value: 137 | if force_unicode(val) not in pks: 138 | raise forms.ValidationError(self.error_messages['invalid_choice'] % val) 139 | # Since this overrides the inherited ModelChoiceField.clean 140 | # we run custom validators here 141 | self.run_validators(value) 142 | return list(qs) 143 | 144 | def prepare_value(self, value): 145 | if hasattr(value, '__iter__') and not hasattr(value, '_meta'): 146 | return [super(DocumentMultipleChoiceField, self).prepare_value(v) for v in value] 147 | return super(DocumentMultipleChoiceField, self).prepare_value(value) 148 | 149 | 150 | class MongoFormFieldGenerator(object): 151 | """This is singleton class generates Django form-fields for mongoengine-fields.""" 152 | 153 | _instance = None 154 | def __new__(cls, *args, **kwargs): 155 | if not cls._instance: 156 | cls._instance = super(MongoFormFieldGenerator, cls).__new__( 157 | cls, *args, **kwargs) 158 | return cls._instance 159 | 160 | def generate(self, field, **kwargs): 161 | """Tries to lookup a matching formfield generator (lowercase 162 | field-classname) and raises a NotImplementedError of no generator 163 | can be found. 164 | """ 165 | if hasattr(self, 'generate_%s' % field.__class__.__name__.lower()): 166 | return getattr(self, 'generate_%s' % \ 167 | field.__class__.__name__.lower())(field, **kwargs) 168 | else: 169 | for cls in field.__class__.__bases__: 170 | if hasattr(self, 'generate_%s' % cls.__name__.lower()): 171 | return getattr(self, 'generate_%s' % \ 172 | cls.__name__.lower())(field, **kwargs) 173 | 174 | raise NotImplementedError('%s is not supported by MongoForm' % \ 175 | field.__class__.__name__) 176 | 177 | def get_field_choices(self, field, include_blank=True, 178 | blank_choice=BLANK_CHOICE_DASH): 179 | first_choice = include_blank and blank_choice or [] 180 | return first_choice + list(field.choices) 181 | 182 | def string_field(self, value): 183 | if value in EMPTY_VALUES: 184 | return None 185 | return smart_unicode(value) 186 | 187 | def integer_field(self, value): 188 | if value in EMPTY_VALUES: 189 | return None 190 | return int(value) 191 | 192 | def boolean_field(self, value): 193 | if value in EMPTY_VALUES: 194 | return None 195 | return value.lower() == 'true' 196 | 197 | def get_field_label(self, field): 198 | if field.verbose_name: 199 | return field.verbose_name 200 | return field.name.capitalize() 201 | 202 | def get_field_help_text(self, field): 203 | if field.help_text: 204 | return field.help_text.capitalize() 205 | 206 | def generate_stringfield(self, field, **kwargs): 207 | form_class = MongoCharField 208 | 209 | defaults = {'label': self.get_field_label(field), 210 | 'initial': field.default, 211 | 'required': field.required, 212 | 'help_text': self.get_field_help_text(field)} 213 | 214 | if field.max_length and not field.choices: 215 | defaults['max_length'] = field.max_length 216 | 217 | if field.max_length is None and not field.choices: 218 | defaults['widget'] = forms.Textarea 219 | 220 | if field.regex: 221 | defaults['regex'] = field.regex 222 | elif field.choices: 223 | form_class = forms.TypedChoiceField 224 | defaults['choices'] = self.get_field_choices(field) 225 | defaults['coerce'] = self.string_field 226 | 227 | if not field.required: 228 | defaults['empty_value'] = None 229 | 230 | defaults.update(kwargs) 231 | return form_class(**defaults) 232 | 233 | def generate_emailfield(self, field, **kwargs): 234 | defaults = { 235 | 'required': field.required, 236 | 'min_length': field.min_length, 237 | 'max_length': field.max_length, 238 | 'initial': field.default, 239 | 'label': self.get_field_label(field), 240 | 'help_text': self.get_field_help_text(field) 241 | } 242 | 243 | defaults.update(kwargs) 244 | return forms.EmailField(**defaults) 245 | 246 | def generate_urlfield(self, field, **kwargs): 247 | defaults = { 248 | 'required': field.required, 249 | 'min_length': field.min_length, 250 | 'max_length': field.max_length, 251 | 'initial': field.default, 252 | 'label': self.get_field_label(field), 253 | 'help_text': self.get_field_help_text(field) 254 | } 255 | 256 | defaults.update(kwargs) 257 | return forms.URLField(**defaults) 258 | 259 | def generate_intfield(self, field, **kwargs): 260 | if field.choices: 261 | defaults = { 262 | 'coerce': self.integer_field, 263 | 'empty_value': None, 264 | 'required': field.required, 265 | 'initial': field.default, 266 | 'label': self.get_field_label(field), 267 | 'choices': self.get_field_choices(field), 268 | 'help_text': self.get_field_help_text(field) 269 | } 270 | 271 | defaults.update(kwargs) 272 | return forms.TypedChoiceField(**defaults) 273 | else: 274 | defaults = { 275 | 'required': field.required, 276 | 'min_value': field.min_value, 277 | 'max_value': field.max_value, 278 | 'initial': field.default, 279 | 'label': self.get_field_label(field), 280 | 'help_text': self.get_field_help_text(field) 281 | } 282 | 283 | defaults.update(kwargs) 284 | return forms.IntegerField(**defaults) 285 | 286 | def generate_floatfield(self, field, **kwargs): 287 | 288 | form_class = forms.FloatField 289 | 290 | defaults = {'label': self.get_field_label(field), 291 | 'initial': field.default, 292 | 'required': field.required, 293 | 'min_value': field.min_value, 294 | 'max_value': field.max_value, 295 | 'help_text': self.get_field_help_text(field)} 296 | 297 | defaults.update(kwargs) 298 | return form_class(**defaults) 299 | 300 | def generate_decimalfield(self, field, **kwargs): 301 | form_class = forms.DecimalField 302 | defaults = {'label': self.get_field_label(field), 303 | 'initial': field.default, 304 | 'required': field.required, 305 | 'min_value': field.min_value, 306 | 'max_value': field.max_value, 307 | 'help_text': self.get_field_help_text(field)} 308 | 309 | defaults.update(kwargs) 310 | return form_class(**defaults) 311 | 312 | def generate_booleanfield(self, field, **kwargs): 313 | if field.choices: 314 | defaults = { 315 | 'coerce': self.boolean_field, 316 | 'empty_value': None, 317 | 'required': field.required, 318 | 'initial': field.default, 319 | 'label': self.get_field_label(field), 320 | 'choices': self.get_field_choices(field), 321 | 'help_text': self.get_field_help_text(field) 322 | } 323 | 324 | defaults.update(kwargs) 325 | return forms.TypedChoiceField(**defaults) 326 | else: 327 | defaults = { 328 | 'required': field.required, 329 | 'initial': field.default, 330 | 'label': self.get_field_label(field), 331 | 'help_text': self.get_field_help_text(field) 332 | } 333 | 334 | defaults.update(kwargs) 335 | return forms.BooleanField(**defaults) 336 | 337 | def generate_datetimefield(self, field, **kwargs): 338 | defaults = { 339 | 'required': field.required, 340 | 'initial': field.default, 341 | 'label': self.get_field_label(field), 342 | } 343 | 344 | defaults.update(kwargs) 345 | return forms.DateTimeField(**defaults) 346 | 347 | def generate_referencefield(self, field, **kwargs): 348 | defaults = { 349 | 'label': self.get_field_label(field), 350 | 'help_text': self.get_field_help_text(field), 351 | 'required': field.required 352 | } 353 | 354 | defaults.update(kwargs) 355 | 356 | id_field_name = field.document_type._meta['id_field'] 357 | id_field = field.document_type._fields[id_field_name] 358 | 359 | if isinstance(id_field, (SequenceField, IntField)): 360 | defaults['coerce'] = int 361 | 362 | return ReferenceField(field.document_type.objects, **defaults) 363 | 364 | def generate_listfield(self, field, **kwargs): 365 | if field.field.choices: 366 | defaults = { 367 | 'choices': field.field.choices, 368 | 'required': field.required, 369 | 'label': self.get_field_label(field), 370 | 'help_text': self.get_field_help_text(field), 371 | 'widget': forms.CheckboxSelectMultiple 372 | } 373 | 374 | defaults.update(kwargs) 375 | return forms.MultipleChoiceField(**defaults) 376 | elif isinstance(field.field, MongoReferenceField): 377 | defaults = { 378 | 'label': self.get_field_label(field), 379 | 'help_text': self.get_field_help_text(field), 380 | 'required': field.required 381 | } 382 | 383 | defaults.update(kwargs) 384 | f = DocumentMultipleChoiceField(field.field.document_type.objects, **defaults) 385 | return f 386 | 387 | def generate_filefield(self, field, **kwargs): 388 | defaults = { 389 | 'required': field.required, 390 | 'label': self.get_field_label(field), 391 | 'help_text': self.get_field_help_text(field), 392 | } 393 | defaults.update(kwargs) 394 | return forms.FileField(**defaults) 395 | -------------------------------------------------------------------------------- /mongotools/forms/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import itertools 3 | import gridfs 4 | 5 | from django import forms 6 | from mongoengine.base import ValidationError 7 | from mongoengine.fields import EmbeddedDocumentField, ListField, ReferenceField 8 | from mongoengine.connection import _get_db 9 | 10 | from fields import MongoFormFieldGenerator 11 | 12 | def generate_field(field): 13 | generator = MongoFormFieldGenerator() 14 | return generator.generate(field) 15 | 16 | def mongoengine_validate_wrapper(field, old_clean, new_clean): 17 | """ 18 | A wrapper function to validate formdata against mongoengine-field 19 | validator and raise a proper django.forms ValidationError if there 20 | are any problems. 21 | """ 22 | def inner_validate(value, *args, **kwargs): 23 | value = old_clean(value, *args, **kwargs) 24 | 25 | if value is None and field.required: 26 | raise ValidationError("This field is required") 27 | 28 | elif value is None: 29 | return value 30 | try: 31 | new_clean(value) 32 | return value 33 | except ValidationError, e: 34 | raise forms.ValidationError(e) 35 | return inner_validate 36 | 37 | def iter_valid_fields(meta): 38 | """walk through the available valid fields..""" 39 | 40 | # fetch field configuration and always add the id_field as exclude 41 | meta_fields = getattr(meta, 'fields', ()) 42 | meta_exclude = getattr(meta, 'exclude', ()) 43 | 44 | if hasattr(meta.document, '_meta'): 45 | meta_exclude += (meta.document._meta.get('id_field'),) 46 | # walk through the document fields 47 | 48 | for field_name, field in sorted(meta.document._fields.items(), key=lambda t: t[1].creation_counter): 49 | # skip excluded or not explicit included fields 50 | if (meta_fields and field_name not in meta_fields) or field_name in meta_exclude: 51 | continue 52 | 53 | if isinstance(field, EmbeddedDocumentField): #skip EmbeddedDocumentField 54 | continue 55 | 56 | if isinstance(field, ListField): 57 | if hasattr(field.field, 'choices') and not isinstance(field.field, ReferenceField): 58 | if not field.field.choices: 59 | continue 60 | elif not isinstance(field.field, ReferenceField): 61 | continue 62 | 63 | yield (field_name, field) 64 | 65 | def _get_unique_filename(name): 66 | fs = gridfs.GridFS(_get_db()) 67 | file_root, file_ext = os.path.splitext(name) 68 | count = itertools.count(1) 69 | while fs.exists(filename=name): 70 | # file_ext includes the dot. 71 | name = os.path.join("%s_%s%s" % (file_root, count.next(), file_ext)) 72 | return name 73 | 74 | def save_file(instance, field_name, file): 75 | field = getattr(instance, field_name) 76 | 77 | filename = _get_unique_filename(file.name) 78 | file.file.seek(0) 79 | 80 | field.replace(file, content_type=file.content_type, filename=filename) 81 | return field 82 | -------------------------------------------------------------------------------- /mongotools/views/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2011 Wilson Pinto Júnior 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | 20 | # This file is based in Django Class Views 21 | # adapted for use of mongoengine 22 | 23 | from django.views.generic.detail import BaseDetailView 24 | from django.views.generic.edit import FormMixin, ProcessFormView, DeletionMixin 25 | from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist 26 | from django.views.generic.base import TemplateResponseMixin, View 27 | from django.http import HttpResponseRedirect, Http404 28 | from django.views.generic.list import MultipleObjectMixin 29 | from django.shortcuts import render 30 | from django.contrib import messages 31 | 32 | class MongoSingleObjectMixin(object): 33 | """ 34 | Provides the ability to retrieve a single object for further manipulation. 35 | """ 36 | document = None 37 | queryset = None 38 | context_object_name = None 39 | 40 | def get_object(self, queryset=None): 41 | """ 42 | Returns the object the view is displaying. 43 | 44 | By default this requires `self.queryset` and a `pk` or `slug` argument 45 | in the URLconf, but subclasses can override this to return any object. 46 | """ 47 | if queryset is None: 48 | queryset = self.get_queryset() 49 | 50 | pk = self.kwargs.get('pk', None) 51 | if pk is not None: 52 | queryset = queryset.filter(pk=pk) 53 | 54 | else: 55 | raise AttributeError(u"Generic detail view %s must be" 56 | u" called with object pk." 57 | % self.__class__.__name__) 58 | 59 | try: 60 | obj = queryset.get() 61 | except queryset._document.DoesNotExist: 62 | raise Http404(u"No %(verbose_name)s found matching the query" % 63 | {'verbose_name': queryset._document.__name__}) 64 | return obj 65 | 66 | def get_queryset(self): 67 | """ 68 | Get the queryset to look an object up against. May not be called if 69 | `get_object` is overridden. 70 | """ 71 | if self.queryset is None: 72 | if self.document: 73 | return self.document.objects 74 | else: 75 | raise ImproperlyConfigured(u"%(cls)s is missing a queryset. Define " 76 | u"%(cls)s.document, %(cls)s.queryset, or override " 77 | u"%(cls)s.get_object()." % { 78 | 'cls': self.__class__.__name__ 79 | }) 80 | return self.queryset.clone() 81 | 82 | def get_context_data(self, **kwargs): 83 | return kwargs 84 | 85 | class MongoMultipleObjectMixin(MultipleObjectMixin): 86 | 87 | document = None 88 | 89 | def get_queryset(self): 90 | """ 91 | Get the list of items for this view. This must be an interable, and may 92 | be a queryset (in which qs-specific behavior will be enabled). 93 | """ 94 | if self.queryset is not None: 95 | queryset = self.queryset 96 | if hasattr(queryset, 'clone'): 97 | queryset = queryset.clone() 98 | elif self.document is not None: 99 | queryset = self.document.objects 100 | else: 101 | raise ImproperlyConfigured(u"'%s' must define 'queryset' or 'document'" 102 | % self.__class__.__name__) 103 | return queryset 104 | 105 | class MongoSingleObjectTemplateResponseMixin(TemplateResponseMixin): 106 | template_name_field = None 107 | template_name_suffix = '_detail' 108 | 109 | def get_template_names(self): 110 | """ 111 | Return a list of template names to be used for the request. Must return 112 | a list. May not be called if get_template is overridden. 113 | """ 114 | 115 | try: 116 | names = super(MongoSingleObjectTemplateResponseMixin, 117 | self).get_template_names() 118 | except ImproperlyConfigured: 119 | # If template_name isn't specified, it's not a problem -- 120 | # we just start with an empty list. 121 | names = [] 122 | 123 | # If self.template_name_field is set, grab the value of the field 124 | # of that name from the object; this is the most specific template 125 | # name, if given. 126 | if self.object and self.template_name_field: 127 | name = getattr(self.object, self.template_name_field, None) 128 | if name: 129 | names.insert(0, name) 130 | 131 | if hasattr(self.object, '_meta'): 132 | names.append("%s/%s.html" % ( 133 | self.object.__class__.__name__.lower(), 134 | self.template_name_suffix 135 | )) 136 | elif hasattr(self, 'document') and hasattr(self.document, '_meta'): 137 | names.append("%s/%s.html" % ( 138 | self.document.__name__.lower(), 139 | self.template_name_suffix 140 | )) 141 | 142 | return names 143 | 144 | class MongoFormMixin(FormMixin, MongoSingleObjectMixin): 145 | """ 146 | A mixin that provides a way to show and handle a mongo in a request. 147 | """ 148 | success_message = None 149 | historic_action = None 150 | save_permission = None 151 | 152 | def get_form_class(self): 153 | """ 154 | Returns the form class to use in this view 155 | """ 156 | if not self.form_class: 157 | raise NotImplemented(u"Please specify the form_class" 158 | u" argument or get_form_class method") 159 | return self.form_class 160 | 161 | def get_form_kwargs(self): 162 | """ 163 | Returns the keyword arguments for instanciating the form. 164 | """ 165 | kwargs = super(MongoFormMixin, self).get_form_kwargs() 166 | kwargs.update({'instance': self.object}) 167 | return kwargs 168 | 169 | def get_success_url(self): 170 | if self.success_url: 171 | url = self.success_url % self.object.__dict__ 172 | else: 173 | try: 174 | url = self.object.get_absolute_url() 175 | except AttributeError: 176 | raise ImproperlyConfigured( 177 | "No URL to redirect to. Either provide a url or define" 178 | " a get_absolute_url method on the Model.") 179 | return url 180 | 181 | def send_messages(self): 182 | if self.success_message: 183 | messages.success(self.request, 184 | self.success_message % self.object) 185 | 186 | def write_historic(self): 187 | if self.historic_action: 188 | self.request.user.register_historic(self.object, 189 | self.historic_action) 190 | 191 | def form_valid(self, form): 192 | if self.save_permission: 193 | if not self.request.user.has_perm(self.save_permission): 194 | return render(self.request, 'access_denied.html', locals()) 195 | self.object = form.save() 196 | 197 | self.write_historic() 198 | self.send_messages() 199 | 200 | return super(MongoFormMixin, self).form_valid(form) 201 | 202 | def get_context_data(self, **kwargs): 203 | context = kwargs 204 | if self.object: 205 | context['object'] = self.object 206 | return context 207 | 208 | class BaseDetailView(MongoSingleObjectMixin, View): 209 | historic_view_action = None 210 | def get(self, request, **kwargs): 211 | self.object = self.get_object() 212 | context = self.get_context_data(object=self.object) 213 | 214 | if self.historic_view_action: 215 | self.request.user.register_historic( 216 | self.object, 217 | self.historic_view_action) 218 | 219 | return self.render_to_response(context) 220 | 221 | class BaseCreateView(MongoFormMixin, ProcessFormView): 222 | """ 223 | Base view for creating an new object instance. 224 | 225 | Using this base class requires subclassing to provide a response mixin. 226 | """ 227 | def get(self, request, *args, **kwargs): 228 | self.object = None 229 | return super(BaseCreateView, self).get(request, *args, **kwargs) 230 | 231 | def post(self, request, *args, **kwargs): 232 | self.object = None 233 | return super(BaseCreateView, self).post(request, *args, **kwargs) 234 | 235 | 236 | class CreateView(MongoSingleObjectTemplateResponseMixin, BaseCreateView): 237 | """ 238 | View for creating an new object instance, 239 | with a response rendered by template. 240 | """ 241 | template_name_suffix = 'form' 242 | 243 | class BaseUpdateView(MongoFormMixin, ProcessFormView): 244 | """ 245 | Base view for updating an existing object. 246 | 247 | Using this base class requires subclassing to provide a response mixin. 248 | """ 249 | def get(self, request, *args, **kwargs): 250 | self.object = self.get_object() 251 | return super(BaseUpdateView, self).get(request, *args, **kwargs) 252 | 253 | def post(self, request, *args, **kwargs): 254 | self.object = self.get_object() 255 | return super(BaseUpdateView, self).post(request, *args, **kwargs) 256 | 257 | 258 | class UpdateView(MongoSingleObjectTemplateResponseMixin, BaseUpdateView): 259 | """ 260 | View for updating an object, 261 | with a response rendered by template.. 262 | """ 263 | template_name_suffix = 'form' 264 | 265 | 266 | class DeletionMixin(object): 267 | """ 268 | A mixin providing the ability to delete objects 269 | """ 270 | success_url = None 271 | success_message = None 272 | historic_action = None 273 | 274 | def delete(self, request, *args, **kwargs): 275 | self.object = self.get_object() 276 | msg = None 277 | 278 | if self.success_message: 279 | msg = self.success_message % self.object 280 | 281 | if self.historic_action: 282 | self.request.user.register_historic(self.object, 283 | self.historic_action) 284 | 285 | self.object.delete() 286 | return HttpResponseRedirect(self.get_success_url()) 287 | 288 | # Add support for browsers which only accept GET and POST for now. 289 | def post(self, *args, **kwargs): 290 | return self.delete(*args, **kwargs) 291 | 292 | def get_success_url(self): 293 | if self.success_url: 294 | return self.success_url 295 | else: 296 | raise ImproperlyConfigured( 297 | "No URL to redirect to. Provide a success_url.") 298 | 299 | 300 | class BaseDeleteView(DeletionMixin, BaseDetailView): 301 | """ 302 | Base view for deleting an object. 303 | Using this base class requires subclassing to provide a response mixin. 304 | """ 305 | 306 | class DeleteView(MongoSingleObjectTemplateResponseMixin, BaseDeleteView): 307 | """ 308 | View for deleting an object retrieved with `self.get_object()`, 309 | with a response rendered by template. 310 | """ 311 | template_name_suffix = 'confirm_delete' 312 | 313 | class BaseListView(MongoMultipleObjectMixin, View): 314 | def get(self, request, *args, **kwargs): 315 | self.object_list = self.get_queryset() 316 | allow_empty = self.get_allow_empty() 317 | if not allow_empty and len(self.object_list) == 0: 318 | raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.") 319 | % {'class_name': self.__class__.__name__}) 320 | context = self.get_context_data(object_list=self.object_list) 321 | return self.render_to_response(context) 322 | 323 | class MongoMultipleObjectTemplateResponseMixin(TemplateResponseMixin): 324 | template_name_suffix = 'list' 325 | 326 | def get_template_names(self): 327 | """ 328 | Return a list of template names to be used for the request. Must return 329 | a list. May not be called if get_template is overridden. 330 | """ 331 | try: 332 | names = TemplateResponseMixin.get_template_names(self) 333 | except ImproperlyConfigured: 334 | names = [] 335 | 336 | if hasattr(self.object_list, '_document'): 337 | object_name = self.object_list._document.__name__ 338 | names.append("%s/%s.html" % (object_name.lower(), self.template_name_suffix)) 339 | 340 | return names 341 | 342 | class DetailView(MongoSingleObjectTemplateResponseMixin, BaseDetailView): 343 | template_name_suffix = 'detail' 344 | def get_context_data(self, **kwargs): 345 | return kwargs 346 | 347 | class ListView(MongoMultipleObjectTemplateResponseMixin, BaseListView): 348 | """ 349 | Render some list of objects, set by `self.model` or `self.queryset`. 350 | `self.queryset` can actually be any iterable of items, not just a queryset. 351 | """ 352 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | 6 | setup( 7 | name='django-mongotools', 8 | version='0.1dev', 9 | description='ClassViews, Form mongoengine support for django', 10 | author='Wilson Pinto Júnior', 11 | author_email='wilsonpjunior@gmail.com', 12 | url='http://github.com/wpjunior/django-mongotools/', 13 | packages=find_packages(exclude=['examples', 'examples.*']), 14 | classifiers=[ 15 | 'Development Status :: 3 - Alpha', 16 | 'Environment :: Web Environment', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: GPL License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Framework :: Django', 22 | ], 23 | zip_safe=False, 24 | tests_require=['django==1.3', 'mongoengine'] 25 | ) 26 | --------------------------------------------------------------------------------