Skip to content

Commit 1209a9a

Browse files
committed
Merge branch 'master' of git://github.com/django-mongodb-engine/mongodb-engine
2 parents b1ee072 + 23b9bcc commit 1209a9a

File tree

13 files changed

+206
-101
lines changed

13 files changed

+206
-101
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Past Primary Authors
1313

1414
Contributions by
1515
----------------
16+
* Ellie Frost (https://github.com/stillinbeta)
1617
* Dag Stockstad (https://github.com/dstockstad) -- found tons of bugs in our query generator
1718
* Shane R. Spencer (https://github.com/whardier) -- Website Review
1819
* Sabin Iacob (https://github.com/m0n5t3r)

django_mongodb_engine/compiler.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ def execute_sql(self, result_type=MULTI):
403403
ret.append(result)
404404
return ret
405405

406+
406407
class SQLInsertCompiler(NonrelInsertCompiler, SQLCompiler):
407408
@safe_call
408409
def insert(self, data, return_id=False):
@@ -412,39 +413,21 @@ def insert(self, data, return_id=False):
412413
pass
413414
return self._save(data, return_id)
414415

416+
415417
# TODO: Define a common nonrel API for updates and add it to the nonrel
416418
# backend base classes and port this code to that API
417419
class SQLUpdateCompiler(NonrelUpdateCompiler, SQLCompiler):
418420
query_class = MongoQuery
419421

420-
@safe_call
421-
def execute_raw(self, update_spec, multi=True, **kwargs):
422-
collection = self.get_collection()
423-
criteria = self.build_query()._mongo_query
424-
options = self.connection.operation_flags.get('update', {})
425-
options = dict(options, **kwargs)
426-
info = collection.update(criteria, update_spec, multi=multi, **options)
427-
if info is not None:
428-
return info.get('n')
429-
430-
def execute_sql(self, result_type):
431-
return self.execute_raw(*self._get_update_spec())
432-
433-
def _get_update_spec(self):
422+
def update(self, values):
434423
multi = True
435424
spec = {}
436-
for field, _, value in self.query.values:
425+
for field, value in values:
437426
if getattr(field, 'forbids_updates', False):
438427
raise DatabaseError("Updates on %ss are not allowed" %
439428
field.__class__.__name__)
440429
if field.unique:
441430
multi = False
442-
if hasattr(value, 'prepare_database_save'):
443-
value = value.prepare_database_save(field)
444-
else:
445-
value = field.get_db_prep_save(value, connection=self.connection)
446-
447-
value = self.convert_value_for_db(field.db_type(connection=self.connection), value)
448431
if hasattr(value, "evaluate"):
449432
assert value.connector in (value.ADD, value.SUB)
450433
assert not value.negated
@@ -465,7 +448,18 @@ def _get_update_spec(self):
465448
raise DatabaseError("Can not modify _id")
466449
spec.setdefault(action, {})[column] = value
467450

468-
return spec, multi
451+
return self.execute_update(spec, multi)
452+
453+
@safe_call
454+
def execute_update(self, update_spec, multi=True, **kwargs):
455+
collection = self.get_collection()
456+
criteria = self.build_query()._mongo_query
457+
options = self.connection.operation_flags.get('update', {})
458+
options = dict(options, **kwargs)
459+
info = collection.update(criteria, update_spec, multi=multi, **options)
460+
if info is not None:
461+
return info.get('n')
462+
469463

470464
class SQLDeleteCompiler(NonrelDeleteCompiler, SQLCompiler):
471465
pass

django_mongodb_engine/contrib/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def raw_update(self, spec_or_q, update_dict, **kwargs):
4646
queryset = self.filter(spec_or_q)
4747
queryset._for_write = True
4848
compiler = _compiler_for_queryset(queryset, 'SQLUpdateCompiler')
49-
compiler.execute_raw(update_dict, **kwargs)
49+
compiler.execute_update(update_dict, **kwargs)
5050

5151
raw_update.alters_data = True
5252

@@ -131,6 +131,10 @@ def inline_map_reduce(self, *args, **kwargs):
131131
def _get_query(self):
132132
return _compiler_for_queryset(self).build_query()
133133

134+
def distinct(self, *args, **kwargs):
135+
query = self._get_query()
136+
return query.collection.distinct(*args, **kwargs)
137+
134138
class MongoDBManager(models.Manager, RawQueryMixin):
135139
"""
136140
Lets you use Map/Reduce and raw query/update with your models::
@@ -147,3 +151,6 @@ def inline_map_reduce(self, *args, **kwargs):
147151

148152
def get_query_set(self):
149153
return MongoDBQuerySet(self.model, using=self._db)
154+
155+
def distinct(self, *args, **kwargs):
156+
return self.get_query_set().distinct(*args, **kwargs)

django_mongodb_engine/creation.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
from djangotoolbox.db.base import NonrelDatabaseCreation
33
from .utils import make_index_list
44

5+
56
class DatabaseCreation(NonrelDatabaseCreation):
67
data_types = dict(NonrelDatabaseCreation.data_types, **{
78
'AutoField': 'objectid',
89
'ForeignKey': 'objectid',
910
'OneToOneField': 'objectid',
10-
'RelatedAutoField' : 'objectid',
11+
'RelatedAutoField': 'objectid',
1112
'DecimalField': 'float',
1213
})
1314

@@ -34,6 +35,8 @@ def ensure_index(*args, **kwargs):
3435
self._handle_oldstyle_indexes(ensure_index, meta)
3536

3637
def _handle_newstyle_indexes(self, ensure_index, meta, indexes):
38+
from djangotoolbox.fields import AbstractIterableField, EmbeddedModelField
39+
3740
# Django indexes
3841
for field in meta.local_fields:
3942
if not (field.unique or field.db_index):
@@ -44,21 +47,35 @@ def _handle_newstyle_indexes(self, ensure_index, meta, indexes):
4447

4548
# Django unique_together indexes
4649
indexes = list(indexes)
50+
4751
for fields in getattr(meta, 'unique_together', []):
4852
assert isinstance(fields, (list, tuple))
4953
indexes.append({'fields': make_index_list(fields), 'unique': True})
5054

55+
def get_column_name(field):
56+
opts = meta
57+
parts = field.split('.')
58+
for i, part in enumerate(parts):
59+
field = opts.get_field(part)
60+
parts[i] = field.column
61+
if isinstance(field, AbstractIterableField):
62+
field = field.item_field
63+
if isinstance(field, EmbeddedModelField):
64+
opts = field.embedded_model._meta
65+
else:
66+
break
67+
return '.'.join(parts)
68+
5169
for index in indexes:
5270
if isinstance(index, dict):
5371
kwargs = index.copy()
5472
fields = kwargs.pop('fields')
5573
else:
5674
fields, kwargs = index, {}
57-
fields = [(meta.get_field(name).column, direction)
75+
fields = [(get_column_name(name), direction)
5876
for name, direction in make_index_list(fields)]
5977
ensure_index(fields, **kwargs)
6078

61-
6279
def _handle_oldstyle_indexes(self, ensure_index, meta):
6380
from warnings import warn
6481
warn("'descending_indexes', 'sparse_indexes' and 'index_together' are"

django_mongodb_engine/storage.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import os
2+
import urlparse
23

34
from gridfs import GridFS, NoFile
45

56
from django.core.exceptions import ImproperlyConfigured
67
from django.core.files.storage import Storage
8+
from django.utils.encoding import filepath_to_uri
9+
710

811
def _get_subcollections(collection):
912
"""
@@ -15,6 +18,7 @@ def _get_subcollections(collection):
1518
if cleaned != collection.name and cleaned.startswith(collection.name):
1619
yield cleaned
1720

21+
1822
class GridFSStorage(Storage):
1923
"""
2024
GridFS Storage backend for Django.
@@ -39,15 +43,24 @@ class GridFSStorage(Storage):
3943
:param database:
4044
Alias of the Django database to use. Defaults to 'default' (the default
4145
Django database).
46+
:param base_url:
47+
URL that serves the files in GridFS (for instance, through nginx-gridfs).
48+
Defaults to None (file not accessible through a URL).
4249
"""
4350

44-
def __init__(self, location='', collection='storage', database='default'):
51+
def __init__(self, location='', collection='storage', database='default',
52+
base_url=None):
4553
self.location = location.strip(os.sep)
4654
self.collection = collection
4755
self.database = database
56+
self.base_url = base_url
57+
4858
if not self.collection:
4959
raise ImproperlyConfigured("'collection' may not be empty")
5060

61+
if self.base_url and not self.base_url.endswith('/'):
62+
raise ImproperlyConfigured("If set, 'base_url' must end with a slash")
63+
5164
def _open(self, path, mode='rb'):
5265
"""
5366
Returns a :class:`~gridfs.GridOut` file opened in `mode`, or raises
@@ -104,6 +117,11 @@ def size(self, path):
104117
gridfs, filename = self._get_gridfs(path)
105118
return gridfs.get_last_version(filename=filename).length
106119

120+
def url(self, name):
121+
if self.base_url is None:
122+
raise ValueError("This file is not accessible via a URL.")
123+
return urlparse.urljoin(self.base_url, filepath_to_uri(name))
124+
107125
def created_time(self, path):
108126
"""
109127
Returns the datetime the file at `path` was created.

django_mongodb_engine/utils.py

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
import time
33
from pymongo import ASCENDING
4+
from pymongo.cursor import Cursor
45
from django.conf import settings
56
from django.db.backends.util import logger
67

@@ -39,29 +40,56 @@ def __init__(self, collection, db_alias):
3940
def __getattr__(self, attr):
4041
return getattr(self.collection, attr)
4142

42-
def logging_wrapper(method, npositional=1):
43+
def profile_call(self, func, args=(), kwargs={}):
44+
start = time.time()
45+
retval = func(*args, **kwargs)
46+
duration = time.time() - start
47+
return duration, retval
48+
49+
def log(self, op, duration, args, kwargs=None):
50+
args = ' '.join(str(arg) for arg in args)
51+
msg = '%s.%s (%.2f) %s' % (self.collection.name, op, duration, args)
52+
kwargs = dict((k, v) for k, v in kwargs.iteritems() if v)
53+
if kwargs:
54+
msg += ' %s' % kwargs
55+
if len(settings.DATABASES) > 1:
56+
msg = self.alias + '.' + msg
57+
logger.debug(msg, extra={'duration' : duration})
58+
59+
def find(self, *args, **kwargs):
60+
if not 'slave_okay' in kwargs and self.collection.slave_okay:
61+
kwargs['slave_okay'] = True
62+
return DebugCursor(self, self.collection, *args, **kwargs)
63+
64+
def logging_wrapper(method):
4365
def wrapper(self, *args, **kwargs):
44-
if npositional is not None:
45-
assert len(args) == npositional
46-
start = time.time()
47-
try:
48-
result = getattr(self.collection, method)(*args, **kwargs)
49-
finally:
50-
duration = time.time() - start
51-
msg = '%s.%s (%.3f) %s' % (self.collection.name, method, duration,
52-
' '.join(str(arg) for arg in args))
53-
if any(kwargs.itervalues()):
54-
msg += ' %s' % kwargs
55-
if len(settings.DATABASES) > 1:
56-
msg = self.alias + '.' + msg
57-
logger.debug(msg, extra={'duration' : duration})
58-
return result
66+
func = getattr(self.collection, method)
67+
duration, retval = self.profile_call(func, args, kwargs)
68+
self.log(method, duration, args, kwargs)
69+
return retval
5970
return wrapper
6071

61-
find = logging_wrapper('find')
6272
save = logging_wrapper('save')
6373
remove = logging_wrapper('remove')
64-
update = logging_wrapper('update', npositional=2)
65-
map_reduce = logging_wrapper('map_reduce', npositional=None)
74+
update = logging_wrapper('update')
75+
map_reduce = logging_wrapper('map_reduce')
76+
inline_map_reduce = logging_wrapper('inline_map_reduce')
6677

6778
del logging_wrapper
79+
80+
class DebugCursor(Cursor):
81+
def __init__(self, collection_wrapper, *args, **kwargs):
82+
self.collection_wrapper = collection_wrapper
83+
super(DebugCursor, self).__init__(*args, **kwargs)
84+
85+
def _refresh(self):
86+
super_meth = super(DebugCursor, self)._refresh
87+
if self._Cursor__id is not None:
88+
return super_meth()
89+
# self.__id is None: first time the .find() iterator is entered.
90+
# find() profiling happens here.
91+
duration, retval = self.collection_wrapper.profile_call(super_meth)
92+
kwargs = {'limit': self._Cursor__limit, 'skip': self._Cursor__skip,
93+
'sort': self._Cursor__ordering}
94+
self.collection_wrapper.log('find', duration, [self._Cursor__spec], kwargs)
95+
return retval

docs/source/conf.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# coding: utf-8
2-
import django_mongodb_engine
2+
import sys; sys.path.append('.')
3+
from .utils import get_current_year, get_git_head
34

45
project = 'Django MongoDB Engine'
5-
copyright = '2011, Jonas Haag, Flavio Percoco Premoli and contributors'
6-
version = release = ''.join(map(str, django_mongodb_engine.__version__))
6+
copyright = '2010-%d, Jonas Haag, Flavio Percoco Premoli and contributors' % get_current_year()
77

8-
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo',
9-
'sphinx.ext.coverage']
8+
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
109

1110
master_doc = 'index'
1211
exclude_patterns = ['_build']
@@ -22,27 +21,19 @@
2221

2322
# -- Options for HTML output ---------------------------------------------------
2423

25-
from subprocess import check_output, CalledProcessError
26-
GIT_HEAD = None
27-
try:
28-
GIT_HEAD = check_output(['git', 'rev-parse', 'HEAD'])
29-
except CalledProcessError:
30-
pass
31-
except OSError, exc:
32-
if exc.errno != 2:
33-
raise
34-
35-
3624
html_title = project
25+
3726
html_last_updated_fmt = '%b %d, %Y'
38-
if GIT_HEAD:
39-
html_last_updated_fmt += ' (%s)' % GIT_HEAD[:7]
40-
html_show_copyright = False
27+
git_head = get_git_head()
28+
if git_head:
29+
html_last_updated_fmt += ' (%s)' % git_head[:7]
30+
4131
html_theme = 'mongodbtheme'
4232
html_theme_path = ['mongodbtheme', '.']
33+
html_show_copyright = False
4334

4435
# Custom sidebar templates, maps document names to template names.
45-
html_sidebars = {'**' : 'sidebar-contribute.html'}
36+
html_sidebars = {'**' : ['localtoc.html', 'sidebar.html']}
4637

4738
# If false, no module index is generated.
4839
html_domain_indices = False

docs/source/mongodbtheme/sidebar-contribute.html renamed to docs/source/mongodbtheme/sidebar.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
<h3>⚠ Documentation version</h3>
2+
<p>
3+
These are the Git version docs.
4+
Docs for 0.4 (PyPI) are <a href="/0.4/">here</a>.
5+
</p>
6+
17
<h3>Help out!</h3>
28
<p>
39
To make this documentation even better, we'd love to receive your

docs/source/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from datetime import datetime
2+
from subprocess import check_output, CalledProcessError
3+
4+
def get_current_year():
5+
return datetime.utcnow().year
6+
7+
def get_git_head():
8+
try:
9+
return check_output(['git', 'rev-parse', 'HEAD'])
10+
except CalledProcessError:
11+
pass
12+
except OSError, exc:
13+
if exc.errno != 2:
14+
raise

0 commit comments

Comments
 (0)